精品伊人久久大香线蕉,开心久久婷婷综合中文字幕,杏田冲梨,人妻无码aⅴ不卡中文字幕

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
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 時使用默認的構造函數:

@BeanRestTemplate 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:

@Overridepublic 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 消息轉換鏈中去:

@BeanRestTemplate 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 的使用

其實也不算坑,主要是我太蠢。
一開始我是這樣寫的:

@Overridepublic 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 才發現我用錯了!正確用法是這樣:

@Overridepublic 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 轉換失敗!

最終獲取用戶信息的方法變成了這樣子:

@Overridepublic 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 來處理應該會更好。大家按需處理吧。

四、總結

就這么一個簡單的過程,我竟然踩了這么多坑,真是蠢。不過對也些東西的認識也加深了。如果您有更優雅的方式,請留言或者貼個鏈接呀,謝謝 :)

五、參考

本站僅提供存儲服務,所有內容均由用戶發布,如發現有害或侵權內容,請點擊舉報
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
SPRING MVC3.2案例講解
JAVA版微信支付V3-完全版
QQ互聯OAuth2.0 .NET SDK 發布以及網站QQ登陸示例代碼
SpringMVC源碼剖析(五)
No HttpMessageConverter for java.util.LinkedHashMap and content type \"application/x-www-form-urlencoded;charset=UTF-8\
Spring MVC測試框架詳解
更多類似文章 >>
生活服務
分享 收藏 導長圖 關注 下載文章
綁定賬號成功
后續可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點擊這里聯系客服!

聯系客服

主站蜘蛛池模板: 聂拉木县| 乐山市| 承德市| 竹山县| 台北县| 秦皇岛市| 利川市| 阿克陶县| 东方市| 灌云县| 登封市| 涿州市| 静安区| 建瓯市| 洛阳市| 汾阳市| 藁城市| 彭泽县| 宜兰市| 泗水县| 汪清县| 樟树市| 汽车| 固始县| 六枝特区| 定远县| 镇安县| 海原县| 宜丰县| 安庆市| 无棣县| 瓮安县| 江西省| 亚东县| 峨边| 明溪县| 黔江区| 涿州市| 介休市| 平阳县| 利辛县|