ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

钉钉机器人消息推送:Java对接Webhook与告警避坑指南

钉钉机器人消息推送:Java对接Webhook与告警避坑指南 简介面向需要在钉钉群中接入自动化通知的开发者这份配套源码完整演示了通过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_tokenxxxxxxxx这个地址就是机器人的入口。注意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核心字段典型场景文本textcontent简单告警、普通通知Markdownmarkdowntitle text带格式的日报、多维度信息Linklinktitle 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.5pom.xml里需要加上依赖dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId version4.5.13/version /dependency不要加错成httpcore或httpmimehttpclient是面向应用的高层封装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.javaimport 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; charsetutf-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所以只判断statusCode200会漏掉问题。参数说明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。如果每次发送都createDefaultclose连接池就白配了。重试逻辑只针对IOException和超时errcode非0不重试。我一般这样处理针对IOException最多重试3次每次间隔1秒、2秒、4秒做指数退避。不要用固定间隔猛烈重试钉钉对高频重复请求会触发限流反而导致后续消息发不出去。钉钉Webhook的正常响应是{errcode:0,errmsg:ok}非0的errcode常见有310000关键词与消息内容不匹配、300001token不正确、40035JSON参数格式错误。实际排错时日志里一定要把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拼字符串。MapString, Object text new LinkedHashMap(); text.put(content, 告警order-service OOM请及时处理); MapString, 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但我当时只判断了statusCode200没看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_tokenxxx如果是手写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_tokenxxxtimestampxxxsignxxx。注意加签和关键词可以同时启用启用加签后原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.mainClasscom.alarm.TestSend如果返回errcode0再去钉钉群看消息如果errcode非0看日志里的响应体。注意日志只记录到AbstractAppender的话可能只看到发送成功看不到响应体。建议在AlarmService里把响应体整体打印出来像第3章代码那样System.out.println完整JSON。从那以后我每次改完Webhook配置、密钥或消息格式都会强制走一遍真实发送确认群里出现消息才收工。网上把钉钉机器人说得再玄学配置正确时它就是一条稳定的POST请求出错的地方九成都在转义、签名和限流上。希望帮到你。本文还有配套的精品资源点击获取
返回列表