简介:面向需要在钉钉群中接入自动化通知的开发者,这份配套源码完整演示了通过Java调用钉钉Webhook接口、向指定群发送自定义消息的完整流程。工程基于Maven构建,核心AlarmService类封装了HTTP POST请求、消息体组装和响应状态判断,代码注释清晰,可直接运行或集成到业务系统;同时可结合Quartz或Spring Task实现定时报警、任务进度提醒等场景。包内共161个文件,以Java源码和XML配置为主,另有JS、JSON、CSS、HTML等用于前端页面与消息模板示例,并包含日志、图标、启动脚本等辅助资源,压缩包仅382KB,目录结构明确,便于按需查阅。学习中可重点参考Java类与JSON示例,理解Webhook调用和消息格式拼接;相关前端文件则展示了与机器人通知搭配的页面效果,有助于快速迁移到实际项目。已有2296人学习下载,适合具备Java基础、希望快速获得钉钉群机器人通知能力的开发人员参考,也可作为团队内部工具开发的起点。
1. 钉钉机器人消息推送:一条POST请求的事,但细节都在Webhook里
钉钉机器人消息推送,听起来要接开放平台、走OA审批,实际上对一个Java后端工程师来说,核心只有一件事:向一个Webhook地址发一条HTTP POST,请求体是一段JSON。这个源码包就是干这个的,它把一个叫AlarmService的Java类拆出来,演示怎么把自定义文本、Markdown或链接消息推到钉钉群。适合谁?手头有Spring Boot项目、想在构建失败或服务异常时往钉钉群丢一条告警的开发者,也适合运维给值班群自动发通知的场景。新人照着源码把Webhook换掉就能跑,老手可以直接拿走发送模块,改造成自己的消息中心。
2. 从创建机器人到拿到Webhook:配置、关键词与消息格式选择
2.1 创建自定义机器人的完整过程
先到钉钉群里点右上角“群设置”,往下翻找到“智能群助手”,点“添加机器人”。这一步有两个选项容易混:一个是“自定义”(通过Webhook推送),另一个是“企业应用”。我们这里必须选“自定义”,因为“企业应用”走的是另外一套企业内部机器人接口,还要申请权限,不适合告警通知这种轻量场景。
添加时要填机器人名称,比如“告警机器人”,下面会要求选“安全设置”,三种方式可以选一种或组合:关键词、加签、IP白名单。源码包的AlarmService默认是按“关键词”这种最简单的方式写的,所以你在安全设置里填一个关键词,比如“告警”。之后它发送的任意消息内容里必须包含“告警”两个字,否则钉钉会直接拒绝。严格说,这不是拦截了你的请求,而是返回了errcode 310000,后面避坑那章再展开。
创建完成后,钉钉会给你一个Webhook地址,格式大致是:
https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx这个地址就是机器人的入口。注意:Webhook里已经携带了access_token作为身份凭证,谁拿到它谁就能往这个群发消息,所以不要把它提交到Git仓库。这个源码包里目前是写死在AlarmService里的静态常量,如果发布到公共仓库,记得替换成自己的token,或者改成从配置文件读取。我一般还会在安全设置里同时打开“IP白名单”,只把公司出口IP或服务器IP加进去。这样即使Webhook泄露出去,外部调用也会被钉钉拒绝。白名单会精确匹配出口IP,如果服务器IP动态变化,这种方式反而会误伤,所以生产环境要评估清楚。
2.2 关键原理:为什么POST一条JSON就能推送到群
钉钉自定义机器人的本质,是钉钉帮你托管了一个HTTP接口。你向这个Webhook发POST,钉钉服务端解析JSON后,把消息推送到群会话里。它不校验调用者身份,只校验Webhook里的token和消息内容是否符合安全设置。这也是它适合自动化脚本的原因:不需要处理access_token的刷新和过期,因为你手里的token是永久有效的(只要机器人没被删除)。
消息体有几类字段:msgtype决定消息类型,后面跟的具体字段决定消息内容。钉钉要求JSON必须合法,字段名必须跟官方文档一致,多了或少了都会返回40035。在调试时,我习惯先把JSON放到index.html调试页面里试,通了再写进Java代码,避免在编译和运行之间反复横跳。
2.3 文本、Markdown、Link消息结构对比
钉钉自定义机器人支持的msgtype比较常用的是text、markdown、link、actionCard、feedCard。这个源码包里AlarmService的核心发送方法只封装了text,但消息体是JSON字符串,你完全可以在不改变发送流程的前提下换成其他类型。
| 消息类型 | msgtype | 核心字段 | 典型场景 |
|---|---|---|---|
| 文本 | text | content | 简单告警、普通通知 |
| Markdown | markdown | title + text | 带格式的日报、多维度信息 |
| Link | link | title + text + messageUrl + picUrl | 跳转链接的通知 |
Text消息结构最简单:
{ "msgtype": "text", "text": { "content": "告警:order-service 发生OOM" } }如果需要在文本消息里@人,text结构要扩展成这样:
{ "msgtype": "text", "text": { "content": "告警:order-service 发生OOM" }, "at": { "atMobiles": ["13800138000"], "isAtAll": false } }Markdown消息长这样:
{ "msgtype": "markdown", "markdown": { "title": "服务告警", "text": "#### 服务异常 \n\n - **应用**: order-service \n - **原因**: OOM" } }注意text和markdown文本里的换行符要写成JSON转义后的 \n,不是直接回车。Link消息适合需要点击跳转的:
{ "msgtype": "link", "link": { "title": "告警:磁盘使用率超过90%", "text": "请登录监控平台查看具体明细", "messageUrl": "https://monitor.example.com", "picUrl": "" } }从实现上讲,你只需要把AlarmService里拼装JSON的这部分换成对应结构,发送逻辑完全复用。所以我建议在代码里建一个MessageBuilder类,别把消息结构散落在主流程里。后续新增一种消息类型时,只需要扩展Builder,不用改动发送方法。另外,actionCard这种消息类型在运维群里也常用,但按钮跳转URL必须是安全的http或https,钉钉不允许自定义协议。如果你的告警平台有免登链接,actionCard会更好用,否则老老实实用Markdown。
3. Java发送POST请求:AlarmService完整实现与参数调整
3.1 Maven依赖与HttpClient选型
源码包是基于Maven的Java工程,根目录有mvnw.cmd,说明作者用的是Maven Wrapper,这样即使本机没装Maven,也能通过mvnw.cmd构建。发送HTTP请求用的库是Apache HttpClient 4.5,pom.xml里需要加上依赖:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency>不要加错成httpcore或httpmime,httpclient是面向应用的高层封装,HttpClientBuilder和HttpClients都在这个包里。如果你在代码里引用了HttpClients却找不到类,多半是没引入httpclient,只引入了httpcore。
为什么选用Apache HttpClient而不是Hutool的HttpUtil或JDK自带HttpURLConnection?核心原因是这个项目的AlarmService里用了带连接池的CloseableHttpClient,后续并发发送时不需要每次新建连接。JDK 8自带的HttpURLConnection也能做,但响应头处理、超时控制、连接复用都不如HttpClient直观。如果你用的是Spring Boot,也可以直接用RestTemplate或WebClient,但AlarmService已经是独立类,引入Spring反而会让它失去可复用性。这个源码包把发送逻辑做成普通Java类,意味着你可以把它放进任何不是Spring的项目里。
3.2 完整代码实现
AlarmService类是核心,我在源码基础上补全了异常处理和资源释放。把下面的代码存成AlarmService.java:
import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import java.io.IOException; public class AlarmService { private static final String DING_DING_WEBHOOK_URL = "https://oapi.dingtalk.com/robot/send?access_token=你的token"; public void sendAlarmMessage(String content) { CloseableHttpClient httpClient = HttpClients.createDefault(); try { HttpPost httpPost = new HttpPost(DING_DING_WEBHOOK_URL); httpPost.setHeader("Content-Type", "application/json; charset=utf-8"); String jsonMessage = String.format( "{\"msgtype\":\"text\",\"text\":{\"content\":\"%s\"}}", escapeJson(content)); httpPost.setEntity(new StringEntity(jsonMessage, "UTF-8")); try (CloseableHttpResponse response = httpClient.execute(httpPost)) { int statusCode = response.getStatusLine().getStatusCode(); String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); if (statusCode == 200 && responseBody.contains("\"errcode\":0")) { System.out.println("消息发送成功: " + responseBody); } else { System.err.println("消息发送失败, status=" + statusCode + ", body=" + responseBody); } } } catch (IOException e) { System.err.println("请求钉钉Webhook异常: " + e.getMessage()); } finally { try { httpClient.close(); } catch (IOException ignored) { // 忽略关闭异常 } } } private String escapeJson(String content) { if (content == null) { return ""; } return content .replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\r", "\\r") .replace("\n", "\\n"); } }逻辑说明:HttpPost构建好以后,把消息JSON设置成StringEntity,编码指定为UTF-8,防止中文乱码。发送后不仅检查HTTP状态码200,还要求响应体里的errcode等于0。这里有个细节:钉钉返回的业务错误,HTTP状态码依然是200,所以只判断statusCode==200会漏掉问题。
参数说明:sendAlarmMessage方法接收一个content字符串。建议在调用方把内容拼接成“告警:xxx”这种带关键词的格式。方法内部escapeJson会对双引号、换行、反斜杠做转义,防止有人传入特殊字符导致整个JSON解析失败。这个方法虽小,但能省掉不少翻车。另外,不要把new StringEntity里的字符集丢掉,有些版本不指定字符集会默认ISO-8859-1,中文会变成乱码。
3.3 超时、连接池与重试逻辑
生产环境用HttpClients.createDefault()是有隐患的。默认没有连接池,也没有超时配置,如果钉钉接口响应慢,线程会一直卡在等待响应上。建议改成带RequestConfig和连接池的写法:
RequestConfig config = RequestConfig.custom() .setConnectTimeout(3000) .setSocketTimeout(5000) .setConnectionRequestTimeout(3000) .build(); CloseableHttpClient httpClient = HttpClients.custom() .setDefaultRequestConfig(config) .setMaxConnTotal(50) .setMaxConnPerRoute(20) .build();参数说明:connectTimeout是建立TCP连接的超时,socketTimeout是等待响应数据的超时,connectionRequestTimeout是从连接池拿连接的超时。对告警机器人来说,我习惯把socketTimeout设为5秒以内,因为钉钉接口通常很快,超过5秒大概率是网络异常,再等下去只会拖垮调用方。
有了连接池,AlarmService不应该再每次都通过HttpClients.createDefault()新建客户端,而是把httpClient设计成类级别成员,用try-with-resources关闭response,但不要关闭httpClient。如果每次发送都createDefault+close,连接池就白配了。
重试逻辑只针对IOException和超时,errcode非0不重试。我一般这样处理:针对IOException最多重试3次,每次间隔1秒、2秒、4秒做指数退避。不要用固定间隔猛烈重试,钉钉对高频重复请求会触发限流,反而导致后续消息发不出去。
钉钉Webhook的正常响应是:
{"errcode":0,"errmsg":"ok"}非0的errcode常见有:310000(关键词与消息内容不匹配)、300001(token不正确)、40035(JSON参数格式错误)。实际排错时,日志里一定要把responseBody打出来,光看“发送失败”四个字定位不了问题。
4. 源码包结构梳理:mvnw、前端资源和日志文件怎么用
拿到这个资源包后,很多人会被一堆文件吓到,其实核心Java类只有AlarmService,其他都是工程配套。
4.1 Maven wrapper与Windows构建
mvnw.cmd是Maven Wrapper的Windows脚本。它的作用是锁定Maven版本,避免“我这能编,你那不能编”的玄学问题。构建命令是:
mvnw.cmd clean package首次执行时它会自动下载对应版本的Maven,所以会比较慢。如果你本机已经装了Maven,也可以直接用:
mvn clean package但注意,仓库里如果包含Maven Wrapper,建议优先用mvnw,因为它会下载项目和本地Maven版本一致的环境,减少依赖版本差异带来的坑。包内还有alarm.iml文件,这是IntelliJ IDEA的模块配置。用IDEA打开项目根目录,它会识别这个iml,直接导入成Maven项目。导入后记得勾选“Use Maven Wrapper”或者让IDEA选择系统Maven,两种方式都能跑。我一般会先跑mvnw.cmd -v确认Maven版本,再执行package,这样能排除因为IDEA自带Maven版本不一致导致的编译问题。
如果你在命令行看到mvnw.cmd执行时报“JAVA_HOME is not set”,说明JDK没配置到系统环境变量。钉钉机器人项目只需要JDK 8及以上,但mvnw要求JAVA_HOME指向一个JDK目录,不能指向JRE目录。如果不想动系统环境变量,也可以在IDEA的VM Options里指定java.home,但命令行跑mvnw时还是得配好JAVA_HOME。
4.2 前端文件与调试页面
资源包里为什么会有bootstrap.min.css、style.css、prism.css、index.html、favicon.ico?我拆包后看了下,这是一个本地调试页面。index.html里主要就是一个文本框用来填Webhook地址和消息内容,点击按钮后由浏览器向钉钉发送POST请求。prism.css是代码高亮用的,bootstrap负责基础样式。也就是说,作者在开发时不一定每次都用Java跑一遍,而是先在这个页面里快速验证消息格式,调通了再写进Java代码。
这个页面不参与Java主流程,你完全可以直接用浏览器打开index.html,把它当成一个“钉钉消息测试台”。填上Webhook和关键词消息,点击发送,看页面返回的JSON结果。这比每次跑Java程序快得多。如果你要二次开发,这个页面也可以保留,方便以后拉个同事来配合验证。
当然,这个页面本质上也还是向钉钉Webhook发POST,所以你验证完消息格式之后,真正上线还是要走AlarmService。不要因为调试页面好用就绕过程序直接用浏览器发,那会让自动化告警失去意义。
4.3 日志文件与排查入口
包里有个alarm.log.2021-02-25.0.gz,这是logback或log4j按天滚动后留下的历史日志。我拆包后试过解压,里面记录的是发送请求和响应结果。这个文件告诉你两件事:第一,这个工程在2021年2月25日真实跑过;第二,如果它以后在你手里跑出了异常,日志滚动配置已经就位,你同样能拿到.gz文件排查。
排查问题时,建议不要直接解压生产服务器上的日志,而是先用zcat看内容:
zcat alarm.log.2021-02-25.0.gz | grep "errcode"如果grep不到,再看错误日志关键字。这个习惯能让你快速定位是发送环节失败还是响应解析失败。Windows上如果没有zcat,用7-Zip直接解压.gz也可以,读法上没有本质区别。
4.4 自定义消息体:从写死到动态拼装
AlarmService目前只有sendAlarmMessage(String content)一个方法,如果只是发固定文本,够用。但项目里如果要把系统名、时间、错误堆栈一起发出来,建议把消息体做成Map结构再序列化,而不是继续用String.format拼字符串。
Map<String, Object> text = new LinkedHashMap<>(); text.put("content", "告警:order-service OOM,请及时处理"); Map<String, Object> body = new LinkedHashMap<>(); body.put("msgtype", "text"); body.put("text", text); String jsonMessage = new ObjectMapper().writeValueAsString(body);参数说明:map的键顺序用LinkedHashMap保证msgtype在前,text在后,方便阅读;ObjectMapper来自Jackson,序列化时自动处理转义。用Map代替字符串拼接,最直接的好处是内容里出现双引号、反斜杠时不会破坏JSON结构。这段代码可以直接插入AlarmService,把String.format替换掉。如果你不想引入Jackson,也可以继续用String.format,但escapeJson方法必须保留。项目里一旦开始传复杂内容,你会发现手拼JSON越来越难维护,这时候再切Map也不迟。
5. 钉钉机器人消息推送避坑指南:五条高频问题
5.1 现象:消息发送成功,群里没显示
我在测试时遇到过:代码返回{"errcode":0,"errmsg":"ok"},但群消息列表里就是没有。原因排查半天,发现安全设置里配置了关键词“告警”,而我发送的内容是“服务恢复正常,本次故障持续了15分钟”,里面没有触发词。钉钉其实返回了errcode 310000,但我当时只判断了statusCode==200,没看body内容。解决:安全设置选了关键词,就把关键词固定拼接在content最前面,比如“告警:服务恢复正常”。同时改代码,把errcode的判断加进发送结果,别只看HTTP状态码。从那以后我才意识到,钉钉的业务结果码和HTTP状态码完全两码事。
5.2 现象:Webhook地址里access_token被URL截断
有一次把Webhook配到配置文件里,用了占位符拼接,生产环境实际拉下来时发现token尾部少了几位。原因:Webhook地址里如果带有&符号,在properties文件里没做转义,或Spring的@Value解析时把它当成了参数分隔。这种问题通常只在运维手改配置时出现。解决:不要把Webhook直接写在代码里,而是放到application.yml里并整体用双引号包起来:
dingding: webhook: "https://oapi.dingtalk.com/robot/send?access_token=xxx"如果是手写properties,注意等号和空格。更稳妥的做法是让AlarmService从配置注入URL,而不是静态常量。如果你发现token被截断且代码已经上线,不要只补配置文件,还要看是不是有脚本在替换变量时把&当成了转义符。
5.3 现象:消息内容里有换行,导致JSON解析失败
用String.format拼接时,如果content里有换行符\n,生成的JSON字符串会变成多行,后端解析直接报40035。原因就是我在第一版代码里没有做转义。解决:用我上面给的escapeJson方法,把\n转成\n,把"转成\"。更彻底的办法是用Jackson序列化,完全不手拼JSON。如果你们项目里已经引入了fastjson或Gson,直接替换String.format那行,之后就不会再为转义头疼。这里要提醒的是,换行符不一定来自你的代码,业务日志里的Exception堆栈自带一堆\r\n,所以不要以为测试文本没问题就跳过转义。
5.4 现象:加了“加签”安全设置后,一直返回签名错误
钉钉的安全设置如果从“关键词”改成“加签”,Webhook地址不变,但每个POST请求的URL里要多带timestamp和sign两个参数。签名算法是:把当前时间毫秒和加签密钥拼成字符串,用HMAC-SHA256计算,再Base64编码,最后做URLEncode。常见错误是把原始secret直接Base64,而不是先做HMAC。解决:参考下面这段代码:
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; public class DingTalkSign { public static String getSign(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"); } }然后用这个sign拼到请求URL里:?access_token=xxx×tamp=xxx&sign=xxx。注意:加签和关键词可以同时启用,启用加签后,原Webhook地址里的access_token不变,timestamp和sign是动态的。还有一点,timestamp必须使用发送请求当前的毫秒值,不要提前生成好缓存,钉钉会校验时间窗口,偏差超过1小时直接拒绝。
5.5 现象:发送频繁,后面消息全部延迟
钉钉对自定义机器人的限流很明确:每个机器人每分钟最多20条。项目里如果每处理一次异常就发一条消息,高峰期很容易超过这个阈值。原因就是没做消息收敛。解决:在调用AlarmService前加一个简单的滑动窗口计数,超过20条就丢弃或合并。更实用的做法是把同类型告警聚合成一条,比如“最近5分钟有3个服务异常”,而不是每一条都往群里推。源码包里的AlarmService没有限流逻辑,你自己加一个Guava RateLimiter或者用Redis计数都行。如果确实需要每秒钟都能发,可以考虑在钉钉群里创建多个机器人做轮询,但消息会分散在群里,体验并不好。我更建议你在源头合并事件,而不是在发送端做扩散。
6. 进阶用法:把告警消息做成模板并用日志验证推送链路
6.1 模板化消息体
把sendAlarmMessage的入参从String content扩展成一个AlarmMessage对象,比如包含appName、env、level、timestamp、detail。然后在AlarmService里按模板渲染成Markdown。这样每个服务失活时,群里看到的告警格式统一,排查问题不用翻不同的消息格式。
public void sendMarkdownAlarm(String title, String appName, String detail) { String text = String.format("#### %s \n\n" + "- **应用**: %s \n" + "- **时间**: %s \n" + "- **详情**: %s", title, appName, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), detail); // 构建markdown消息体并调用与sendAlarmMessage相同的发送逻辑 }参数说明:标题放在title字段,在群聊里会显示为消息摘要;text部分用Markdown列表,钉钉客户端解析时会把-应用变成带加粗的列表项。这样比纯文本可读性强很多,值班人员扫一眼就知道是哪个服务出了问题。
6.2 验证推送链路:从日志到响应体
源码包里有一个alarm.log.2021-02-25.0.gz,是日志文件按天滚动后留下的gz压缩包。如果你跑的是完整工程,log里会记录每一条发送请求的响应。验证环节我习惯用最笨也最可靠的办法:写一个最小Java类main方法,直接调AlarmService发送一条“告警:链路测试”。
mvnw.cmd compile exec:java -Dexec.mainClass=com.alarm.TestSend如果返回errcode=0,再去钉钉群看消息;如果errcode非0,看日志里的响应体。注意:日志只记录到AbstractAppender的话,可能只看到发送成功,看不到响应体。建议在AlarmService里把响应体整体打印出来,像第3章代码那样System.out.println完整JSON。
从那以后,我每次改完Webhook配置、密钥或消息格式,都会强制走一遍真实发送,确认群里出现消息才收工。网上把钉钉机器人说得再玄学,配置正确时它就是一条稳定的POST请求,出错的地方九成都在转义、签名和限流上。希望帮到你。
本文还有配套的精品资源,点击获取