微信接入探秘(五)——万事俱备,只欠架构(API篇)

本文出处:http://blog.csdn.net/chaijunkun/article/details/53504856,转载请注明。由于本人不定期会整理相关博文,会对相应内容作出完善。因此强烈建议在原始出处查看此文。

微信接入的另外一个重点接口分类是主动调用接口。与被动回调接口不同,接口响应数据格式全部为JSON,调用方式也有很大不同。今天就来聊一聊这类接口的适配思路。

本专栏代码可在本人的CSDN代码库中下载,项目地址为:https://github.com/chaijunkun/wechat-common

让HTTP接口地址拼接效率更高

微信的主动调用接口使用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。那么这个动作都做了些什么呢?

Created with Raphaël 2.2.0 开始 设置域名domain 反射遍历当前类所有final、private的URLBean 清空绝对路径absoluteURL 是否启用了SSL absoluteURL = https:// absoluteURL += domain + relativeURL 是否遍历完所有的URLBean 结束 absoluteURL = http:// yes no yes no

这样当设置一个URLFactory的domain参数时,代码就会自动刷新对象内部所有private final修饰的URLBean的绝对路径。下面的例子是利用Spring生成tokenAPI实例的配置方法:

<!-- 微信获取Token相关API  -->
<bean id="tokenAPIURLFactory" class="com.github.chaijunkun.wechat.common.api.access.TokenAPIURLFactory">
	<property name="domain" value="${com.qq.weixin.mp.api.domain}" />
</bean>
<bean id="tokenAPI" class="com.github.chaijunkun.wechat.common.api.access.TokenAPI">
	<property name="urlFactory" ref="tokenAPIURLFactory" />
</bean>

让代码风格统一化

当调用微信接口时,可以预见的情况分为:返回为空(null);返回有数据,但调用失败;返回有数据,调用成功。分解的流程如下图所示:

Created with Raphaël 2.2.0 开始 调用API 是否返回为空(null) 抛出异常 结束 是否调用失败 提示错误信息 操作业务数据 yes no yes no

我们来看一下调用失败时,微信给我们返回什么内容:

{"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从无到有,从弱到强的过程。走过了弯路,踩过了坑,才知道写好代码不是件容易的事。起初这个项目只是为一个技术调研而随便写写的,最后一不小心写成了线上项目,现在想想还是挺意外的一件事。虽然它还有很多接口没有来得及适配,但是框架已经搭起来了,未适配的接口只需要照着现有思路补充即可。最后,希望我的这一系列文章能够给朋友们一些技术上的启发,不仅限于微信接入。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值