本文出处:http://blog.csdn.net/chaijunkun/article/details/53504856,转载请注明。由于本人不定期会整理相关博文,会对相应内容作出完善。因此强烈建议在原始出处查看此文。
微信接入的另外一个重点接口分类是主动调用接口。与被动回调接口不同,接口响应数据格式全部为JSON,调用方式也有很大不同。今天就来聊一聊这类接口的适配思路。
本专栏代码可在本人的CSDN代码库中下载,项目地址为:https://code.csdn.net/chaijunkun/wechat-common
微信的主动调用接口使用HTTP方式实现,严格意义上来说是HTTPS,这样就保证了传输过程的安全性。让我们先从文档中随便看几个接口的地址:
获取access_token接口:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
获取用户基本信息接口:
https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
在评估过其他接口后总结到结论:除去业务参数外,接口使用的协议和域名相同,路径不同。在笔者的工作中也涉及到了类似的开发实践:正式环境中使用域名DomainA;测试环境中使用域名DomainB,通过指定host来访问特定的测试环境。这样做的好处就是测试和线上隔离得非常彻底,避免因为使用相同域名导致访问混乱(尤其是带有写入功能的接口)。微信的接入相对于笔者上述的需求更加简单,因为不涉及到访问微信的测试环境,域名一直是线上域名,测试时使用的账号不同而已,这与接口地址无关。
当然,你可以在接入每个接口时都写入完整地址,但是这样就损失掉了代码的可维护性。举个最简单的例子,在微信官方文档中有如下描述:
开发者可以根据自己的服务器部署情况,选择最佳的接入点(延时更低,稳定性更高)。除此之外,可以将其他接入点用作容灾用途,当网络链路发生故障时,可以考虑选择备用接入点来接入。
1. 通用域名(api.weixin.qq.com),使用该域名将访问官方指定就近的接入点;
2. 上海域名(sh.api.weixin.qq.com),使用该域名将访问上海的接入点;
3. 深圳域名(sz.api.weixin.qq.com),使用该域名将访问深圳的接入点;
4. 香港域名(hk.api.weixin.qq.com),使用该域名将访问香港的接入点。
系统上线后你可能发现访问速度不甚理想,而选择了特定地区的域名后访问速度变得很快。如果你每个接口都写完整路径那就杯具了。于是我们有动机要实现一个需求:接口域名可配,一改全改。
你或许会想:那还不简单?把配置的域名拿到,每次调用接口的时候对地址进行拼接:”https://”+Domain+”/cgi-bin/….”。不可否认,这样做确实可以实现功能,然而这样做够高效吗?我们的服务启动后,拿到微信接口的域名配置,此后的启动-运行生命周期内几乎不会再对该值进行修改。每次都进行拼接是对计算和内存资源的浪费,最好是加载一次就生成一个固定的链接,每次都拿这个生成好的地址。
于是我们创建了一个这样的URL配置对象:
public class URLBean {
/** 相对地址 */
private final String relativeURL;
/** 绝对地址 */
private String absoluteURL;
/** * 构建URL封装对象 * @param relativeURL 相对路径,初始化后不可修改 */
public URLBean(final String relativeURL) {
this.relativeURL = relativeURL;
this.absoluteURL = relativeURL;
}
/** * 获取相对地址 * @return 相对地址 */
public String getRelativeURL() {
return relativeURL;
}
/** * 获取绝对地址 * @return 绝对地址 */
public String getAbsoluteURL() {
return absoluteURL;
}
}
注意,这里的URLBean不是一个严格意义上的Bean,其中的相对地址relativeURL被修饰为final,对象的构造函数中对其进行初始化赋值,赋值后就不能被修改。绝对地址absoluteURL在表面上是一个只读属性,并且默认是和相对地址relativeURL一样的,没有被final修饰。先不急,它的作用一会儿介绍。我们先创建一个接口地址工厂,顾名思义,该工厂是用来生产对应接口的完整地址的。以获取access_token接口为例:
public class TokenAPIURLFactory extends AbstractURLFactory {
/** 接入令牌接口URL */
private final URLBean token = new URLBean("/cgi-bin/token");
/** * 获取接入令牌接口URL * @return 接入令牌接口URL */
public String getToken() {
return token.getAbsoluteURL();
}
}
相信你看完上面这段代码更让人一头雾水了。创建了一个被final修饰过的URLBean,只写了一个相对地址,然后就给出了一个获取完整地址的getToken()方法,怎么生成的完整地址?来,接着看它的父类AbstractURLFactory里面都写了什么:
public abstract class AbstractURLFactory {
/** 是否使用https */
private Boolean enableSSL;
/** 域名 */
private String domain;
/** * 获取是否使用https * @return 是否使用https */
public Boolean getEnableSSL() {
return enableSSL;
}
/** * 获取域名 * @return 域名 */
public String getDomain() {
return domain;
}
/** * 递归设置domain * @param enableSSL * @param domain * @param clazz */
private void recursiveSetDomain(Boolean enableSSL, String domain, Class<?> clazz){
if (null == clazz){
return;
}
//获取所有字段
Field[] declaredFields = clazz.getDeclaredFields();
//特定修饰符字段筛选器
int modifierFilter = Modifier.PRIVATE | Modifier.FINAL;
boolean hasDomain = StringUtils.isNotBlank(domain);
if (hasDomain){
domain = domain.trim();
}
//默认开启SSL
boolean useSSL = (null == enableSSL ? true : enableSSL);
for (Field field : declaredFields) {
//筛选特定字段
if (modifierFilter != (modifierFilter & field.getModifiers())){
continue;
}
//筛选指定类型类型
if (URLBean.class != field.getType()){
continue;
}
field.setAccessible(true);
try{
URLBean urlBean = (URLBean) field.get(this);
Field relativeURL = urlBean.getClass().getDeclaredField("relativeURL");
Field absoluteURLField = urlBean.getClass().getDeclaredField("absoluteURL");
relativeURL.setAccessible(true);
absoluteURLField.setAccessible(true);
if (hasDomain){
//这里不使用String.format是考虑到有可能以后相对URL中存在%s通配符
if (useSSL){
absoluteURLField.set(urlBean, "https://".concat(domain).concat((String)relativeURL.get(urlBean)));
}else{
absoluteURLField.set(urlBean, "http://".concat(domain).concat((String)relativeURL.get(urlBean)));
}
}else{
absoluteURLField.set(urlBean, relativeURL.get(urlBean));
}
}catch(Exception e){
//忽略错误
}
}
recursiveSetDomain(enableSSL, domain, clazz.getSuperclass());
}
/** * 设置是否使用https * @param enableSSL 是否使用https */
public void setEnableSSL(Boolean enableSSL) {
this.enableSSL = enableSSL;
//防止属性设置先后不同步的问题,每一次属性的改变都要刷新URL
recursiveSetDomain(this.enableSSL, this.domain, getClass());
}
/** * 设置域名 * @param domain 域名 */
public void setDomain(String domain){
this.domain = domain;
//防止属性设置先后不同步的问题,每一次属性的改变都要刷新URL
recursiveSetDomain(this.enableSSL, this.domain, getClass());
}
}
里面是一些通用的配置信息:
enableSSL:是否启用SSL(默认启用)
domain:接口使用的域名
与普通的Bean不同在于,这个抽象的URL工厂配置属性都是是只写(write-only)的,并且写入之后附加了递归设置域名的动作recursiveSetDomain。那么这个动作都做了些什么呢?
这样当设置一个URLFactory的domain参数时,代码就会自动刷新对象内部所有private final修饰的URLBean的绝对路径。下面的例子是利用Spring生成tokenAPI实例的配置方法:
<!-- 微信获取Token相关API -->
<bean id="tokenAPIURLFactory" class="net.csdn.blog.chaijunkun.wechat.common.api.access.TokenAPIURLFactory">
<property name="domain" value="${com.qq.weixin.mp.api.domain}" />
</bean>
<bean id="tokenAPI" class="net.csdn.blog.chaijunkun.wechat.common.api.access.TokenAPI">
<property name="urlFactory" ref="tokenAPIURLFactory" />
</bean>
当调用微信接口时,可以预见的情况分为:返回为空(null);返回有数据,但调用失败;返回有数据,调用成功。分解的流程如下图所示:
我们来看一下调用失败时,微信给我们返回什么内容:
{"errcode":40013,"errmsg":"invalid appid"}
当调用成功时返回的内容(以获取access_token接口为例):
{"access_token":"ACCESS_TOKEN","expires_in":7200}
通读文档后发现:所有的调用失败返回数据格式都是一样的。根据业务不同,调用成功时的数据格式各自有很大的不同,但是调用任何一个接口都有失败的可能。因此我们把调用失败时的数据抽象成了所有返回对象的父类:
@JsonInclude(Include.NON_NULL)
public abstract class WeChatAPIRet implements Serializable {
private static final long serialVersionUID = 2422896542684235099L;
/** 成功返回的代码 */
public static final int CODE_OK = 0;
/** 错误代码 */
@JsonProperty(value = "errcode")
private Integer errcode;
/** 错误消息 */
@JsonProperty(value = "errmsg")
private String errmsg;
/** * 判断是否是成功返回 * @return */
public boolean isSuccess(){
if (null == errcode || errcode == CODE_OK){
return true;
}else{
return false;
}
}
//一些getters和setters,这里省略...
}
判断是否调用成功是个经常性的行为,因此为了简化判断逻辑,加入了一个isSuccess()方法,当返回结果中没有errcode字段,或者errcode字段等于0,则表示调用成功,其他情况认为调用失败。
然后定义一个接口调用正常返回时的数据结构映射(以获取access_token接口为例):
public class TokenResult extends WeChatAPIRet {
private static final long serialVersionUID = -8242372755146179695L;
/** 获取到的凭证 */
@JsonProperty(value = "access_token")
private String accessToken;
/** 凭证有效时间,单位:秒 */
@JsonProperty(value = "expires_in")
private Integer expiresIn;
//一些getters和setters,这里省略...
}
JSON转换组件会根据当时返回的数据进行字段匹配,无论成功还是失败都将生成一个TokenResult对象,在业务中直接调用其继承下来的isSuccess()方法即可判断是否成功,相关伪代码如下:
private void toDoSomething(TokenParam param) throws WeChatAPIException {
TokenResult token = tokenAPI.getToken(param);
if (null == token){
throw new WeChatAPIException(APIErrEnum.SysErr, new IllegalStateException("获取到的token为空"));
}
if (!token.isSuccess()){
throw new WeChatAPIException(token.getErrcode(), token.getErrmsg());
}
try {
//TODO 业务方面的操作
} catch (IOException e) {
throw new WeChatAPIException(APIErrEnum.SysErr, e);
}
}
简单来说,只要你的返回结果继承自WeChatAPIRet,在使用过程中的代码风格就会自然而然保持一致了。这也是Java作为工业化编程语言的一个特点。
经过很长时间的酝酿积累,终于完成了微信接入探秘系列的文章。感觉自己在写这些文字的时候又回顾了一遍wechat-common从无到有,从弱到强的过程。走过了弯路,踩过了坑,才知道写好代码不是件容易的事。起初这个项目只是为一个技术调研而随便写写的,最后一不小心写成了线上项目,现在想想还是挺意外的一件事。虽然它还有很多接口没有来得及适配,但是框架已经搭起来了,未适配的接口只需要照着现有思路补充即可。最后,希望我的这一系列文章能够给朋友们一些技术上的启发,不仅限于微信接入。