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]
之所以被微信摆了一道,是因为微信接口文档虽说返回的是 Json 数据,但是同时返回的 Header 里面的 Content-Type 值确是 text/plain 的!!
最终结果就是导致 RestTemplate 把数据从 HttpResponse 转换成 Object 的时候,找不到合适的 HttpMessageConverter 来转换!
我使用 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。
我的项目里引入了 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"));
}
可以看到,默认构造的 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 响应没有转换器来反序列化,所以最佳的方式还是“追加”。
三、第二个坑: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;
}
但是,微信竟然提示我缺失 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
RestTemplate 微信接口 text/plain HttpMessageConverter的更多相关文章
- Django:之中间件、微信接口和单元测试
		
Django中间件 我们从浏览器发出一个请求 Request,得到一个响应后的内容 HttpResponse ,这个请求传递到 Django的过程如下: 也就是说,每一个请求都是先通过中间件中的 pr ...
 - asp.net C# 实现微信接口权限开发类
		
当前微信接口类已实现以下接口,代码上如果不够简洁的,请自行处理. 1.获取access_token 2.获取用户基本信息 3.生成带参数二维码 4.新增永久素材 5.新增临时素材 6.发送微信模版 7 ...
 - 练习题(登陆-进度条-微信接口判断qq-微信接口判断列车时刻表-)
		
1.写一个用户的登陆注册的界面,用户的密码用hashlib加密存在文件中,登陆时候,用户的密码要和文件中的密码一致才行 def sha(password): #加密函数 passwd = hashli ...
 - java微信接口之五—消息分组群发
		
一.微信消息分组群发接口简介 1.请求:该请求是使用post提交地址为: https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_t ...
 - python实现微信接口(itchat)
		
python实现微信接口(itchat) 安装 sudo pip install itchat 登录 itchat.auto_login() 这种方法将会通过微信扫描二维码登录,但是这种登录的方式确实 ...
 - C# 调用微信接口上传素材和发送图文消息
		
using Common;using Newtonsoft.Json.Linq;using System;using System.IO;using System.Net;using System.T ...
 - python实现微信接口——itchat模块
		
python实现微信接口——itchat模块 安装 sudo pip install itchat 登录 itchat.auto_login() 这种方法将会通过微信扫描二维码登录,但是这种登录的方 ...
 - Python3 获取RDS slowlog+微信接口报警
		
一.功能说明 二.代码详情 1.通过阿里sdk获取慢查询列表,格式化. 2.企业微信报警接口 3.deamon #!/usr/bin/python #-*- conding:utf-8 -*- fro ...
 - [转]SpeedPHP微信接口扩展
		
这个扩展实现了SP和微信公众平台的对接,1.0版暂时只实现了最简单的功能:绑定,收信息,回复信息. 扩展配置方法: $spConfig = array( 'mode' => 'debug ...
 
随机推荐
- LeetCode之“动态规划”:Word Break && Word Break II
			
1. Word Break 题目链接 题目要求: Given a string s and a dictionary of words dict, determine if s can be seg ...
 - Mina源码阅读笔记(七)—Mina的拦截器FilterChain
			
Filter我们很熟悉,在Mina中,filter chain的用法也类似于Servlet的filters,这种拦截器的设计思想能够狠轻松的帮助我们实现对资源的统一处理.我们先大致连接下mina中的f ...
 - ASP.NET Provider模式应用之SqlMembershipProvider类的剖析
			
太多了,先给个流程图吧 Provider模式就是GOF中的两种设计模式的应用:策略模式和工厂模式,在程序中使用好这个模型能够解除模块与模块之间的耦合甚至是DIP,同时,不管是ASP.NET MVC还是 ...
 - 解决Visual Studio 2017隐藏“高级保存选项”命令
			
Visual Studio提供高级保存选项功能,它能指定特定代码文件的编码规范和行尾所使用的换行符.在Visual Studio 2017中,该命令没有默认显示在“文件”菜单中.用户需要手工设置,才能 ...
 - 理解java值传递与引用传递
			
1.基本类型和引用类型在内存中的保存 Java中数据类型分为两大类,基本类型和对象类型.相应的,变量也有两种类型:基本类型和引用类型.基本类型的变量保存原始值,即它代表的值就是数值本身:而引用类型的变 ...
 - 简单了解JS中的几种遍历
			
忙了好一段时间,项目上线后终于有那么一点点空档期静下来整理一些问题了.当我们在开发项目的时候,用到遍历的地方肯定少不了,那么我们有那么多的遍历方法,在不同情况下用那种方法会更优雅而且还没bug呢? 首 ...
 - javascript学习(二)javascript常见问题总结
			
在js使用过程中,经常会碰到一些问题,本人利用闲暇时间整理了一些常见问题的解决方法,贴出来和大家分享,有需要的朋友可以参考下 1.JS中方法和变量都是区分大小写的 2.单引号.双引号在JS中没有特殊 ...
 - .net中的各种委托(Delegate、Action、Func)
			
1.Delegate,委托的鼻祖 protected delegate int ClassDelegate(int x, int y);//定义委托类型及参数 static void Main(str ...
 - 电商网站开发记录(三)  Spring的引入,以及配置详解
			
1.web.xml配置注解<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi=& ...
 - [转]FFMpeg框架代码阅读
			
简介 FFmpeg是一个集录制.转换.音/视频编码解码功能为一体的完整的开源解决方案. FFmpeg的开发是基于Linux操作系统,但是可以在大多数操作系统中编译和使用.FFmpeg支持MPEG.Di ...