Sprint Boot:RestTemplate使用及踩坑

文章目录

  • 背景
  • RestTemplate的使用
    • 简单对象映射
    • 复杂对象映射
  • RestTemplate踩坑
    • 坑1: Content-type类型处理
    • 坑2: Json字段名与Java对象名映射
    • 坑3: RestTemplate中MessageConverter类型重复
    • 坑4: Url encode

背景

最近项目中需要使用http请求对方数据,一开始考虑使用HttpClient来完成,其基本的流程如下:

  1. 创建一个http请求。
  2. 设置请求参数。
  3. 获取返回结果。

虽然说就这么简单的3步,但其中很多都是模版代码,还要写各种异常处理,实际上还是比较麻烦的。
由于项目中的http请求相对简单,处理过程中不需要关心太多细节,网上搜了一番后,决定采用Spring自带的RestTemplate。

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接口。可以看到接口主要分两种xxxForObjcetxxxForEntity,前者直接返回自定义的结果对象,只关注返回结果中的主要内容;后者返回一个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;

假设TMap类型,为了正确映射结果,使用方式如下:

exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Response<Map<String, String>>() {});

RestTemplate踩坑

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

坑1: Content-type类型处理

但是这个缺省构造的转换器只会处理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;
}

坑2: Json字段名与Java对象名映射

这么操作之后,上面的异常没有了,但是又出现了新的问题,这个问题其实不是一个坑,是自己对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;
}

坑3: RestTemplate中MessageConverter类型重复

经过上面一步处理后,返回结果能正确填入对象了。但是换了另外一种请求后,发现结果又无法正确写入了,发现无法正确写入的情况是http返回的Content-typeapplication\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;
}

坑4: Url encode

在调用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处理,导致urlencode了两次,造成的结果就是,假如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的重载接口,搞定。

你可能感兴趣的:(Spring,Boot)