钉钉官方的SDK对钉钉API提供了简单的封装,但官方的SDK使用起来较为臃肿,并且最重要的是官方SDK仅仅是封装了API,对于accessToken的维护以及消息回调处理等等都没有封装,在项目中大规模使用比较麻烦,因此萌生了对钉钉官方SDK进行全面重构的想法

钉钉官方SDK存在的一些问题或不足

我们在使用官方SDK的过程中发现官方SDK存在一些比较蛋疼的问题:

  1. accessToken需要自己来维护,SDK并不维护,那么在实际使用中就要做好accessToken的过期处理、多线程刷新时保证刷新是正确的
  2. 除了自己维护accessToken外,基本上每次调用都需要将accessToken作为参数传入,没有自动化的将固定参数补齐
  3. 消息回调的加解密并不包含在SDK中,需要自己整合进来
  4. 对不同消息如何处理应该在SDK层面来解决掉,这样业务层面就只需要关心具体的处理逻辑即可,但SDK并不包含这些
  5. 不支持HTTP代理,当代码部署在内网后没办法直接和钉钉API交互,需要借助代理来完成交互,但SDK并不支持代理且由于第6点的原因想优化SDK却不好下手优化
  6. SDK封装方式比较蛋疼,底层又是用到了淘宝的SDK,不是很容易来修改
  7. 当然还有其他的小问题,比如调用SDK方法时并不知道需要传哪些参数所以需要对照着开发文档才能发起调用等等。

基于以上的原因,官方SDK不满足我们的实际场景太多了,用起来太蛋疼了,主要是:

  1. 不维护accessToken
  2. 没有加解密
  3. 没有消息路由
  4. 不支持代理
  5. 对照开发文档才能使用

我们是完全抛弃了钉钉官方SDK,从零搭建出了一套Java版本的钉钉SDK,并且已经开源出来了,可见https://github.com/tingyugetc520/DtJava,希望大家能多多点点Star、Fork,跪谢。

比官方SDK还好用的钉钉SDK

自认为DtJava(钉钉 Java SDK)对开发者更加友好,使用起来比官方SDK好用多了。

基本的API调用封装

下面简单比较下官方SDK和DtJava之间的调用方式。
首先是官方SDK的调用方式:

DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/user/get");
OapiUserGetRequest req = new OapiUserGetRequest();
req.setUserid("userid1");
req.setHttpMethod("GET");
OapiUserGetResponse rsp = client.execute(req, accessToken);

整个调用过程就是

  1. 初始化一个DingTalkClient出来,初始化的同时指定API的URL地址是啥
  2. 初始化请求参数,并设置好参数与请求方式
  3. 获取accessToken,在这一步中还要处理好过期刷新的问题
  4. 发起请求

在这个调用过程中,完全没必要这样来使用,我们可以将API的地址、参数、请求全部封装起来,整体对外暴露成一个方法,再进一步封装可以将accessToken封装进方法中。

这样封装之后基本上类似DtJava的调用方式:

DtUser user = dtService.getUserService().getById(userId);

一行代码便足够了,当然在这个背后整体的调用逻辑是

  1. 获取到对应的API地址
  2. 拼接设置参数
  3. 获取accessToken,包含了过期处理等等逻辑
  4. 发起请求
  5. 解析响应并封装成对应的Bean

全部的逻辑都被封装起来,具体的封装细节大家可以到GitHub上面先Star后查看源码(求Star求魔怔了),对开发者而言就是一个方法的调用即可,比官方SDK友好的多。

回调消息处理

除了基本的API调用外,钉钉的开放平台还有一个很重要的部分就是事件回调,在通讯录同步等等场景中回调发挥着重要的作用。但官方SDK并没有涉足到这一部分,对于开发者而言需要自己去整合加解密的流程以及对不同消息的处理逻辑。但好在官方已经将这部分的代码举例出来了:

public Map<String, String> callBack(HttpServletRequest request,
                                    @RequestParam(value = "msg_signature", required = false) String msg_signature,
                                    @RequestParam(value = "timestamp", required = false) String timeStamp,
                                    @RequestParam(value = "nonce", required = false) String nonce,
                                    @RequestBody(required = false) JSONObject json) {
    try {
        // 1. 从http请求中获取加解密参数

        // 2. 使用加解密类型
        // Constant.OWNER_KEY 说明:
        // 1、开发者后台配置的订阅事件为应用级事件推送,
        //      此时OWNER_KEY为应用的APP_KEY(企业内部应用)或SUITE_KEY(三方应用)。
        // 2、调用订阅事件接口订阅的事件为企业级事件推送,
        //      此时OWNER_KEY为:企业的CORP_ID(企业内部应用)或SUITE_KEY(三方应用)。
        // 其中的DingCallBackCrypto参见https://github.com/open-dingtalk/dingtalk-callback-Crypto
        DingCallbackCrypto callbackCrypto = new DingCallbackCrypto(Constant.AES_TOKEN, Constant.AES_KEY, Constant.OWNER_KEY);
        String encryptMsg = json.getString("encrypt");
        String decryptMsg = callbackCrypto.getDecryptMsg(msg_signature, timeStamp, nonce, encryptMsg);

        // 3. 反序列化回调事件json数据
        JSONObject eventJson = JSON.parseObject(decryptMsg);
        String eventType = eventJson.getString("EventType");

        // 4. 根据EventType分类处理
        if ("check_url".equals(eventType)) {
            // 测试回调url的正确性
            bizLogger.info("测试回调url的正确性");
        } else if ("user_add_org".equals(eventType)) {
            // 处理通讯录用户增加事件
            bizLogger.info("发生了:" + eventType + "事件");
        } else {
            // 添加其他已注册的
            bizLogger.info("发生了:" + eventType + "事件");
        }

        // 5. 返回success的加密数据
        Map<String, String> successMap = callbackCrypto.getEncryptedMap("success");
        return successMap;
    } catch (DingTalkEncryptException e) {
        e.printStackTrace();
    }
    return null;
}

通过官方示例,开发者已经基本上能够完成消息回调的处理。

但这仅仅是能够完成回调消息处理,我相信随着业务发展会出现很多的if...else...,在后续的开发维护中就变得比较麻烦了,当然大家都会想办法将这里优化掉,DtJava也同样做了这个优化的工作。

在DtJava中,消息通过消息路由器来完成不同消息的处理。

// 消息路由器
DtMessageRouter newRouter = new DtMessageRouter(dtService);

// 通讯录用户增加事件
newRouter.rule().async(false).eventType("user_add_org").handler(this.contactUserAddHandler).end();

// 默认,记录所有事件的日志
newRouter.rule().async(true).handler(this.logHandler).end();
return newRouter;

消息路由器DtMessageRouter主要是从eventType来匹配不同的handler,降低系统复杂度,更多的内容可以查看Wiki,这里不再介绍了。

在最后再放一下地址:https://github.com/tingyugetc520/DtJava,还望大家能够多多Star。