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 来处理应该会更好。大家按需处理吧。

四、总结

就这么一个简单的过程,我竟然踩了这么多坑,真是蠢。不过对也些东西的认识也加深了。如果您有更优雅的方式,请留言或者贴个链接呀,谢谢 :)

五、参考

http://blog.csdn.net/kinginblue/article/details/52706155

时间: 2024-12-12 11:28:58

RestTemplate 微信接口 text/plain HttpMessageConverter的相关文章

postman测试接口报Content type &#39;text/plain;charset=UTF-8&#39; not supported解决方法

增加一个请求头管理器,添加content-type:application/json.并将请求修改为json数据传输试试. 参考:https://zhidao.baidu.com/question/1644957725975214740.html postman测试接口报Content type 'text/plain;charset=UTF-8' not supported解决方法 原文地址:https://www.cnblogs.com/xiaoni-fighting/p/12298436.

Force.com - 微信接口后台开发与配置

为寻找国内免费云资源作为微信后台,花了一天时间试用SinaAppEngine(SAE),调试太不方便用户体验差.新浪作为媒体公司技术功底经不起考验,亚马逊能推出AWS,新浪还不行!更好选项是百度BaiduAppEngine(BAE),但最近尽然开始收费,还是安心回到force.com,至少老外承诺免费的东西一直免费且可靠. 第一步,申请force.com账号,请至developer.force.com申请,如是www.salesforce.com申请下来的是作为客户的账号,不适合开发人员:第二步

asp.net C# 实现微信接口权限开发类

当前微信接口类已实现以下接口,代码上如果不够简洁的,请自行处理. 1.获取access_token 2.获取用户基本信息 3.生成带参数二维码 4.新增永久素材 5.新增临时素材 6.发送微信模版 7.网页授权获取用户基本信息 8.分享朋友圈 关于需要使用poststr字符串可以在asp.net 页面进行poststr配置 //获取素材列表 var jsonitem = new { type = "image", offset = 0, count = 999 }; JavaScrip

微信接口图文消息群发,预览

上来就直接贴代码了,具体问题放到最后说,这个类我将方法全都封装在里面了,基本上拿去修改一下就可以用了 using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Text.RegularExpressions;using Newtonsoft.Json;using System.Web.SessionState;using Website_CS;using mgtArt

微信接口校验

1.Servlet package com.itmayiedu.servlet; import java.io.IOException;import java.io.PrintWriter;import java.util.Date;import java.util.Map; import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.Http

java微信接口之五—消息分组群发

一.微信消息分组群发接口简介 1.请求:该请求是使用post提交地址为: https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=ACCESS_TOKEN   其中ACCESS_TOKEN是我们动态获取的.   发送的数据:(这里使用图文消息示例) { "filter":{ "group_id":"2" }, "mpnews":{ "me

练习题(登陆-进度条-微信接口判断qq-微信接口判断列车时刻表-)

1.写一个用户的登陆注册的界面,用户的密码用hashlib加密存在文件中,登陆时候,用户的密码要和文件中的密码一致才行 def sha(password): #加密函数 passwd = hashlib.sha256(bytes('wxtrkbc', encoding='utf-8')) passwd.update(bytes(password,encoding='utf-8')) return passwd.hexdigest() def register(user,passwd): #注册函

C#-MVC开发微信应用(2)--开始使用微信接口

微信应用使用场景和商机很多,所以这也是一个技术的方向,因此,有空研究下.学习下微信的相关开发,也就成为SNF完善的必要条件了.本系列文章希望从一个循序渐进的角度上,全面介绍微信的相关开发过程和相关经验总结,希望给大家了解一下相关的开发历程.本篇随笔主要基于上一篇<C#-MVC开发微信应用(1)--开始使用微信接口>的基础上进行深入的介绍,介绍微信消息的处理和应答的过程. 1.微信的消息应答交互 我们知道,微信的服务器架起了客户手机和开发者服务器的一个桥梁,通过消息的传递和响应,实现了与用户的交

C# 调用微信接口上传素材和发送图文消息

using Common;using Newtonsoft.Json.Linq;using System;using System.IO;using System.Net;using System.Text; /// <summary> /// 调用微信接口凭证access_token /// </summary> private static string test_access_token { get { return "XXXXXXXXXXXX"; } }