Java集成钉钉API实战:AccessToken获取与消息推送指南
简介阿里钉钉集成APIJava是一套面向企业级Java开发者的钉钉开放平台对接方案覆盖从OAuth2.0授权、access_token获取、消息推送到企业通讯录与审批流管理的关键链路并附带可运行的Demo与源码分析适合需要将业务系统深度整合进钉钉的团队参考。压缩包共174个文件大小8.17MB其中包含32个java源码、38个class编译产物、30个jar依赖库以及html/jsp页面、js/css前端资源、xml配置和png/jpg素材、md说明文档等目录划分为src与demo两大模块便于按功能查阅。目前已有3452人学习浏览是一份结构清晰、上手门槛较低的实战资料。通过阅读源码、运行Demo并配合调试日志开发者可快速掌握钉钉API调用、消息卡片发送、组织架构管理等核心技能减少重复踩坑。 最近在给公司做一套内部自动化通知系统需求其实很简单工单状态变更、定时报表跑完、系统告警这些消息要能第一时间推到员工的钉钉上。一开始我还在纠结要不要自己写推送服务后来发现钉钉开放平台的API已经覆盖了消息、审批、通讯录、机器人这些核心场景直接用现成的能力比自己造轮子省太多事。这篇文章就把我用Java集成钉钉API的完整过程、代码片段和踩过的坑整理出来给正准备接钉钉的同学做个参考。整篇不绕弯子能直接复制的代码和配置我都会贴出来。1. 集成前的整体思路先搞清楚你要用钉钉的哪部分能力1.1 钉钉API能做什么以及最常用的三类场景钉钉开放平台的能力比我一开始想象的大得多。光是企业内部应用能调用的API就覆盖了消息通知、审批流、考勤、通讯录、日程、会议、群机器人、工作台等等。但落到实际项目里90%以上的Java后端集成需求其实集中在三个方向。第一个是消息推送就是往指定员工或者整个部门发送工作通知。这类需求最常见工单提醒、告警通知、报表推送、待办提醒全是这一种。第二个是审批流集成比如把内部业务系统的审批申请自动提交到钉钉审批或者反过来钉钉审批通过后把结果回传给我们自己的系统。第三个是通讯录同步很多公司会把钉钉组织架构作为主数据源把部门、员工信息拉取下来和自己系统的用户体系做映射。我这次做的主要是第一个场景顺带用了通讯录查询来根据手机号匹配userid。后面写的内容也会围绕这三块展开尤其是消息推送因为它是绝大多数集成的第一站。1.2 选型官方SDK还是裸HTTP调用我为什么推荐先学会HTTP调用钉钉的Java接入方式有两条路一条是用官方提供的SDK一条是直接用HTTP请求调API。很多新手上来就依赖SDK一个方法调用搞定但我还是建议先弄懂HTTP调用的原理再去用SDK。原因很简单SDK本质上就是对HTTP接口的封装你如果不知道底层请求长什么样出了问题会非常被动。比如返回errcode是40001你都不知道这个错误其实是token失效了。另外公司里很多线上环境用的还是老旧的JDK8或者受管控的依赖库SDK版本装不上、拉不下来是常有的事。学会用最基础的HTTP请求写一次调用至少能保证在任何环境下都能把接口跑通。从另一个角度说如果只用SDK遇到SDK版本和JDK不兼容的问题网上搜到的解决方案少之又少。而HTTP调用方案是通用的跟语言无关排查思路也透明。我实际项目里用的是新版SDK但排查问题的时候所有关键API我都会先用Postman或者命令行curl验证一遍确认是接口问题还是代码问题再回来看代码。2. 前置准备与工程初始化2.1 开发者后台配置企业内部应用、权限申请、发布在写代码之前必须先到钉钉开放平台后台把应用建好这一步漏了后续全是白费。登录open.dingtalk.com后进入开发者后台创建应用时有两个选择企业内部应用和第三方应用。如果只是给自己公司内部用选企业内部应用就够了审批流程短权限点也好申请。创建应用之后你会拿到几个关键参数这些后面写代码都要用AppKey、AppSecret、AgentId还有企业CorpId。这几个参数千万别写死到代码仓库里至少要放到配置中心或者环境变量里管起来。AppSecret尤其重要它是应用的密钥泄露了等于别人可以冒充你的应用调接口。然后是权限点申请。很多API不是创建应用就能直接调的需要在权限管理里申请对应的权限点。比如发工作通知就要申请工作通知消息发送权限查通讯录用户信息要申请通讯录个人信息读权限。权限点申请后一般需要一个审批流程企业内部应用通常几分钟就能过。这一步特别容易踩坑权限没申请或者申请了但应用没发布调用接口就会报没有权限或者无权限之类的错误。最后别忘了把应用发布。在版本管理与发布里发布一个正式版本设置好可见范围应用才算真正可用。开发阶段如果只是在应用后台点了调试没走发布流程部分API会一直返回无权限。2.2 Java工程环境检查与依赖引入集成钉钉API并不挑Java版本JDK8以上基本都能跑。不过因为要走HTTPS请求建议直接用JDK 11及以上版本java.net.http.HttpClient用起来比老旧的HttpURLConnection舒服太多。如果你的机器还没配好JAVA_HOME和环境变量先花十分钟把环境搞定否则后面编译会出现找不到或无法加载主类、ClassNotFoundException这类问题那不是代码的错是环境变量没配好。Maven工程的话要是用官方新版SDK只需要引入一个依赖dependency groupIdcom.aliyun/groupId artifactIddingtalk/artifactId version2.1.42/version /dependency这个依赖包很大下载慢是正常的。另外它传递依赖的版本有时候会和项目里已有的依赖冲突尤其是com.google.code.gson这类常见库如果启动时发现类冲突或者方法找不到优先检查依赖树把版本统一一下。如果不想用SDK那就简单了项目里只要有一个HTTP客户端就行。RestTemplate、OkHttp、HttpClient都行甚至不引第三方库直接用JDK原生的HttpClient也能做完整集成。我下面的示例大部分会用JDK原生的HttpClient写法这样你复制过去就能跑不需要额外引依赖。3. 核心API调用实战从获取AccessToken到发送消息3.1 获取AccessToken的正确姿势以及为什么不建议每次都重新获取钉钉API的鉴权方式和大多数开放平台一样先用AppKey和AppSecret换一个AccessToken后续的业务API都带着这个Token去调。这个Token本质上就是一张临时通行证有效期默认是7200秒也就是两个小时。获取Token的接口很简单import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class DingTalkTokenService { private static final String APP_KEY 你的AppKey; private static final String APP_SECRET 你的AppSecret; public static String getAccessToken() throws Exception { String url https://oapi.dingtalk.com/gettoken?appkey APP_KEY appsecret APP_SECRET; HttpClient client HttpClient.newBuilder().build(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); // 用你项目里已有的JSON库解析即可 return parseAccessTokenFromResponse(response.body()); } }返回的JSON结构长这样{ errcode: 0, errmsg: ok, access_token: xxxxx, expires_in: 7200 }有一个非常关键的细节钉钉获取token的接口有频率限制官方文档说同一应用获取AccessToken不能过于频繁短时间内反复调用会被限流直接返回错误或者临时封禁。所以绝对不能每次发消息前都调一次gettoken正确做法是把token缓存起来快到过期时间再刷新。简单点说就是用一个全局变量或者Redis存token加上一个过期时间。在高并发或者多实例部署的场景下要注意多台机器同时刷新token导致的请求风暴最好用分布式锁控制。我见过有团队每调用一次API就重新拿一次token结果线上被限流了一个多小时排查了很久才发现是这个原因。3.2 发送工作通知消息的标准流程与消息体结构拿到token之后发送工作通知就是一次标准的POST请求。接口地址是POST https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_tokenACCESS_TOKEN请求体示例{ agent_id: 123456789, userid_list: zhangsan,lisi, msg: { msgtype: text, text: { content: 工单 #20240601 已处理完成请及时关注。 } } }对应的Java代码用JDK原生HttpClient写大概是这样public class DingTalkMessageSender { public static void sendWorkNotification(String accessToken, Long agentId, String userIds, String content) throws Exception { String url https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2 ?access_token accessToken; String body { agent_id: %d, userid_list: %s, msg: { msgtype: text, text: { content: %s } } } .formatted(agentId, userIds, content); HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); } }这里有几个点要特别提醒。userid_list是钉钉内部的用户ID不是手机号也不是员工的工号。如果你手上只有手机号得先调用通讯录查询接口根据手机号查出userid再拼到userid_list里。还有接收人如果不在应用的可见范围内消息也会发送失败。另外agent_id填错了比如填成了AppKey接口会返回类似无效的agentId的错误所以这两个参数一定要区分清楚。消息内容msg字段支持很多类型text、markdown、link、actionCard、feedCard等等。实际使用中markdown类型特别实用可以加标题、加颜色、加链接比纯文本直观很多。构建markdown消息的时候只需要把msgtype改成markdown再填一个markdown.title和处理好的markdown文本就行。成功发送后返回结果里会带一个task_id这是这条消息任务的唯一标识可以用来查消息的送达状态。如果返回errcode是0、errmsg是ok基本就说明钉钉服务端接收成功了但接收人有没有真正看到那是异步送达的事查状态要调另一个查询接口。3.3 官方新版SDK的使用体验减少造轮子但别依赖过度前面说推荐先懂HTTP原理但实际项目里我会用官方新版SDK因为它的模型定义非常完整响应对象都帮你解析好了开发效率高很多。新版SDK坐标就是前面提到的那一个调用获取token的逻辑长这样import com.aliyun.dingtalkoauth2_1_0.*; import com.aliyun.teaopenapi.models.Config; public class NewSdkDemo { public static String getToken() throws Exception { Config config new Config(); config.protocol https; config.regionId central; com.aliyun.dingtalkoauth2_1_0.Client client new com.aliyun.dingtalkoauth2_1_0.Client(config); GetAccessTokenRequest request new GetAccessTokenRequest() .setAppKey(你的AppKey) .setAppSecret(你的AppSecret); GetAccessTokenResponse response client.getAccessToken(request); return response.getBody().getAccessToken(); } }新版SDK在包结构上做了很细的拆分不同模块是独立的包比如dingtalkoauth2_1_0负责tokendingtalkmessage_1_0负责消息dingtalkhrbot_1_0负责机器人你需要哪个能力就引哪个包。这个设计好处是依赖更清晰坏处是包多API名字有细微差别第一次用还是要翻文档。用了一周之后我的感受是SDK适合功能开发阶段能省很多时间线上排查问题阶段我还是习惯直接拼HTTP请求复现。所以建议你两边都要会HTTP是保底方案SDK是效率方案两者不冲突。4. 其他常用场景机器人、回调与通讯录查询4.1 自定义机器人Webhook和加签机制除了工作通知还有一类非常受欢迎的消息通道群机器人。在钉钉群里添加一个自定义机器人会得到一个Webhook地址往这个地址POST一条JSON消息消息就会出现在群里。这个方案最友好的地方在于不需要申请任何API权限也不需要AppKey和AppSecret只需要一个Webhook URL。但安全上要留意Webhook地址一旦泄露任何人都能往这个群里发消息。所以创建机器人的时候强烈建议开启加签安全设置。加签的逻辑是发起请求时带上当前时间戳毫秒把时间戳和密钥拼成字符串用HmacSHA256算法签名再把签名做URL编码放到请求里。import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; public class RobotSign { public static String generateSign(long timestamp, String secret) throws Exception { String stringToSign timestamp \n secret; Mac mac Mac.getInstance(HmacSHA256); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HmacSHA256)); byte[] signData mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); return URLEncoder.encode(Base64.getEncoder().encodeToString(signData), UTF-8); } }请求Webhook时的URL要带上timestamp和sign参数服务端会校验时间戳与签名是否匹配时间戳和当前时间偏差太大会拒绝。这里最大的坑是服务器时钟漂移如果服务器时间不准确加签一直会失败表现为同样的代码本地能跑通线上却一直401。排查方式很简单先date一下服务器时间差太多就要同步NTP。4.2 回调事件订阅与加解密消息推送是单向的钉钉往外推我们接收。但很多时候需要双向联动比如员工在钉钉上提交了审批我们希望审批通过后自己的系统能自动收到通知。这就需要配置事件订阅回调。配置回调分两步。第一步在开发者后台事件订阅里填一个回调URL和一个AES密钥。第二步钉钉会先发一个验证请求来确认URL是你的这个验证请求非常绕它不是普通JSON而是加过密的。你需要完成解密、解析出msgSignature、timeStamp、nonce、encrypt等字段算好签名再按指定格式返回响应验证才能通过。验证的核心是四步用token、timestamp、nonce、encrypt拼字符串算SHA1签名解base64解码后的AES密文拿AES密钥解密出JSON把JSON里的echostr原样返回。第一次做这个流程我大概花了半天时间主要是钉钉文档里对密钥的格式描述得不是很直观AES密钥长度、加解密模式有严格限制。这里送你一个经验回调URL的处理逻辑单独写一个controller不要把业务逻辑混进去。因为回调是高频请求而且有超时要求如果业务处理太重被拖到响应超时钉钉会认为回调失败然后不停重试。我一般是先返回成功把事件丢到MQ或者线程池里异步处理。4.3 通讯录查询与用户ID映射绑定消息接收人这一步几乎绕不开通讯录接口。业务系统里存的是手机号或者工号而钉钉消息接口要的是userid所以要做一层映射。获取用户信息的接口是POST https://oapi.dingtalk.com/topapi/v2/user/getbyunionid实际上更常用的是先通过手机号查用户接口是topapi/v2/user/getbymobile。请求体和返回体都是JSON请求只需要传一个mobile字段返回里带出userid、name、unionid这些关键信息。我建议在公司内部维护一个缓存表把员工手机号和钉钉userid的映射关系定期同步到本地库不要每次发消息都现查钉钉接口。因为通讯录接口同样有频率限制而且发一批通知可能涉及几十上百人实时查询既慢又容易触发限流。定时任务每天同步一次就好员工入职离职的增量变化再单独处理。5. 常见问题与排查技巧实录5.1 高频报错与解决方案速查表我整理了一下集成过程中最容易遇到的一批报错按错误码和现象分好了类你遇到类似问题可以直接对着查。现象/错误码可能原因解决办法40001AccessToken无效或过期重新获取token确认AppKey/AppSecret正确检查token是否被其他环境覆盖40002参数错误核对agent_id、userid_list、msg结构是否符合接口文档40003无效用户userid不是当前企业下的或者传成了手机号/邮箱40005无权限权限点未申请或者应用未发布、接收人不在可见范围内40101签名不匹配机器人加签的timestamp和签名算法有误检查服务器时间41002企业不存在corpId填错了确认是企业CorpId而不是应用ID46001请求频率超限触发了频控检查token是否缓存消息发送是否做了批量控制回调查验失败加密解密不匹配AES密钥长度或编码不对签名算法顺序错了这里要特别记一下错误码40005是最容易被忽略的。很多人代码逻辑完全正确但忘了在后台把权限点申请走完或者忘了发布应用导致明明本地测试没问题一上生产就报无权限。我在预发环境验证时用的还是测试应用发到正式环境前忘了给正式应用申请工作通知消息发送权限结果整整排查了一个下午。5.2 几个必须注意的隐形坑第一个坑是Token缓存的时间差。官方说token有效期7200秒但偶尔在临近过期的时间段调用会出现间歇性的401。我采取的做法是缓存时间设置成7000秒留出200秒的余量宁可多刷新一次也不要线上大面积失败。第二个坑是分布式环境下Redis缓存token的数据类型。网上有很多用Redis存token并计数防止超限的代码但如果你用RedisTemplate的increment方法做计数返回值一定要用Long接收。我见过同事在这里踩了个大坑方法返回的是Integer强转后直接抛不是Integer或out of range异常。原因很简单Redis整型自增返回的是64位长整型你用32位的Integer去接当然会溢出。第三个坑是Java编译环境和Lombok的版本冲突。如果你用的是新版钉钉SDK并且项目里启用了Lombok启动时可能会报You arent using a compiler supported by lombok, so lombok will not work这类警告严重时会直接导致类加载失败。这个问题的本质是Lombok不认当前的JDK版本升级Lombok插件版本到1.18.30以上或者把JDK版本调整到和Lombok匹配的版本就好。5.3 一次线上消息发送失败的完整排查思路最后分享一个真实的排查案例希望能帮你形成一套顺手的排错顺序。当时现象是定时任务半夜发报表通知部分人收到了部分人没收到。我拿到问题第一反应先去查钉钉的调用日志看看接口返回的task_id是否存在、errcode是否为0。日志显示发送都成功了说明钉钉服务端已经接收。接下来按task_id调消息查询接口发现部分消息状态是已读部分消息状态是未读还有几条状态是发送失败。查看失败详情原因是接收人已经离职userid在钉钉里被清理了。这时候再去查通讯录果然这些员工的手机号已经不在组织架构里。问题根源就清楚了我们的本地映射表同步不及时离职员工的userid还残留着导致发消息时报无效用户。后续的修复方案一个是同步任务里增加离职员工标记另一个是发送接口调用前先批量校验userid是否有效。排查下来整个过程其实不复杂但如果一开始不去查日志而是一条条核对代码效率会低很多。说实话钉钉这类开放平台的问题绝大多数都出在配置和参数上代码层面的问题反而少。所以排查思路一定是配置参数优先代码逻辑次之最后才是环境因素。本文还有配套的精品资源点击获取