Lark(飞书)群机器人提醒 OpenFeign+异步+动态配置

前言

定时任务执行失败时期望发送Lark(飞书)群消息提醒

最下方免费获取源码

前置条件

1、创建群聊

2、自定义群机器人

3、获取 Webhook 地址和 签名校验 secret

代码

1、依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>4.1.4</version>
</dependency>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

其他基础依赖...

2、配置文件(建议配置到nacos中)

enabled:配置是否生效

webapi:机器人webhook地址

secret:机器人密钥

yaml 复制代码
server:
  port: 8099

lark:
  robot:
    enabled: false
    webapi: your Webhook
    secret: your Secret

3、配置属性

java 复制代码
@ConfigurationProperties(prefix = "lark.robot")
@Data
public class LarkRobotProperties {

    private Boolean enabled = false;

    private String webapi;

    private String secret;
}

4、FeignClient接口调用

java 复制代码
@FeignClient(name = "lesson-client", url = "${lark.robot.webapi}")
public interface LarkRobotClient {

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    String sendMessage(String request);

}

5、配置拦截器

创建此拦截器的目的:

​ @FeignClient(name = "lesson-client", url = "${lark.robot.webapi}") 如果将webapi放入nacos动态配置时,修改配置无法生效,目前采用拦截器修改,如果有大佬知道如何解决希望评论区讨论。

java 复制代码
@Configuration(proxyBeanMethods = false)
@EnableFeignClients(clients = {LarkRobotClient.class})
@EnableConfigurationProperties(LarkRobotProperties.class)
public class RpcConfiguration {

    @Resource
    private LarkRobotProperties larkRobotProperties;
    
    /**
     * 此拦截器会全局生效,一定要加控制好范围,否则会影响到其他模块的调用
     */
    @Bean
    public RequestInterceptor urlInterceptor() {

        // 动态修改 lark机器人的webapi
        return template -> {
            if ("lark-robot-client".equals(template.feignTarget().name())) {
                String newUrl = larkRobotProperties.getWebapi();
                template.target(newUrl);
            }
        };
    }
}

6、配置线程池(可选)

java 复制代码
@Configuration
@EnableAsync
public class ThreadPoolConfig {

    @Bean("asyncCommonExecutor")
    public Executor handleDeviceRecordExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数:线程池创建时候初始化的线程数
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
        // 最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 5);
        // 队列最大值
        executor.setQueueCapacity(500);
        // 允许线程的空闲时间60秒:当超过了核心线程之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(60);
        // 等待所有任务结束后再关闭线程池
         executor.setWaitForTasksToCompleteOnShutdown(true);
        // 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
        executor.setThreadNamePrefix("async-common-");
        // 设置拒绝策略,超限的不在新线程中执行任务,而是由调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
        executor.setAwaitTerminationSeconds(600);
        executor.initialize();
        return executor;
    }

}

7、LarkRobotUtils

genSign:根据密钥获取签名

getPostContent:封装富文本实体类

java 复制代码
@Component
public class LarkRobotUtils {


    public static String genSign(String secret, int timestamp) throws NoSuchAlgorithmException, InvalidKeyException {
        //把timestamp+"\n"+密钥当做签名字符串
        String stringToSign = timestamp + "\n" + secret;

        //使用HmacSHA256算法计算签名
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(new byte[]{});
        return new String(Base64.encodeBase64(signData));
    }


    /**
     * 富文本 post类型消息
     */
    public static PostContent getPostContent(AlarmContentRequest request) {

        // 富文本内容
        PostContent postContent = new PostContent();
        // 创建内容并封装
        List<List<PostElement>> postElements = new ArrayList<>();
        // 处理内容
        handleContentList(postElements, request.getContentList());
        // 拼装结果内容
        setPostContent(request, postElements, postContent);

        return postContent;
    }

    private static void setPostContent(AlarmContentRequest request, List<List<PostElement>> postElements, PostContent postContent) {
        Content cnContent = new Content();
        cnContent.setTitle(request.getTitle());
        cnContent.setPostElements(postElements);

        Post post = new Post();
        post.setCnContent(cnContent);

        postContent.setPost(post);
    }

    @SuppressWarnings("all")
    private static void handleContentList(List<List<PostElement>> contentList, List<ContentRequest> list) {
        // @所有人 + 标题
        contentList.add(List.of(createPostElement(PostTagEnum.AT, "all", null, null)));
        if (CollUtil.isEmpty(list)) return;
        // 遍历内容
        for (ContentRequest contentRequest : list) {
            // 创建元素列表
            List<PostElement> elements = new ArrayList<>();
            // 文本内容
            String text = contentRequest.getText();
            if (StringUtils.isNotBlank(text)) {
                elements.add(createPostElement(PostTagEnum.TEXT, null, text, null));
            }
            // 链接
            String href = contentRequest.getHref();
            if (StringUtils.isNotBlank(href)) {
                elements.add(createPostElement(PostTagEnum.LINK, null, contentRequest.getHrefDesc(), href));
            }
            contentList.add(elements);
        }
    }

    /**
     * 创建 PostElement
     */
    @SuppressWarnings("all")
    private static PostElement createPostElement(PostTagEnum tag, String userId, String text, String href) {
        PostElement element = new PostElement();
        element.setTag(tag.getTag());
        element.setUserId(userId);
        element.setText(text);
        element.setHref(href);
        return element;
    }

}

