RestTemplate 微信接口 text/plain HttpMessageConverter
一、背景介紹
使用 Spring Boot 寫項目,需要用到微信接口獲取用戶信息。
在 Jessey 和 Spring RestTemplate 兩個 Rest 客戶端中,想到盡量不引入更多的東西,然後就選擇了 Spring RestTemplate 作為 網絡請求的 Client,然後就被微信接口擺了一道,然後踩了一個 RestTemplate 的坑。
二、第一個坑:被微信擺了一道
報錯信息是:
org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.solar.app.model.weixin.WxBaseUserInfo] and content type [text/plain]
- 1
之所以被微信擺了一道,是因為微信接口文檔雖說返回的是 Json 數據,但是同時返回的 Header 裏面的 Content-Type 值確是 text/plain 的!!
最終結果就是導致 RestTemplate 把數據從 HttpResponse 轉換成 Object 的時候,找不到合適的 HttpMessageConverter 來轉換!
我使用 RestTemplate 時配置 Bean 時使用默認的構造函數:
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
- 1
- 2
- 3
- 4
繼續看 RestTemplate() 默認構造函數都幹了啥:
/**
* Create a new instance of the {@link RestTemplate} using default settings.
* Default {@link HttpMessageConverter}s are initialized.
*/
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());// tag1
}
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
可以看到,RestTemplate() 默認構造函數設置了一系列 HttpMessageConverter。
我的項目裏引入了 com.fasterxml.jackson,所以 RestTemplate() 會構造一個 MappingJackson2HttpMessageConverter 加到它的 messageConverters 中,即上面的代碼:【tag1】
繼續看 MappingJackson2HttpMessageConverter() 默認構造函數:
/**
* Construct a new {@link MappingJackson2HttpMessageConverter} using default configuration
* provided by {@link Jackson2ObjectMapperBuilder}.
*/
public MappingJackson2HttpMessageConverter() {
this(Jackson2ObjectMapperBuilder.json().build());
}
/**
* Construct a new {@link MappingJackson2HttpMessageConverter} with a custom {@link ObjectMapper}.
* You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
* @see Jackson2ObjectMapperBuilder#json()
*/
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
可以看到,默認構造的 MappingJackson2HttpMessageConverter 中的 supportedMediaTypes 只支持:application/json 的 MediaType。
再看 RestTemplate 請求的流程,會執行到這裏:
/**
* Execute the given method on the provided URI.
* <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback};
* the response with the {@link ResponseExtractor}.
* @param url the fully-expanded URL to connect to
* @param method the HTTP method to execute (GET, POST, etc.)
* @param requestCallback object that prepares the request (can be {@code null})
* @param responseExtractor object that extracts the return value from the response (can be {@code null})
* @return an arbitrary object, as returned by the {@link ResponseExtractor}
*/
protected <T> T doExecute(URI url, HttpMethod method, RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor) throws RestClientException {
Assert.notNull(url, "‘url‘ must not be null");
Assert.notNull(method, "‘method‘ must not be null");
ClientHttpResponse response = null;
try {
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
response = request.execute();
handleResponse(url, method, response);
if (responseExtractor != null) {
return responseExtractor.extractData(response);// tag2
}
else {
return null;
}
}
catch (IOException ex) {
String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0, resource.indexOf(query) - 1) : resource);
throw new ResourceAccessException("I/O error on " + method.name() +
" request for \"" + resource + "\": " + ex.getMessage(), ex);
}
finally {
if (response != null) {
response.close();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
從 HttpResponse 中獲取數據實際是執行 【tag2】。這個操作由 HttpMessageConverterExtractor 類來完成:
@Override
@SuppressWarnings({"unchecked", "rawtypes", "resource"})
public T extractData(ClientHttpResponse response) throws IOException {
MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
return null;
}
MediaType contentType = getContentType(responseWrapper);// tag3, 微信返回的是 text/plain
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericMessageConverter = (GenericHttpMessageConverter<?>) messageConverter;
if (genericMessageConverter.canRead(this.responseType, null, contentType)) {// tag4
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + this.responseType + "] as \"" +
contentType + "\" using [" + messageConverter + "]");
}
return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
}
}
if (this.responseClass != null) {
if (messageConverter.canRead(this.responseClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + this.responseClass.getName() + "] as \"" +
contentType + "\" using [" + messageConverter + "]");
}
return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
}
}
}
throw new RestClientException("Could not extract response: no suitable HttpMessageConverter found " +
"for response type [" + this.responseType + "] and content type [" + contentType + "]");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
【tag4】處的代碼用於判斷 MappingJackson2HttpMessageConverter 是否支持 【tag3】 類型的 MediaType。
AbstractJackson2HttpMessageConverter:
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
if (!canRead(mediaType)) {// tag5
return false;
}
JavaType javaType = getJavaType(type, contextClass);
if (!logger.isWarnEnabled()) {
return this.objectMapper.canDeserialize(javaType);
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
if (this.objectMapper.canDeserialize(javaType, causeRef)) {
return true;
}
logWarningIfNecessary(javaType, causeRef.get());
return false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
AbstractHttpMessageConverter:
/**
* Returns {@code true} if any of the {@linkplain #setSupportedMediaTypes(List)
* supported} media types {@link MediaType#includes(MediaType) include} the
* given media type.
* @param mediaType the media type to read, can be {@code null} if not specified.
* Typically the value of a {@code Content-Type} header.
* @return {@code true} if the supported media types include the media type,
* or if the media type is {@code null}
*/
protected boolean canRead(MediaType mediaType) {
if (mediaType == null) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.includes(mediaType)) {
return true;
}
}
return false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
一路追蹤下來,可以確定,只要讓 MappingJackson2HttpMessageConverter 能處理頭部 Content-Type 為 text/plain 類型的 Json 返回值的話,我們就能讓其幫我們把 Json 反序列化成我們要的對象。
我們繼承 MappingJackson2HttpMessageConverter 並在構造過程中設置其支持的 MediaType 類型即可:
public class WxMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
public WxMappingJackson2HttpMessageConverter(){
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.TEXT_PLAIN);
setSupportedMediaTypes(mediaTypes);// tag6
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
【tag6】的代碼,會覆蓋其默認的 MediaType 設置。
然後把這個 WxMappingJackson2HttpMessageConverter 追加到 RestTemplate 的 messageConverters 消息轉換鏈中去:
@Bean
RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new WxMappingJackson2HttpMessageConverter());
return restTemplate;
}
- 1
- 2
- 3
- 4
- 5
- 6
我既不推薦把 WxMappingJackson2HttpMessageConverter 實例當作構造 RestTemplate 時的參數來構造 RestTemplate,也不推薦 使用新的 WxMappingJackson2HttpMessageConverter 替換 RestTemplate 默認構造中創建的 MappingJackson2HttpMessageConverter 實例,因為這兩種方式都會導致 Content-Type 為 application/json 的 Json 響應沒有轉換器來反序列化,所以最佳的方式還是“追加”。
三、第二個坑:RestTemplate 的使用
其實也不算坑,主要是我太蠢。
一開始我是這樣寫的:
@Override
public WxBaseUserInfo getBaseUserInfo(String access_token, String openid) {
String url = "https://api.weixin.qq.com/sns/userinfo";
Map<String, String> params = new HashMap<>();
params.put("access_token", access_token);
params.put("openid", openid);
params.put("lang", "zh_CN");
WxBaseUserInfo result = null;
try{
result = restTemplate.getForObject(url, WxBaseUserInfo.class, params);
}catch (RestClientException e){
LOGGER.error("getBaseUserInfo", e);
}
return result;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
但是,微信竟然提示我缺失 access_token !後來看 官方示例:REST in Spring 3: RestTemplate 才發現我用錯了!正確用法是這樣:
@Override
public WxBaseUserInfo getBaseUserInfo(String access_token, String openid) {
String url = "https://api.weixin.qq.com/sns/userinfo?" +
"access_token={access_token}&openid={openid}&lang={lang}";// tag7
Map<String, String> params = new HashMap<>();
params.put("access_token", access_token);
params.put("openid", openid);
params.put("lang", "zh_CN");
WxBaseUserInfo result = null;
try{
result = restTemplate.getForObject(url, WxBaseUserInfo.class, params);
}catch (RestClientException e){
LOGGER.error("getBaseUserInfo", e);
}
return result;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
註意以上【tag7】處占位符的用法!
然後,還是有問題:如果因為 access_token 或 openid 的不合法,微信接口會返回一下格式的數據:
{
"errcode":40003,"errmsg":"invalid openid"
}
- 1
- 2
- 3
經測試,當微信接口返回以上格式的錯誤信息 json 後,restTemplate.getForObject() 返回的仍然是一個我們想要的 WxBaseUserInfo 對象,但是該對象的任何字段都為 null!
經查,微信接口所有的錯誤時的 json 信息格式都如以上格式。然後迫不得己用一種很挫的方式來做“接口異常”處理:
public class WxError {
private Integer errcode;
private String errmsg;
// getter and setter...
@Override
public String toString() {
return "WxError{" +
"errcode=" + errcode +
", errmsg=‘" + errmsg + ‘\‘‘ +
‘}‘;
}
//---------- functions
public boolean valid(){
return errcode == null || errcode == 0;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
定義一個公共的錯誤信息類作為父類,所有微信正常返回的數據對象繼承該錯誤類。
public class WxBaseUserInfo extends WxError {
private String openid;
private String nickname;
private Integer sex;
private String province;
private String city;
private String country;
private String headimgurl;
private List<String> privilege;// tag8
private String unionid;
// getter and setter...
@Override
public String toString() {
return "WxBaseUserInfo{" +
"openid=‘" + openid + ‘\‘‘ +
", nickname=‘" + nickname + ‘\‘‘ +
", sex=" + sex +
", province=‘" + province + ‘\‘‘ +
", city=‘" + city + ‘\‘‘ +
", country=‘" + country + ‘\‘‘ +
", headimgurl=‘" + headimgurl + ‘\‘‘ +
", privilege=‘" + privilege + ‘\‘‘ +
", unionid=‘" + unionid + ‘\‘‘ +
‘}‘ + " " + super.toString();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
註意以上的【tag8】處,privilege 類型是 List! 如果類寫成 String 就會導致 Json 轉換失敗!
最終獲取用戶信息的方法變成了這樣子:
@Override
public WxBaseUserInfo getBaseUserInfo(String access_token, String openid) {
String url = "https://api.weixin.qq.com/sns/userinfo?" +
"access_token={access_token}&openid={openid}&lang={lang}";
Map<String, String> params = new HashMap<>();
params.put("access_token", access_token);
params.put("openid", openid);
params.put("lang", "zh_CN");
WxBaseUserInfo result = null;
try{
result = restTemplate.getForObject(url, WxBaseUserInfo.class, params);
if(null == result || !result.valid()){// tag9
LOGGER.error("getBaseUserInfo invalid: " + result);
result = null;
}
}catch (RestClientException e){
LOGGER.error("getBaseUserInfo", e);
}
return result;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
我這裏的處理的當微信接口未能返回預期的數據時,此方法返回 null。換成 Java8 的 Optional 來處理應該會更好。大家按需處理吧。
四、總結
就這麽一個簡單的過程,我竟然踩了這麽多坑,真是蠢。不過對也些東西的認識也加深了。如果您有更優雅的方式,請留言或者貼個鏈接呀,謝謝 :)
五、參考
- REST in Spring 3: RestTemplate
http://blog.csdn.net/kinginblue/article/details/52706155
RestTemplate 微信接口 text/plain HttpMessageConverter