在企业项目里,消息通知几乎是刚需。
比如订单支付成功要通知运营,接口报错要通知开发,定时任务执行失败要通知值班人员,服务器告警也要第一时间推到群里。很多团队第一反应是发短信、发邮件,但真到了落地阶段,飞书群消息反而更直接,成本更低,接入也更快。
如果你的项目是 Spring Boot,那么最适合快速落地的方案,通常就是接入飞书自定义机器人 。它本质上是一个群聊级别的 Webhook,你的服务端只需要向这个地址发送 HTTP POST 请求,就可以把文本、富文本、图片甚至卡片消息推送到飞书群中。飞书官方文档也明确说明了,自定义机器人适合群聊消息推送,通过 webhook 地址即可发送多种类型消息;如果你需要更复杂的系统集成能力,则通常要考虑"应用机器人"方案。(飞书开放平台)
这篇文章我不讲虚的,直接带你从 0 到 1 完成 Spring Boot 集成飞书推送,包括:
-
飞书机器人怎么创建
-
Spring Boot 项目怎么配置
-
文本消息怎么发
-
开启"签名校验"之后怎么处理
-
怎么封装成企业项目里可复用的工具类
-
常见报错怎么排查
-
最后给你一个可以直接上线的实战代码结构
一、为什么企业项目里推荐先接飞书自定义机器人
对于大多数"群通知"场景,比如异常告警、业务提醒、订单通知、审批提醒,自定义机器人已经足够用了。它的优点很明显:
-
接入简单,不需要你先做一整套飞书应用体系
-
只要拿到 webhook 地址,就能直接发消息
-
支持文本、富文本、图片、群名片、交互式消息卡片等消息类型
-
很适合做项目里的"告警通知组件"或者"消息推送基础设施"
飞书官方说明中提到,自定义机器人可以直接通过 webhook 地址推送消息,支持文本、富文本、图片等多种类型;而"应用机器人"更偏向开放接口调用、复杂系统集成。(飞书开放平台)
所以如果你这篇文章是写给 CSDN 读者看,我建议你把主线聚焦在:
Spring Boot + 飞书自定义机器人 + Webhook 推送
这样最实用,也最容易让读者复现。
二、接入前先搞清楚两种方案
很多人一上来就会混淆"自定义机器人"和"应用机器人",这里先给你讲透。
1. 自定义机器人
它是直接在飞书群里添加的,拿到一个 webhook 地址后,你的服务端就能往群里发消息。适合快速通知、监控报警、业务提醒。(飞书开放平台)
2. 应用机器人
它是在飞书开放平台创建应用,然后给应用开通机器人能力,可以调用更多 OpenAPI,适合更完整的业务集成。飞书官方的机器人概述也把这两者做了区分:应用机器人适合系统集成,自定义机器人更适合群聊消息推送。(飞书开放平台)
本文选择的是最容易落地的方案:自定义机器人。
三、先在飞书里创建自定义机器人
这一步不写清楚,很多人连 webhook 都拿不到。
操作步骤
-
打开飞书客户端
-
进入你要接收通知的群聊
-
打开群设置
-
找到"群机器人"
-
选择"添加机器人"
-
选择"自定义机器人"
-
填写机器人名称和描述
-
创建成功后复制 webhook 地址
飞书官方 FAQ 说明,自定义机器人需要在飞书客户端的指定群聊内添加;不是去开发者后台建。(飞书开放平台)
创建完成后,你通常还能看到几项安全设置,比如:
-
自定义关键词
-
IP 白名单
-
签名校验
飞书官方文档明确提到,自定义机器人支持 IP 白名单和签名校验等安全设置,用来保证调用安全。(飞书开放平台)
四、Spring Boot 项目准备
下面我们开始写代码。
1. Maven 依赖
这里我建议用 spring-boot-starter-web,再配合 Jackson 做 JSON 序列化即可。为了方便写 HTTP 请求,也可以直接用 RestTemplate。如果你项目里已经在用 hutool 或 okhttp,也可以替换。
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok,可选 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 单元测试,可选 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
五、配置文件怎么写
一般我们会把 webhook 和签名密钥放到配置文件里。
application.yml
feishu:
webhook: https://open.feishu.cn/open-apis/bot/v2/hook/你的webhook
enabled: true
secret: 你的签名密钥
说明一下:
-
webhook:飞书机器人地址 -
enabled:是否启用,方便区分测试环境和生产环境 -
secret:如果你开启了签名校验,这里要配置对应密钥;如果没开启,可以为空
六、先定义配置类
package com.example.demo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "feishu")
public class FeishuProperties {
/**
* 机器人 webhook 地址
*/
private String webhook;
/**
* 是否启用
*/
private Boolean enabled = true;
/**
* 签名密钥,可为空
*/
private String secret;
}
七、先跑通最基础的文本消息推送
飞书自定义机器人发送消息,本质上就是向 webhook 发一个 POST 请求。常见的消息体里会带 msg_type,文本消息一般对应 text 类型。飞书官方文档中也给出了自定义机器人支持的消息类型,包含文本、富文本、图片等;在相关资料里也能看到 msg_type 常用取值包括 text、post、image、interactive 等。(飞书开放平台)
1. 定义请求对象
package com.example.demo.feishu.dto;
import lombok.Data;
@Data
public class FeishuTextRequest {
private String msg_type;
private TextContent content;
@Data
public static class TextContent {
private String text;
}
}
2. 编写发送服务
package com.example.demo.feishu.service;
import com.example.demo.config.FeishuProperties;
import com.example.demo.feishu.dto.FeishuTextRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
@Service
@RequiredArgsConstructor
public class FeishuBotService {
private final FeishuProperties feishuProperties;
private final ObjectMapper objectMapper = new ObjectMapper();
private final RestTemplate restTemplate = new RestTemplate();
public String sendText(String text) {
if (Boolean.FALSE.equals(feishuProperties.getEnabled())) {
return "飞书推送未启用";
}
FeishuTextRequest request = new FeishuTextRequest();
request.setMsg_type("text");
FeishuTextRequest.TextContent content = new FeishuTextRequest.TextContent();
content.setText(text);
request.setContent(content);
return doPost(request);
}
private String doPost(Object requestBody) {
try {
String json = objectMapper.writeValueAsString(requestBody);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(json, headers);
ResponseEntity<String> response = restTemplate.postForEntity(
feishuProperties.getWebhook(),
entity,
String.class
);
return response.getBody();
} catch (JsonProcessingException e) {
throw new RuntimeException("飞书消息序列化失败", e);
} catch (Exception e) {
throw new RuntimeException("飞书消息发送失败", e);
}
}
}
3. 写一个测试接口
package com.example.demo.controller;
import com.example.demo.feishu.service.FeishuBotService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/feishu")
@RequiredArgsConstructor
public class FeishuTestController {
private final FeishuBotService feishuBotService;
@GetMapping("/send")
public String send(@RequestParam String msg) {
return feishuBotService.sendText(msg);
}
}
启动项目后访问:
http://localhost:8080/feishu/send?msg=SpringBoot接入飞书成功
如果群里成功收到消息,说明最基础的链路已经打通了。
八、企业项目里一定要加签名校验
上面只是最简版。
真正线上使用时,我强烈建议你把飞书机器人的签名校验 打开。因为 webhook 一旦泄露,别人就可能往你的群里乱发消息。飞书官方文档明确提到,自定义机器人支持签名校验;开启后,请求中需要带上 timestamp 和 sign 字段。其签名规则是:将 timestamp + "\n" + 密钥 作为签名字符串,使用 HmacSHA256 算法计算,再做 Base64 编码。(飞书开放平台)
这一步非常关键,也是很多教程写得不够清楚的地方。
九、签名算法怎么写
1. 定义带签名的通用请求对象
package com.example.demo.feishu.dto;
import lombok.Data;
@Data
public class FeishuSignedRequest<T> {
/**
* 时间戳
*/
private String timestamp;
/**
* 签名
*/
private String sign;
/**
* 消息类型
*/
private String msg_type;
/**
* 消息内容
*/
private T content;
}
2. 编写签名工具类
package com.example.demo.feishu.util;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class FeishuSignUtil {
private static final String HMAC_SHA256 = "HmacSHA256";
public static String genSign(String timestamp, String secret) {
try {
String stringToSign = timestamp + "\n" + secret;
Mac mac = Mac.getInstance(HMAC_SHA256);
SecretKeySpec spec = new SecretKeySpec(
stringToSign.getBytes(StandardCharsets.UTF_8),
HMAC_SHA256
);
mac.init(spec);
byte[] signData = mac.doFinal(new byte[]{});
return Base64.getEncoder().encodeToString(signData);
} catch (Exception e) {
throw new RuntimeException("生成飞书签名失败", e);
}
}
}
3. 改造发送服务
package com.example.demo.feishu.service;
import com.example.demo.config.FeishuProperties;
import com.example.demo.feishu.dto.FeishuSignedRequest;
import com.example.demo.feishu.util.FeishuSignUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class FeishuBotService {
private final FeishuProperties feishuProperties;
private final ObjectMapper objectMapper = new ObjectMapper();
private final RestTemplate restTemplate = new RestTemplate();
public String sendText(String text) {
if (Boolean.FALSE.equals(feishuProperties.getEnabled())) {
return "飞书推送未启用";
}
Map<String, String> content = new HashMap<>();
content.put("text", text);
return send("text", content);
}
public String send(String msgType, Object content) {
try {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
FeishuSignedRequest<Object> request = new FeishuSignedRequest<>();
request.setMsg_type(msgType);
request.setContent(content);
if (StringUtils.hasText(feishuProperties.getSecret())) {
request.setTimestamp(timestamp);
request.setSign(FeishuSignUtil.genSign(timestamp, feishuProperties.getSecret()));
}
String json = objectMapper.writeValueAsString(request);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(json, headers);
ResponseEntity<String> response = restTemplate.postForEntity(
feishuProperties.getWebhook(),
entity,
String.class
);
return response.getBody();
} catch (JsonProcessingException e) {
throw new RuntimeException("飞书消息序列化失败", e);
} catch (Exception e) {
throw new RuntimeException("飞书消息发送失败", e);
}
}
}
十、发送富文本消息
很多读者写完文本消息就结束了,但真实项目里,纯文本往往不够用。比如你想发:
-
项目名
-
环境名
-
错误时间
-
错误堆栈
-
点击链接查看详情
这时候富文本更适合。
飞书官方文档中说明,自定义机器人支持富文本消息;相关说明中 msg_type 可使用 post。(飞书开放平台)
示例代码
public String sendPost() {
Map<String, Object> zhCn = new HashMap<>();
zhCn.put("title", "系统告警通知");
Object[][] content = new Object[][]{
{
Map.of("tag", "text", "text", "服务名:订单服务")
},
{
Map.of("tag", "text", "text", "级别:ERROR")
},
{
Map.of("tag", "text", "text", "时间:2026-03-26 10:30:00")
},
{
Map.of("tag", "a", "text", "点击查看详情", "href", "https://your-domain.com/log/detail/1001")
}
};
zhCn.put("content", content);
Map<String, Object> post = new HashMap<>();
post.put("zh_cn", zhCn);
return send("post", post);
}
这个结构本质上就是告诉飞书:
我发送的是 post 富文本消息,语言版本是 zh_cn,标题是"系统告警通知",正文里每一行放哪些组件。
十一、发送卡片消息
如果你只是做普通通知,文本和富文本就够了。但如果你想把消息做得更像"运维告警面板"或者"审批通知卡片",那就可以用消息卡片。
飞书官方文档说明,自定义机器人也支持发送卡片消息;同时飞书卡片是一种结构化内容载体,适合做信息展示和轻量交互。(飞书开放平台)
简单卡片示例
public String sendCard() {
Map<String, Object> header = Map.of(
"title", Map.of("tag", "plain_text", "content", "订单支付通知"),
"template", "blue"
);
Map<String, Object> element1 = Map.of(
"tag", "div",
"text", Map.of(
"tag", "lark_md",
"content", "**订单号:** 202603260001\n**支付状态:** 成功\n**支付金额:** ¥99.00"
)
);
Map<String, Object> element2 = Map.of(
"tag", "action",
"actions", new Object[]{
Map.of(
"tag", "button",
"text", Map.of("tag", "plain_text", "content", "查看订单"),
"type", "primary",
"url", "https://your-domain.com/order/detail/202603260001"
)
}
);
Map<String, Object> card = new HashMap<>();
card.put("header", header);
card.put("elements", new Object[]{element1, element2});
return send("interactive", card);
}
这个效果会比普通文本好很多,特别适合:
-
订单通知
-
发布提醒
-
服务器告警
-
审批消息
-
定时任务执行结果
十二、把它封装成项目里的公共能力
如果你是做企业项目,千万别把飞书推送代码散落在各个业务类里。正确做法是做一层统一封装。
我建议你这样分层:
com.example.demo
├── config
│ └── FeishuProperties.java
├── feishu
│ ├── dto
│ │ ├── FeishuTextRequest.java
│ │ └── FeishuSignedRequest.java
│ ├── service
│ │ └── FeishuBotService.java
│ └── util
│ └── FeishuSignUtil.java
└── controller
└── FeishuTestController.java
如果你的项目已经比较大了,可以进一步抽成:
-
message-api:定义消息接口 -
message-feishu:飞书实现 -
message-wechat:企业微信或微信公众号实现 -
message-email:邮件实现
这样后续替换通知渠道时,不需要改业务代码,只要改消息实现。
十三、推荐你在业务里怎么用
飞书推送最常见的使用方式,不是给前台用户发消息,而是给内部团队通知。
1. 异常告警
try {
// 业务代码
} catch (Exception e) {
feishuBotService.sendText("【系统异常】订单服务发生异常:" + e.getMessage());
throw e;
}
2. 支付成功通知
feishuBotService.sendText(
"【支付通知】用户 10001 支付成功,订单号:202603260001,金额:99.00 元"
);
3. 定时任务结果通知
feishuBotService.sendText(
"【定时任务】会员过期扫描任务执行完成,处理 125 条记录,耗时 3.2 秒"
);
4. 运维告警通知
feishuBotService.sendText(
"【告警】生产环境 Redis 连接数过高,请立即排查"
);
十四、频控问题一定要知道
这个点非常重要,很多人线上一出问题,日志炸了,然后代码疯狂往飞书推送,最后直接触发限流。
飞书官方文档里给出了自定义机器人的频控规则:单租户单机器人 100 次/分钟,5 次/秒 。超限后消息会发送失败。(飞书开放平台)
所以线上建议你至少做两层保护:
1. 本地限流
例如同一种错误 1 分钟内只推一次。
2. 去重推送
同一异常堆栈不要连续推送 100 条。
3. 异步发送
不要在主业务线程里同步卡住,尤其是高并发接口。
4. 告警聚合
把 1 分钟内的同类错误汇总后一次发送。
这一步做了,你的飞书告警体系才算真正能用。
十五、常见报错排查
下面我把最常见的问题直接给你总结出来。
1. webhook 地址错误
表现:请求发出去了,但飞书群没收到。
排查:检查 webhook 是否复制完整,是否多了空格。
2. 没开机器人
表现:一直返回失败。
排查:确认机器人已经成功添加到目标群。
3. 签名错误
表现:开启签名校验后发送失败。
排查重点:
-
timestamp是否传了 -
sign是否传了 -
签名算法是否正确
-
密钥是否和飞书后台一致
-
时间戳是否使用秒级,而不是毫秒级
飞书官方文档明确要求,开启签名验证后,请求中需要附带 timestamp 和 sign;签名算法基于 timestamp + "\n" + 密钥,再使用 HmacSHA256 和 Base64 处理。(飞书开放平台)
4. 触发频控
表现:前几条正常,后面突然发送失败。
排查:检查是否短时间内推送过多消息。官方对自定义机器人有明确频控要求。(飞书开放平台)
5. JSON 结构不正确
表现:HTTP 200 但飞书不展示,或者接口直接报参数错误。
排查:检查 msg_type 和 content 结构是否匹配。文本、富文本、卡片对应的 JSON 结构不一样。飞书官方文档对不同消息类型的内容结构有专门说明。(飞书开放平台)
十六、生产环境最佳实践
如果你想把这篇文章写得更"像大厂实战",这一段一定要保留。
1. webhook 不要硬编码
统一放到配置中心,或者至少放到环境变量里。
2. 不同环境分不同机器人
开发环境、测试环境、生产环境分别使用不同群,不然测试消息会污染生产群。
3. 做统一消息网关
不要在订单模块、支付模块、会员模块各写一套飞书代码。统一做成 MessageService。
4. 失败重试要克制
网络抖动可以重试,但不要无限重试,更不要在高频异常场景中疯狂补发,否则会更容易命中频控。
5. 敏感信息脱敏
错误通知里不要直接发用户身份证、手机号、token、密钥等敏感信息。
6. 告警要分级
-
INFO:普通业务通知
-
WARN:业务异常提醒
-
ERROR:系统错误
-
FATAL:核心服务不可用
建议不同级别走不同群,或者不同消息模板。
十七、完整工具类版本
如果你想让读者复制就能用,那最好给一个整合版。下面这个版本可以直接作为公共组件放进项目。
package com.example.demo.feishu.service;
import com.example.demo.config.FeishuProperties;
import com.example.demo.feishu.util.FeishuSignUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class FeishuBotService {
private final FeishuProperties feishuProperties;
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
public String sendText(String text) {
Map<String, String> content = new HashMap<>();
content.put("text", text);
return send("text", content);
}
public String sendPost(String title, Object[][] contentArray) {
Map<String, Object> zhCn = new HashMap<>();
zhCn.put("title", title);
zhCn.put("content", contentArray);
Map<String, Object> post = new HashMap<>();
post.put("zh_cn", zhCn);
return send("post", post);
}
public String sendCard(Map<String, Object> card) {
return send("interactive", card);
}
public String send(String msgType, Object content) {
if (Boolean.FALSE.equals(feishuProperties.getEnabled())) {
return "飞书推送未启用";
}
try {
Map<String, Object> body = new HashMap<>();
body.put("msg_type", msgType);
body.put("content", content);
if ("interactive".equals(msgType)) {
body.remove("content");
body.put("card", content);
}
if (StringUtils.hasText(feishuProperties.getSecret())) {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String sign = FeishuSignUtil.genSign(timestamp, feishuProperties.getSecret());
body.put("timestamp", timestamp);
body.put("sign", sign);
}
String json = objectMapper.writeValueAsString(body);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(json, headers);
ResponseEntity<String> response = restTemplate.postForEntity(
feishuProperties.getWebhook(),
entity,
String.class
);
return response.getBody();
} catch (Exception e) {
throw new RuntimeException("飞书推送失败", e);
}
}
}
十八、这篇文章的总结
如果你是第一次做 Spring Boot 集成飞书推送,你只要记住下面这条主线就够了:
创建飞书自定义机器人 → 拿到 webhook → Spring Boot 发 HTTP POST → 文本消息先跑通 → 再加签名校验 → 最后封装成公共通知组件。
飞书官方资料表明,自定义机器人非常适合群聊消息推送,通过 webhook 即可发送多种消息类型;而开启签名校验后,需要额外带上 timestamp 和 sign,并遵守相应频控规则。(飞书开放平台)
对于大多数 Spring Boot 项目来说,这种接入方式已经足够应对:
-
接口异常告警
-
支付结果通知
-
订单状态提醒
-
审批流程通知
-
定时任务执行结果推送
-
运维报警