8、service层 发送消息服务类

sendMessage 同步发送Lark消息

sendMessageAsync 异步发送Lark消息

java 复制代码
@Slf4j
@Service
public class SendMessageServiceImpl implements SendMessageService {

    @Resource
    private LarkRobotClient larkRobotClient;

    @Resource
    private LarkRobotProperties larkRobotProperties;

    @Override
    public MessageResponse sendMessage(AlarmContentRequest request) {
        if (!larkRobotProperties.getEnabled()) {
            MessageResponse messageResponse = new MessageResponse();
            messageResponse.setCode(-1).setMessage("Lark Robot config is not enabled");
            log.info("排课状态字段更新失败,发送lark结果: {}", JSON.toJSON(messageResponse));
            return messageResponse;
        }

        // 生成时间戳和签名
        int timestamp = (int) (System.currentTimeMillis() / 1000);
        String sign;
        try {
            sign = LarkRobotUtils.genSign(larkRobotProperties.getSecret(), timestamp);
        } catch (Exception e) {
            log.error("加密失败: {}", e.getMessage(), e);
            throw new RuntimeException("加密失败");
        }
        // 构造消息内容
        PostContent postContent = LarkRobotUtils.getPostContent(request);
        // 构造消息请求对象
        String requestJson = this.getRequestJson(timestamp, sign, postContent);
        // 使用 Feign 调用接口
        String response;
        try {
            response = larkRobotClient.sendMessage(requestJson);
        } catch (Exception e) {
            log.error("发送lark异常: {}", e.getMessage(), e);
            throw e;
        }
        MessageResponse messageResponse = JSON.parseObject(response, MessageResponse.class);
        log.info("发送lark结果: {}", messageResponse);
        return messageResponse;
    }

    @Override
    @Async("asyncCommonExecutor")
    public void sendMessageAsync(AlarmContentRequest request) {
        sendMessage(request);
    }


    private String getRequestJson(int timestamp, String sign, PostContent postContent) {
        LarkMessageRequest larkMessageRequest = new LarkMessageRequest();
        larkMessageRequest.setTimestamp(timestamp);
        larkMessageRequest.setSign(sign);
        larkMessageRequest.setMsgType(MessageTypeEnum.POST.getType());
        larkMessageRequest.setContent(postContent);
        return JSON.toJSONString(larkMessageRequest);
    }
}

9、Controller层

java 复制代码
@RestController
@RequestMapping("/lark")
public class SendMessageController {

    @Resource
    private SendMessageService sendMessageService;


    @GetMapping("/send")
    public MessageResponse send() {
        AlarmContentRequest request = setAlarmContentRequest(1, 1, 1, new Exception("test"));
        return sendMessageService.sendMessage(request);
    }

    @GetMapping("/sendAsync")
    public Boolean sendAsync() {
        AlarmContentRequest request = setAlarmContentRequest(2, 2, 2, new Exception("test"));
        sendMessageService.sendMessageAsync(request);
        return true;
    }


    /**
     * 拼装发送lark请求对象
     */
    private AlarmContentRequest setAlarmContentRequest(Integer studentId, int size, int count, Exception e) {
        List<ContentRequest> contentList = Arrays.asList(
                new ContentRequest().setText("** 总共【" + size + "】条数据"),
                new ContentRequest().setText("** 更新了【" + count + "】条数据"),
                new ContentRequest().setText("** 错误studentId:【" + studentId + "】"),
                new ContentRequest().setText("** 错误信息:【" + e.getMessage() + "】")
        );
        AlarmContentRequest request = new AlarmContentRequest();
        request.setTitle("更新失败").setContentList(contentList);
        return request;
    }
}

10、调用接口

localhost:8099/lark/sendAsync

localhost:8099/lark/send

效果如下:

完结。如有疑问麻烦留言或者私信。

subscribe vvv: H先生出品

相关推荐
舒一笑3 分钟前
一文简单记录打通K8s+Kibana流程如何启动(Windows下的Docker版本)
后端·elasticsearch·kibana
亦黑迷失4 分钟前
轻量级 Express 服务器:用 Pug 模板引擎实现动态参数传递
前端·javascript·后端
慧一居士13 分钟前
Kafka批量消费部分处理成功时的手动提交方案
分布式·后端·kafka
命中的缘分44 分钟前
SpringCloud原理和机制
后端·spring·spring cloud
ErizJ44 分钟前
Golang|分布式索引架构
开发语言·分布式·后端·架构·golang
.生产的驴1 小时前
SpringBoot 接口国际化i18n 多语言返回 中英文切换 全球化 语言切换
java·开发语言·spring boot·后端·前端框架
Howard_Stark1 小时前
Spring的BeanFactory和FactoryBean的区别
java·后端·spring
-曾牛1 小时前
Spring Boot中@RequestParam、@RequestBody、@PathVariable的区别与使用
java·spring boot·后端·intellij-idea·注解·spring boot 注解·混淆用法
极客智谷1 小时前
Spring AI应用系列——基于Alibaba DashScope的聊天记忆功能实现
人工智能·后端
极客智谷1 小时前
Spring AI应用系列——基于Alibaba DashScope实现功能调用的聊天应用
人工智能·后端