钉钉机器人介绍

官方地址:https://open.dingtalk.com/document/robots/custom-robot-access

企业内部有较多系统支撑着公司的核心业务流程,譬如CRM系统、交易系统、监控报警系统等等。通过钉钉的自定义机器人,可以将这些系统事件同步到钉钉的聊天群。

接入方式

接入钉钉机器人比较简单,分为两步步骤:

  1. 在钉钉群聊中,添加并配置机器人。
  2. 基于钉钉机器人的 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
/**
* 忽略的告警url
*/
@Getter
@Value("${dingTalk.ignoreUrls:}")
private Set<String> ignoreUrls;

/**
* 指定的告警url
*/
@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
/**
* 发送钉钉消息
*
* @param request 机器人消息对象
* @param canApply 是否需要发送钉钉机器人消息
*/
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;

/**
* 钉钉消息类型为 markdown 的内容
*/
@Setter
@Getter
private Markdown markdown;

/**
* 钉钉消息类型为 text 的内容
*/
@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());
// 配置http请求告警信息
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格式化确定消息内容的复杂性。所以本次工作上的封装主要是针对消息内容做了一个优化,然后结合项目需求开发一些特殊规定,例如什么环境需要@什么人、什么接口需要屏蔽、什么异常是必须要发送消息的等等。