当前位置:首页 > 资讯 > info6 > 正文

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

发表于: 2016-12-07   作者:chaijunkun   来源:转载   浏览:
摘要: 本文出处:http://blog.csdn.net/chaijunkun/article/details/53504856,转载请注明。由于本人不定期会整理相关博文,会对相应内容作出完善。因此强烈建议在原始出处查看此文。微信接入的另外一个重点接口分类是主动调用接口。与被动回调接口不同,接口响应数据格式全部为JSON,调用方式也有很大不同。今天就来聊一聊这类接口的适配思路。本专栏代码可在本人的CSD

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

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

本专栏代码可在本人的CSDN代码库中下载,项目地址为:https://code.csdn.net/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.1.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="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);返回有数据,但调用失败;返回有数据,调用成功。分解的流程如下图所示:

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

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

编辑推荐
电影《飞屋环游记》里漂亮的小屋子可是羡煞旁人。如今,这幢房子已经走出童话故事,来到我们的现实
微信蓝牙BLE接入调试指引 Author: Thornton,447116230@qq.com 硬件篇 1 平台框架简介 微信蓝牙BLE由
微信蓝牙BLE接入调试指引 第三方服务器篇 3 构建第三方服务器 服务器的功能主要是接收微信发过来的
  微信公众平台开发-01-接入篇   在微信用户达到4亿用户的时候,我相信很多开发者已经加入了微
微信登录授权开发 1. 到微信开发平台注册相关APP,现在是等待审核成功后才能获取到对应的key和secre
1 创建一个web工程 1.1 创建一个servlet类:CheckSignature package com.weixin; import java.io.IOE
在微信的应用上,微信支付是一个比较有用的部分,但也是比较复杂的技术要点,在微商大行其道的年代
参考接入指南:http://mp.weixin.qq.com/wiki/17/2d4265491f12608cd170a95559800f2d.html 登录微信
【微信公众号对于开发者来说】 接触微信应该比大部分人都早, 记得那会开了微信号,大多数人还不知
现在实习的公司要做微信开发,然而一直没安排任务,所以一直在看微信接口,记录下学习的内容 微信开
版权所有 IT知识库 CopyRight © 2009-2015 IT知识库 IT610.com , All Rights Reserved. 京ICP备09083238号