最近项目中需要使用http请求对方数据,一开始考虑使用HttpClient来完成,其基本的流程如下:
虽然说就这么简单的3步,但其中很多都是模版代码,还要写各种异常处理,实际上还是比较麻烦的。
由于项目中的http请求相对简单,处理过程中不需要关心太多细节,网上搜了一番后,决定采用Spring自带的RestTemplate。
RestTemplate中,基本的关于http请求的接口主要有下面几个:
// get 请求
public <T> T getForObject();
public <T> ResponseEntity<T> getForEntity();
// post 请求
public <T> T postForObject();
public <T> ResponseEntity<T> postForEntity();
// put 请求
public void put();
// delete
public void delete()
// exchange
public <T> ResponseEntity<T> exchange()
exchange请求可以处理上面各种请求,可以说是一个综合的接口。
在我的项目中,主要就关心get和post接口。可以看到接口主要分两种xxxForObjcet
和xxxForEntity
,前者直接返回自定义的结果对象,只关注返回结果中的主要内容;后者返回一个ResponseEntity对象,除了返回内容外,还包含状态码、请求头等信息。
RestTemplate会自动构建结果对象,并将请求结果数据填入对象中。例如:
exchange(url, HttpMethod.GET, null, result.class);
上面这行代码,用于处理返回类型为简单类型的请求(这里说的简单是指没有范型,类型嵌套等,可能有误,我没有详细测试过)。
对于复杂对象,就需要使用一个名叫ParameterizedTypeReference
的类,T
就是你的结果对象,Spring会去解析其中的对象类型。
假设你有个范型返回类Response
:
public class Response<T> {
private Integer code;
private String message;
private T data;
假设T
为Map
类型,为了正确映射结果,使用方式如下:
exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Response<Map<String, String>>() {});
RestTemplate自动构建结果对象,但是能否正确地将结果值填入相应的变量中,取决于你是否正确设置了用于结果消息转换的转换器。Spring中有个各种消息转换器,他们的公共接口为HttpMessageConverter
。由于在我的项目中,返回的结果为json数据,因此我要设置一个用于将json字符串映射成java对象的转换器。
在设置转换器之前,首先看一下RestTemplate的无参构造函数:
public RestTemplate() {
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new ResourceHttpMessageConverter());
this.messageConverters.add(new SourceHttpMessageConverter<Source>());
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
this.messageConverters.add(new AtomFeedHttpMessageConverter());
this.messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jackson2XmlPresent) {
this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
}
else if (jaxb2Present) {
this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
}
可以看到,RestTemplate的无参构造函数中,会根据你所添加的依赖,加入一些转换器。在我项目中,使用Jackson对Json数据进行转换,因此RestTemplate会加入缺省构造的MappingJackson2HttpMessageConverter
。
但是这个缺省构造的转换器只会处理http返回中Content-type
类型为application/json
的数据,而我项目中的请求处理方,对于某些请求,虽然是返回json格式的数据,但是设置的Content-type
却是text/html
(第一个坑),所以在结果返回时直接报异常提示无法处理text/html
类型的数据,因此需要自定义一个MappingJackson2HttpMessageConverter
加入Restemplate对象中。如下:
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter();
httpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML));
restTemplate.getMessageConverters().add(httpMessageConverter);
return restTemplate;
}
这么操作之后,上面的异常没有了,但是又出现了新的问题,这个问题其实不是一个坑,是自己对Jackson和Spring不熟悉导致,json数据的命名通常是xxx_xxx_xxx
这种格式,java中对象的命名通常是驼峰格式,我原以为Spring会自动处理这种映射(因为Spring设置了很多默认规则),就导致结果无法正确写入到对象中,还需要修改Jackson的对象映射器,如下:
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter();
httpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML));
ObjectMapper objectMapper = httpMessageConverter.getObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); //命名策略
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); //忽略null数据
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); //未知属性不报错
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); //时区设置
restTemplate.getMessageConverters().add(httpMessageConverter);
return restTemplate;
}
经过上面一步处理后,返回结果能正确填入对象了。但是换了另外一种请求后,发现结果又无法正确写入了,发现无法正确写入的情况是http返回的Content-type
为application\json
的情况,然后发现自己定义的消息转换器覆盖了默认的MediaType
,于是加上:
httpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML,
MediaType.APPLICATION_JSON_UTF8, MediaType.APPLICATION_JSON));
但是加上之后,依旧无法正确写入,这个坑排查了好久,最终发现我在将httpMessageConverter
加入restTemplate
对象的时候,为了不影响其他类型的转换器,选择获取MessageConverters List
,并将其附加上去,但是从上面RestTemplate的无参构造函数可以看到,原先MessageConverters List
中已经加入了一个MappingJackson2HttpMessageConverter
对象,然后我又加入一个,导致RestTemplate对象中有两个不同配置的MappingJackson2HttpMessageConverter
对象,而在Spring的处理过程中,由于缺省配置的MappingJackson2HttpMessageConverter
对象在列表中的位置靠前,Spring检测到类型匹配后,直接用该对象对数据进行处理并返回,导致自定义配置的对象无法处理结果。
public T extractData(ClientHttpResponse response) throws IOException {
...
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericMessageConverter =
(GenericHttpMessageConverter<?>) messageConverter;
if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + this.responseType + "] as \"" +
contentType + "\" using [" + messageConverter + "]");
}
//一旦找到一个MediaType类型匹配的MessageConverter,直接处理并返回
return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
}
}
}
...
}
于是直接使用自定义的转换器覆盖原有的转换器列表,搞定。
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter();
... //自定义设置
restTemplate.setMessageConverters(Collections.singletonList(httpMessageConverter));
return restTemplate;
}
在调用RestTemplate请求接口的时候,对于请求的查询参数,有两种处理方式,一种是自己将参数拼接到url上,另外一种是将url写成Spring要求模版格式,并且将参数Map一起传入请求接口中:
// 方法1
exchange("https://api.my.com?abc=xxx", HttpMethod.GET, null, result.class);
// 方法2
Map<String, Object> params = Maps.newHashMap();
params.put("abc", "xxx");
exchange("https://api.my.com?abc={abc}", HttpMethod.GET, null, result.class, params);
这两种方法,我都嫌弃他要自己处理拼接字符串,方法2还要弄成模版(自己作),觉得很麻烦。
查询一番后,发现Spring提供了一个UriComponentsBuilder
用于拼接url:
private String appendQueryParam(String url, Map<String, Object> queryParams) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
for (Map.Entry<String, Object> entry : queryParams.entrySet()) {
builder = builder.queryParam(entry.getKey(), entry.getValue());
}
return builder.toUriString();
}
这样就得到了一个完整的url,但是我使用这个返回的url进行请求的时候,却报请求参数错误,经排查发现,builder.toUriString()
会进行encode
处理,而exchange
接口也会对url
进行encode
处理,导致url
被encode
了两次,造成的结果就是,假如url
中有"
号,第一次encode
会变成%22
,第二次encode
就变成了%2522
,即对%
做了一次encode
。
于是修改代码:
private static URI appendQueryParam(String url, Map<String, Object> queryParams) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
for (Map.Entry<String, Object> entry : queryParams.entrySet()) {
builder = builder.queryParam(entry.getKey(), entry.getValue());
}
return builder.build().toUri();
}
不返回String
类型的url
,直接返回URI
对象,就不会进行encode
,调用exchange
针对URI
的重载接口,搞定。