使用 Spring Boot 寫項目,須要用到微信接口獲取用戶信息。java
在 Jessey 和 Spring RestTemplate 兩個 Rest 客戶端中,想到儘可能不引入更多的東西,而後就選擇了 Spring RestTemplate 做爲 網絡請求的 Client,而後就被微信接口擺了一道,而後踩了一個 RestTemplate 的坑。web
報錯信息是:spring
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]
之因此被微信擺了一道,是由於微信接口文檔雖然說返回的是 Json 數據,可是同時返回的 Header 裏面的 Content-Type 值確是 text/plain 的!!json
最終結果就是致使 RestTemplate 把數據從 HttpResponse 轉換成 Object 的時候,找不到合適的 HttpMessageConverter 來轉換!api
我使用 RestTemplate 時配置 Bean 時使用默認的構造函數:微信
@Bean RestTemplate restTemplate(){ return new RestTemplate(); }
繼續看 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()); } }
能夠看到,RestTemplate() 默認構造函數設置了一系列 HttpMessageConverter。app
個人項目裏引入了 com.fasterxml.jackson,因此 RestTemplate() 會構造一個 MappingJackson2HttpMessageConverter 加到它的 messageConverters 中,即上面的代碼:【tag1】ide
繼續看 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")); }
能夠看到,默認構造的 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(); } } }
從 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 + "]"); }
【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; }
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; }
一路追蹤下來,能夠肯定,只要讓 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 } }
【tag6】的代碼,會覆蓋其默認的 MediaType 設置。
而後把這個 WxMappingJackson2HttpMessageConverter 追加到 RestTemplate 的 messageConverters 消息轉換鏈中去:
@Bean RestTemplate restTemplate(){ RestTemplate restTemplate = new RestTemplate(); restTemplate.getMessageConverters().add(new WxMappingJackson2HttpMessageConverter()); return restTemplate; }
我既不推薦把 WxMappingJackson2HttpMessageConverter 實例看成構造 RestTemplate 時的參數來構造 RestTemplate,也不推薦 使用新的 WxMappingJackson2HttpMessageConverter 替換 RestTemplate 默認構造中建立的 MappingJackson2HttpMessageConverter 實例,由於這兩種方式都會致使 Content-Type 爲 application/json 的 Json 響應沒有轉換器來反序列化,因此最佳的方式仍是「追加」。
其實也不算坑,主要是我太蠢。
一開始我是這樣寫的:
@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; }
可是,微信居然提示我缺失 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; }
注意以上【tag7】處佔位符的用法!
而後,仍是有問題:若是由於 access_token 或 openid 的不合法,微信接口會返回一下格式的數據:
{ "errcode":40003,"errmsg":"invalid openid" }
經測試,當微信接口返回以上格式的錯誤信息 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; } }
定義一個公共的錯誤信息類做爲父類,全部微信正常返回的數據對象繼承該錯誤類。
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(); } }
注意以上的【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; }
我這裏的處理的當微信接口未能返回預期的數據時,此方法返回 null。換成 Java8 的 Optional 來處理應該會更好。你們按需處理吧。
就這麼一個簡單的過程,我居然踩了這麼多坑,真是蠢。不過對也些東西的認識也加深了。若是您有更優雅的方式,請留言或者貼個連接呀,謝謝 :)
http://blog.csdn.net/kinginblue/article/details/52706155