二十一、Spring Rest Template 常见错误
一般而言,微服务之间的通信大多都是使用 HTTP 方式进行的,这自然少不了使用 HttpClient。在不使用 Spring 之前,一般都是直接使用 Apache HttpClient 和 Ok HttpClient 等,而一旦引入 Spring,就有了一个更好的选择,这就是RestTemplate。
一.参数类型是 MultiValueMap(HashMap会被转换为JSON对象,使用 RestTemplate 提交表单必须是 MultiValueMap)
1、Controller代码
@RestController
public class HelloWorldController {
@RequestMapping(path = "hi", method = RequestMethod.POST)
public String hi(@RequestParam("para1") String para1, @RequestParam("para2") String para2){
return "helloworld:" + para1 + "," + para2;
};
}
2、客户端请求
RestTemplate template = new RestTemplate();
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("para1", "001");
paramMap.put("para2", "002");
String url = "http://localhost:8080/hi";
String result = template.postForObject(url, paramMap, String.class);
System.out.println(result);
代码定义了一个 Map,包含了 2 个表单参数,然后使用 RestTemplate 的 postForObject 提交这个表单。
执行之后,返回提示 400 错误,即请求出错:
3、案例解析
使用Wireshark 抓包工具进行抓包
从上图可以看出,实际上是将定义的表单数据以 JSON 请求体(Body)的形式提交过去了,所以接口处理自然取不到任何表单参数。
当我们使用的 Body 是一个 HashMap 时,会进行JSON 序列化的,将一个MAP转换为JSON对象。
使用 RestTemplate 提交表单必须是 MultiValueMap,而我们案例定义的就是普通的 HashMap,最终是按请求 Body 的方式发送出去的。
二.当 URL 中含有特殊字符(当 URL 中含有特殊字符时,一定要注意 URL 的组装方式,最好对URL进行提前组装)
1、Controller代码
@RestController
public class HelloWorldController {
@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi(@RequestParam("para1") String para1){
return "helloworld:" + para1;
};
}
2、客户端调用
String url = "http://localhost:8080/hi?para1=1#2";
HttpEntity<?> entity = new HttpEntity<>(null);
RestTemplate restTemplate = new RestTemplate();
HttpEntity<String> response = restTemplate.exchange(url, HttpMethod.GET,entity,String.class);
System.out.println(response.getBody());
客户端调用hi,入参是para1=1#2,我们期望获得controller方法输出的是helloworld:1#2,但是实际输出的是helloworld:1,因为服务器并不认为#2是para1的内容。
3、案例解析
debug模式下可以看到,#2并不是消失了,而是以Fragment(锚点) 的方式被记录下来了。
1.URL 的格式定义
URL 的格式定义:protocol://hostname[:port]/path/[?query]#fragment
1.Query(查询参数)
页面加载请求数据时需要的参数,用 & 符号隔开,每个参数的名和值用 = 符号隔开。
2.Fragment(锚点)
#开始,字符串,用于指定网络资源中的片断。例如一个网页中有多个名词解释,可使用 Fragment 直接定位到某一名词的解释。例如定位网页滚动的位置,可以参考下面一些使用示例:
http://example.com/data.csv#row=4 – Selects the 4th row.
http://example.com/data.csv#col=2 – Selects 2nd column.
4、问题修正(提前对url进行编码)
String url = "http://localhost:8080/hi?para1=1#2";
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
URI uri = builder.build().encode().toUri();
HttpEntity<?> entity = new HttpEntity<>(null);
RestTemplate restTemplate = new RestTemplate();
HttpEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET,entity,String.class);
System.out.println(response.getBody());
关键步骤
String url = "http://localhost:8080/hi?para1=1#2";
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
URI uri = builder.build().encode().toUri();
当 URL 中含有特殊字符时,一定要注意 URL 的组装方式,尤其是要区别下面这两种方式:
三.小心多次 URL Encoder(使用正确的URL组装方式,避免多次编码)
1、Controller
@RestController
public class HelloWorldController {
@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi(@RequestParam("para1") String para1){
return "helloworld:" + para1;
};
}
2、客户端调用
RestTemplate restTemplate = new RestTemplate();
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/hi");
builder.queryParam("para1", "开发测试 001");
String url = builder.toUriString();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
System.out.println(forEntity.getBody());
期望结果是"helloworld: 开发测试 001"
但是实际结果却是helloworld:%E5%BC%80%E5%8F%91%E6%B5%8B%E8%AF%95001
3、案例解析
关键点就在于
String url = builder.toUriString();
builder.toUriString()执行的是UriComponentsBuilder.toUriString:
public String toUriString() {
return this.uriVariables.isEmpty() ?
build().encode().toUriString() :
buildInternal(EncodingHint.ENCODE_TEMPLATE).toUriString();
}
.encode()
可以看到又进行了一次编码
public final UriComponents encode() {
return encode(StandardCharsets.UTF_8);
}
4、问题修正
RestTemplate restTemplate = new RestTemplate();
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/hi");
builder.queryParam("para1", "开发测试 001");
URI url = builder.encode().build().toUri();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
System.out.println(forEntity.getBody());
避免多次转化而发生多次编码。
关键点在于
5、正确组装URL的方法
RestTemplate restTemplate = new RestTemplate();
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/hi");
builder.queryParam("para1", "开发测试 001");
URI url = builder.encode().build().toUri();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
System.out.println(forEntity.getBody());