一、背景介绍

使用 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的更多相关文章

  1. Django:之中间件、微信接口和单元测试

    Django中间件 我们从浏览器发出一个请求 Request,得到一个响应后的内容 HttpResponse ,这个请求传递到 Django的过程如下: 也就是说,每一个请求都是先通过中间件中的 pr ...

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

    当前微信接口类已实现以下接口,代码上如果不够简洁的,请自行处理. 1.获取access_token 2.获取用户基本信息 3.生成带参数二维码 4.新增永久素材 5.新增临时素材 6.发送微信模版 7 ...

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

    1.写一个用户的登陆注册的界面,用户的密码用hashlib加密存在文件中,登陆时候,用户的密码要和文件中的密码一致才行 def sha(password): #加密函数 passwd = hashli ...

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

    一.微信消息分组群发接口简介 1.请求:该请求是使用post提交地址为: https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_t ...

  5. python实现微信接口(itchat)

    python实现微信接口(itchat) 安装 sudo pip install itchat 登录 itchat.auto_login() 这种方法将会通过微信扫描二维码登录,但是这种登录的方式确实 ...

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

    using Common;using Newtonsoft.Json.Linq;using System;using System.IO;using System.Net;using System.T ...

  7. python实现微信接口——itchat模块

    python实现微信接口——itchat模块 安装 sudo pip install itchat 登录 itchat.auto_login()  这种方法将会通过微信扫描二维码登录,但是这种登录的方 ...

  8. Python3 获取RDS slowlog+微信接口报警

    一.功能说明 二.代码详情 1.通过阿里sdk获取慢查询列表,格式化. 2.企业微信报警接口 3.deamon #!/usr/bin/python #-*- conding:utf-8 -*- fro ...

  9. [转]SpeedPHP微信接口扩展

    这个扩展实现了SP和微信公众平台的对接,1.0版暂时只实现了最简单的功能:绑定,收信息,回复信息. 扩展配置方法: $spConfig = array(     'mode' => 'debug ...

随机推荐

  1. Learning ROS for Robotics Programming Second Edition学习笔记(一) indigo v-rep

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS for Robotics Pr ...

  2. 【Android 应用开发】Android中的回调Callback

    回调就是外部设置一个方法给一个对象, 这个对象可以执行外部设置的方法, 通常这个方法是定义在接口中的抽象方法, 外部设置的时候直接设置这个接口对象即可. 例如给安卓添加按钮点击事件, 我们创建了OnC ...

  3. 【59】Quartz+Spring框架详解

    什么是Quartz Quartz是一个作业调度系统(a job scheduling system),Quartz不但可以集成到其他的软件系统中,而且也可以独立运行的:在本文中"job sc ...

  4. GCC/gcc/g++/CC/cc区别

    平常在Linux上经常会用到gcc或者g++来编译程序,但对这两者的理解也就停留在一个是用来编译C程序,另一个是用来编译C++程序的(请注意:这种说法是有问题的,待会改进). 1. GCC GCC,是 ...

  5. 苹果新的编程语言 Swift 语言进阶(十)--类的继承

    一.类的继承 类能够从其它类继承方法.属性以及其它特性,当一个类从另外的类继承时,继承的类称为子类,它继承的类称为超类.在Swift中,继承是类区别与其它类型(结构.枚举)的基础行为. 1.1 .类的 ...

  6. C++语言之类class

    在现实世界中,经常有属于同一类的对象.例如,你的自行车只是世界上很多自行车中的一辆.在面向对象软件中,也有很多共享相同特征的不同的对象:矩形.雇用记录.视频剪辑等.可以利用这些对象的相同特征为它们建立 ...

  7. Leetcode_263_Ugly Number

    本文是在学习中的总结,欢迎转载但请注明出处:http://blog.csdn.net/pistolove/article/details/49431329 Write a program to che ...

  8. The 13th tip of DB Query Analyzer, powerful processing EXCEL file

    The 13thtip of DB Query Analyzer, powerful processing EXCEL file MA Genfeng (Guangdong UnitollServic ...

  9. Python_PyMySQL数据库操作

    连接数据库: conn=pymysql.connect(host=,user=',charset='utf8') 建立游标: cur = conn.cursor() 创建一个名字叫 lj 的数据库: ...

  10. Next Permutation 下一个排列

    Implement next permutation, which rearranges numbers into the lexicographically next greater permuta ...