前言
实现流程
创建钉钉群机器人后,得到 Webhook 与 Secret。Java 代码 实现 Admin 的 Notifier 接口,当监听到 Admin 服务状态变更后,直接调用 Webhook 发送消息给钉钉群机器人,群成员就可以收到报警消息通知,这个过程与 Github 的 Webhook 实现流程一致。
钉钉官方文档
值得一提的是,本文使用的是钉钉提供的 自定义机器人
接口,而不是 开发企业内部机器人
接口,同时 Webhook 里包含的 access_token
不存在有效期(永久有效),即不需要定时刷新 access_token
。
创建钉钉群机器人
首先登录钉钉的 PC 版,创建钉钉群机器人,得到钉钉群机器人的 Webhook;在群机器人的安全设置页面,若选择加签名,加签一栏下面还可以获取到 SEC 开头的字符串(签名秘钥)。
Admin 集成钉钉群机器人通知
Java 核心代码
钉钉群机器人的配置类
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
| import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration;
@Configuration public class DingTalkConfig {
@Value("${notify.dingtalk.enable:false}") private boolean robotEnable;
@Value("${notify.dingtalk.access-token:}") private String accessToken;
@Value("${notify.dingtalk.sign-secret:}") private String signSecret;
@Value("${notify.dingtalk.enable-signature:true}") private boolean enableSignature;
public boolean isRobotEnable() { return robotEnable; }
public String getAccessToken() { return accessToken; }
public String getSignSecret() { return signSecret; }
public boolean isEnableSignature() { return enableSignature; }
}
|
钉钉群机器人的消息类型枚举类
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
| public enum DingTalkMessageType {
TEXT("text", "文本消息"),
LINK("link", "链接消息"),
MARK_DOWN("markdown", "MarkDown消息"),
FEED_CARD("feedCard", "FeedCard消息"),
ACTION_CARD("actionCard", "ActionCard消息");
private String value;
private String name;
DingTalkMessageType(String value, String name) { this.value = value; this.name = name; }
public String getValue() { return value; }
public String getName() { return name; }
}
|
钉钉群机器人的常量类
1 2 3 4 5 6 7 8 9
| public class DingTalkConstants {
public static final String SERVER_URL = "https://oapi.dingtalk.com";
public static final String API_SEND_MESSAGE = SERVER_URL + "/robot/send?access_token=%s";
public static final String API_SEND_MESSAGE_SIGN = SERVER_URL + "/robot/send?access_token=%s×tamp=%s&sign=%s";
}
|
钉钉群机器人的消息发送类
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
| import cn.hutool.core.codec.Base64; import com.dingtalk.api.DefaultDingTalkClient; import com.dingtalk.api.DingTalkClient; import com.dingtalk.api.request.OapiRobotSendRequest; import com.monitor.notify.config.DingTalkConfig; import com.monitor.notify.constants.DingTalkConstants; import com.monitor.notify.enums.DingTalkMessageType; import com.taobao.api.TaobaoResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component;
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URLEncoder;
@Component @ConditionalOnExpression("${notify.dingtalk.enable:false}") public class DingTalkMessageSender {
private static final Logger logger = LoggerFactory.getLogger(DingTalkMessageSender.class);
private DingTalkConfig dingTalkConfig;
public DingTalkMessageSender(DingTalkConfig dingTalkConfig) { this.dingTalkConfig = dingTalkConfig; }
public boolean sendTextMessage(String msgText) { String logContent = msgText.replace(" ", "").replace("\n", ""); try { logger.info("钉钉群机器人发送Text消息: {}", logContent); DingTalkClient client = new DefaultDingTalkClient(getUrl()); OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text(); text.setContent(msgText); OapiRobotSendRequest request = new OapiRobotSendRequest(); request.setMsgtype(DingTalkMessageType.TEXT.getValue()); request.setText(text); TaobaoResponse response = client.execute(request); return response.isSuccess(); } catch (Exception e) { logger.error("钉钉群机器人发送Text消息失败 : {} : {}", logContent, e.getLocalizedMessage()); } return false; }
public boolean sendMarkdownMessage(String title, String msgText) { String logContent = msgText.replace(" ", "").replace("\n", ""); try { logger.info("钉钉群机器人发送Markdown消息: {}", logContent); DingTalkClient client = new DefaultDingTalkClient(getUrl()); OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown(); markdown.setTitle(title); markdown.setText(msgText); OapiRobotSendRequest request = new OapiRobotSendRequest(); request.setMsgtype(DingTalkMessageType.MARK_DOWN.getValue()); request.setMarkdown(markdown); TaobaoResponse response = client.execute(request); return response.isSuccess(); } catch (Exception e) { logger.error("钉钉群机器人发送Markdown消息失败 : {} : {}", logContent, e.getLocalizedMessage()); } return false; }
private String getUrl() throws Exception { String accessToken = dingTalkConfig.getAccessToken(); if (!dingTalkConfig.isEnableSignature()) { return String.format(DingTalkConstants.API_SEND_MESSAGE, accessToken); } else { Long timestamp = System.currentTimeMillis(); String sign = getSign(timestamp, dingTalkConfig.getSignSecret()); return String.format(DingTalkConstants.API_SEND_MESSAGE_SIGN, accessToken, timestamp, sign); } }
private String getSign(Long timestamp, String secret) throws Exception { String stringToSign = timestamp + "\n" + secret; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256")); byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8")); return URLEncoder.encode(new String(Base64.encode(signData)), "UTF-8"); }
}
|
消息通知模板类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class MessageTemplate {
public static final String MONITOR_TEXT_TEMPLATE = "服务名: %s(%s) \n服务状态: %s(%s) \n服务 IP: %s \n发送时间: %s";
public static final String MONITOR_MARKDOWN_TEMPLATE_DINGTALK = "**服务名称:**\n\n" + "%s(%s)\n\n" + "**服务状态:**\n\n" + "%s(%s)\n\n" + "**服务IP:**\n\n" + "%s\n\n" + "**发送时间:**\n\n" + "%s\n\n";
}
|
实现 Admin 的 Notifier 接口来添加钉钉群机器人的消息通知,若需要监听服务的所有事件变更,还可以改为继承 AbstractEventNotifier
类
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
| import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.notify.AbstractStatusChangeNotifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono;
public abstract class CustomNotifier extends AbstractStatusChangeNotifier {
private Logger logger = LoggerFactory.getLogger(CustomNotifier.class);
protected CustomNotifier(InstanceRepository repository) { super(repository); }
@Override protected Mono<Void> doNotify(InstanceEvent event, Instance instance) { return Mono.fromRunnable(() -> { if (event instanceof InstanceStatusChangedEvent) { logger.info("Instance {} ({}) is {}", instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus());
String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(); switch (status) { case "DOWN": sendMessage(event, instance, "健康检查没通过"); break; case "OFFLINE": sendMessage(event, instance, "服务下线"); break; case "UP": sendMessage(event, instance, "服务上线"); break; case "UNKNOWN": sendMessage(event, instance, "服务出现未知状态"); break; default: break; } } else { logger.info("Instance {} ({}) {}", instance.getRegistration().getName(), event.getInstance(), event.getType()); } }); }
public abstract void sendMessage(InstanceEvent event, Instance instance, String content);
}
|
Admin 的服务状态变更钉钉群机器人通知实现类
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
| import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; import com.monitor.notify.message.DingTalkMessageSender; import com.monitor.notify.template.MessageTemplate; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component;
import java.util.Date;
@Component @ConditionalOnExpression("${notify.dingtalk.enable:false}") public class DingtalkNotifier extends CustomNotifier {
private DingTalkMessageSender messageSender;
protected DingtalkNotifier(InstanceRepository repository, DingTalkMessageSender messageSender) { super(repository); this.messageSender = messageSender; }
@Override public void sendMessage(InstanceEvent event, Instance instance, String content) { String instanceName = instance.getRegistration().getName(); String instanceId = event.getInstance().toString(); String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(); String serviceUrl = instance.getRegistration().getServiceUrl(); String dateTime = DateUtil.format(new Date(), DatePattern.NORM_DATETIME_MS_PATTERN); String message = String.format(MessageTemplate.MONITOR_MARKDOWN_TEMPLATE_DINGTALK, instanceName, instanceId, status, content, serviceUrl, dateTime); this.messageSender.sendMarkdownMessage("监控消息", message); }
}
|
YML 配置内容
1 2 3 4 5 6
| notify: dingtalk: enable: true access-token: xxxxxxxxxxx sign-secret: SECxxxxxxxxxxx enable-signature: true
|
钉钉的 Java SDK
由于钉钉官方没有将钉钉的 SDK 发布到 Maven 仓库,因此需要在钉钉官网手动下载最新版的 SDK,然后发布到 Maven 私有仓库,或者安装在本地的 Maven 仓库,最后在项目的 pom.xml
配置文件里添加以下依赖。
1 2 3 4 5
| <dependency> <groupId>com.dingtalk</groupId> <artifactId>dingtalk-api-sdk</artifactId> <version>1.0.0</version> </dependency>
|
安装钉钉 SDK 到本地 Maven 仓库的命令如下:
1
| $ mvn install:install-file -Dfile=taobao-sdk-java-auto_1455552377940-20200322.jar -DgroupId=com.dingtalk -DartifactId=dingtalk-api-sdk -Dversion=1.0.0 -Dpackaging=jar
|
值得一提的是,不同版本的 钉钉 SDK,其 Maven 坐标中的 groupId
、artifactId
、version
可能会发生变化,此时只需要将上面对应的参数值替换掉即可。
生产环境扩展建议
上述给出的是 Spring Boot Admin 集成钉钉群机器人消息通知的 Demo 代码,生产环境下还需要考虑到如下的实际问题:
报警消息重复发送
:若 Admin 应用以集群的方式部署,当 A 应用 DOWN 掉后,那么钉钉群成员将会收到多条 A 应用服务状态变更的报警消息报警消息的持久化
:若大量报警消息积压在 Admin 应用里,但还没来得及发送,此时如果 Admin 应用挂掉,那么报警消息将会丢失,建议使用 消息中间件
的持久化特性来解决报警消息的发送频率
:每个钉钉群机器人每分钟最多发送 20 条,如果超过 20 条,会限流 10 分钟;这里建议利用 任务调度线程池
来实现报警消息的调度发送,同时考虑将多条报警消息合并后再发送,以此来解决报警消息发送频率受限制的问题
参考博客