钉钉机器人介绍
官方地址:https://open.dingtalk.com/document/robots/custom-robot-access
企业内部有较多系统支撑着公司的核心业务流程,譬如CRM系统、交易系统、监控报警系统等等。通过钉钉的自定义机器人,可以将这些系统事件同步到钉钉的聊天群。
接入方式
接入钉钉机器人比较简单,分为两步步骤:
- 在钉钉群聊中,添加并配置机器人。
- 基于钉钉机器人的 Webhook 地址发起 HTTP POST 请求,即可实现给该钉钉群发送消息。
发送的消息内容是一个 JSON 对象,按照钉钉给定的消息类型和数据格式进行发送。
当前自定义机器人支持文本 (text)、链接 (link)、markdown(markdown)、ActionCard、FeedCard消息类型。
钉钉提供了SDK接入方式,通过如下依赖实现。
1 2 3 4 5
| <dependency> <groupId>com.aliyun</groupId> <artifactId>alibaba-dingtalk-service-sdk</artifactId> <version>2.0.0</version> </dependency>
|
使用方法
钉钉机器人发送消息可能是一个非常常用的操作,大多数情况下,发送的消息内容结构是固定的,所以在项目中对钉钉机器人发送消息做了一个封装。
首先,看一下如果不封装代码,直接使用 Java 代码发送消息的示例代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class OriginalDemo {
public static void main(String[] args) { Map<String, String> msg = new HashMap<>(); msg.put("msgtype", "text"); msg.put("text", JSONUtil.toJsonStr(new Text("我就是我, 是不一样的烟火")));
HttpResponse httpResponse = HttpUtil.createPost("https://oapi.dingtalk.com/robot/send?access_token=566cc69da782ec******") .body(JSONUtil.toJsonStr(msg)) .execute();
System.out.println(httpResponse); }
@Data @AllArgsConstructor static class Text { private String content; } }
|
这个示例中,发送的钉钉消息是一串文本,需要先定义消息类型,再通过HTTP工具发送。
这其中除了文本消息内容是可变的,其实都是固定的,所以如果通过封装后的工具发送,代码如下:
1
| DingTalkHelper.sendMessage("我就是我, 是不一样的烟火");
|
完整的项目示例代码:https://github.com/wangfarui/work-report/tree/main/dingtalk-rebot
实现封装
工作项目上,因为使用了 Apollo 作为动态属性配置,所以钉钉机器人封装中也用到了它。
参数配置
首先,确定钉钉机器人需要的属性,通过钉钉文档的介绍,机器人发送消息至少需要客户端地址,如果配置了授权和加密,则还需要 token 和 secret ,参数如下:
1 2 3 4 5 6 7 8
| @Value("${dingTalk.client-url:https://oapi.dingtalk.com/robot/send}") private String clientUrl;
@Value("${dingTalk.access-token:}") private String accessToken;
@Value("${dingTalk.secret:}") private String secret;
|
然后,钉钉机器人发送的消息有时是不必须的,例如用于发送系统告警信息时,在内网进行开发联调时,不希望打印一堆异常信息到钉钉群里,就增加了个参数用于控制钉钉机器人的开启和关闭( enabled )。
此外,结合钉钉机器人支持@功能,实现了动态@指定人或所有人功能,通过手机号绑定。参数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@Getter @Value("${dingTalk.enabled:false}") private boolean enabled;
@Getter @Value("${dingTalk.at.atMobiles:}") private List<String> atMobiles;
@Getter @Value("${dingTalk.at.atAll:false}") private boolean atAll;
|
最后,在钉钉机器人用于发送系统异常消息时,有时希望忽略某些接口的告警,有时希望只开启某些接口的告警,所以配置了忽略告警url和指定告警url两个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@Getter @Value("${dingTalk.ignoreUrls:}") private Set<String> ignoreUrls;
@Getter @Value("${dingTalk.warnUrls:}") private Set<String> warnUrls;
|
发送消息
为了减轻项目的依赖项,因此没有接入SDK方式,发送消息仍然采用的是HTTP方式。
为了保证钉钉机器人发送消息功能不影响业务功能的正常进行,因此将发送消息功能独立到新的线程,并对发送结果的异常消息做日志记录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
public void send(final DingTalkSendRequest request, boolean canApply) { if (canApply) { if (properties.isAtAll()) { request.setAtAll(properties.isAtAll()); } else if (CollectionUtils.isNotEmpty(properties.getAtMobiles())) { request.setAtMobiles(properties.getAtMobiles()); }
boolean completed = request.completeRequestParam(); if (!completed) { log.warn("[DingTalkClient][send]钉钉消息请求对象数据异常, request:{}", JSON.toJSONString(request)); } final String requestUrl = properties.getRequestUrl(); EXECUTOR_SERVICE.execute(() -> { HttpResponse httpResponse = HttpUtil.createPost(requestUrl) .body(JSON.toJSONString(request)) .charset(StandardCharsets.UTF_8) .execute(); if (httpResponse == null) { log.warn("[DingTalkClient][send]发送钉钉消息异常, request:{}", JSON.toJSONString(request)); } else if (!httpResponse.isOk()) { log.warn("[DingTalkClient][send]发送钉钉消息失败, request:{}, response:{}", JSON.toJSONString(request), JSON.toJSONString(httpResponse)); } }); } }
|
通过发送消息方法的入参可以看出,此功能主要依赖于 DingTalkSendRequest
对象,此对象的属性是严格按照钉钉文档的参数名进行设定的,避免JSON序列化时的二次包装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| public class DingTalkSendRequest {
@Getter private String msgtype;
@Setter private DingTalkMsgType dingTalkMsgType;
@Setter @Getter private AT at;
@Setter @Getter private Markdown markdown;
@Setter @Getter private Text text; @Setter @Getter public static class AT { private List<String> atMobiles;
private List<String> atUserIds;
private boolean isAtAll; }
public static class Markdown { @Setter @Getter private String title;
@Setter @Getter private String text;
private Map<String, String> content;
public void addContent(String key, String value) { if (this.content == null) { this.content = new LinkedHashMap<>(); } this.content.put(key, value); }
public void formatContentToText() { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : this.content.entrySet()) { sb.append(entry.getKey()).append(":").append(entry.getValue()).append("\n\n"); } this.text = sb.toString(); } }
@Setter @Getter public static class Text { private String content; } }
|
系统异常消息
通过 spring-web 的 @RestControllerAdvice
+ @ExceptionHandler
注解拦截指定异常,对需要的异常发送消息到钉钉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| private void sendDingTalkMessage(Throwable e, HttpServletRequest httpServletRequest) { DingTalkSendRequest request = new DingTalkSendRequest(); request.setMarkdownTitle(e instanceof BizException ? "业务告警" : "系统告警");
request.addMarkdownContent("【告警环境】", this.envStr); request.addMarkdownContent("【traceId】", MDC.get("traceId")); request.addMarkdownContent("【租户id】", UserUtils.getTenantId().toString()); request.addMarkdownContent("【告警时间】", DateUtil.now()); if (httpServletRequest != null) { request.addMarkdownContent("【异常接口】", httpServletRequest.getRequestURI()); } request.addMarkdownContent("【异常堆栈】", ExceptionUtils.exceptionStackTraceText(e, 1000));
DingTalkHelper.send(request); }
|
二次封装
如果直接使用封装的 DingTalkClient
对象,调用 send
方法发送消息,对于开发人员来说还是比较麻烦的,还需要自定义消息对象等。
因此,根据业务项目场景需求,二次封装了一个抽象类,开发人员可以直接静态调用发送消息功能,实现普通消息和异常消息的发送。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public static void sendMessage(String message) { DingTalkSendRequest request = new DingTalkSendRequest(); request.setTextContent(message); getDingTalkClient().send(request); }
public static void sendException(String message, Throwable e) { DingTalkSendRequest request = new DingTalkSendRequest(); request.setMarkdownTitle("自定义异常"); request.addMarkdownContent("异常内容", message); request.addMarkdownContent("异常信息", ExceptionUtils.exceptionStackTraceText(e)); getDingTalkClient().send(request); }
|
总结
钉钉机器人发送消息这个功能,钉钉官方支持的是比较简单的,因此主要是看消息内容的JSON数据,通过JSON格式化确定消息内容的复杂性。所以本次工作上的封装主要是针对消息内容做了一个优化,然后结合项目需求开发一些特殊规定,例如什么环境需要@什么人、什么接口需要屏蔽、什么异常是必须要发送消息的等等。