告别“面条式”代码:基于策略模式的动态卡片通知中心设计(SpringBoot)
最近在写了飞书卡片的管理模块,现在有空了总结一下,分享一套我设计的卡片管理架构,结合了 策略模式 (Strategy Pattern) 和 Spring Boot 的依赖注入机制,实现了卡片模板的集中配置、逻辑隔离和高可靠性发送(大雾)。
整体架构

设计理念:配置驱动与逻辑隔离
目标是:当需要新增或修改一个卡片类型时,只涉及新增一个配置项和一个策略实现类,而无需修改核心服务逻辑。
| 模块 | 核心职责 | 采用模式/技术 | 关键优势 |
|---|---|---|---|
| 通知类型枚举 | 集中配置卡片类型、模板路径和对应的处理策略。 | 枚举 | 配置中心,将所有关联信息强绑定。 |
| 策略模式 | 隔离不同卡片类型的数据填充逻辑(JSON 占位符替换)。 | 策略模式 | 逻辑隔离,避免核心服务代码膨胀。 |
| 策略工厂 | 运行时自动发现并管理所有策略实现。 | Spring ApplicationContextAware | 自动化管理,无需手动注册策略。 |
| 模板存储 | 应用启动时加载所有 JSON 模板到内存。 | Spring InitializingBean | 高性能,避免运行时文件 IO。 |
| API 工具 | 封装底层 API 调用,实现幂等和重试。 | Guava Retryer、飞书SDK | 高可靠性,应对网络波动和限流。 |
1. 策略模式
卡片 JSON 模板中包含大量占位符(如 ${number}、${opinion}),不同卡片类型需要替换的占位符集合和替换逻辑可能不同。策略模式的使用就是为了解决这种类型需要差异化处理的问题。
1.1 策略接口定义
定义通用的卡片占位符替换接口。
// package com.demo.notification.card.strategy;
/**
* 卡片占位符替换策略接口
*/
public interface CardPlaceholderStrategy {
/**
* 根据业务数据,对原始卡片JSON模板进行占位符替换
* @param rawTemplate 原始JSON模板字符串
* @param data 业务数据DTO
* @return 填充占位符后的JSON字符串
*/
String apply(String rawTemplate, NotificationDataDTO data);
}
1.2 策略工厂:Spring 自动发现机制
利用 Spring Boot 的特性,让工厂在应用启动时自动收集所有策略实现。
// package com.demo.notification.card.core;
@Component
public class CardStrategyFactory implements ApplicationContextAware {
private final Map<String, CardPlaceholderStrategy> strategyMap = new HashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
// 关键步骤:利用 Spring API 自动发现所有 CardPlaceholderStrategy 接口的实现类
Map<String, CardPlaceholderStrategy> beans = applicationContext.getBeansOfType(CardPlaceholderStrategy.class);
strategyMap.putAll(beans);
// 此时,所有策略(如 @Component("approvedStrategy"))都已根据其 Bean Name 存储
}
/**
* 根据策略名称(Bean Name)获取对应的策略实现
*/
public CardPlaceholderStrategy getStrategy(String beanName) {
CardPlaceholderStrategy strategy = strategyMap.get(beanName);
if (strategy == null) {
throw new IllegalArgumentException("未找到名为: " + beanName + " 的卡片处理策略。");
}
return strategy;
}
}
1.3 策略实现示例
每个具体的卡片类型(或一组卡片类型)实现自己的填充逻辑。
// package com.demo.notification.card.strategy.impl;
@Component("approvedStrategy")
public class ApprovedCardStrategy implements CardPlaceholderStrategy {
@Override
public String apply(String rawTemplate, NotificationDataDTO data) {
String template = rawTemplate;
// 仅处理“审批通过”卡片所需的占位符,逻辑高度内聚
template = template.replace("${ProcessType}", data.getProcessType() != null ? data.getProcessType() : "未知类型");
template = template.replace("${number}", data.getNumber() != null ? data.getNumber() : "N/A");
// ... 其他特定字段的替换,例如根据数据动态隐藏或显示某些 JSON 块
return template;
}
}
2. 枚举
NotificationTypeEnum 是整个模块的配置中心。它将卡片类型、模板文件路径、默认标题格式和对应的策略 Bean 名称强关联起来,是实现配置驱动的关键。
// package com.demo.notification.card.enums;
@Getter
public enum NotificationTypeEnum {
// 任务通过卡片
TASK_APPROVED(
"task/approved_card.json",
"【已通过】${ProcessType}申请单",
"approvedStrategy" // 关联到 ApprovedCardStrategy 的 Bean Name
),
// 任务驳回卡片
TASK_REJECTED(
"task/rejected_card.json",
"【已驳回】${ProcessType}申请单",
"rejectedStrategy"
);
// ... 其他卡片类型
private final String templateFileName;
private final String defaultTitle;
private final String strategyBeanName;
// 构造函数...
/**
* 核心方法:编排占位符替换流程
*/
public String applyPlaceholders(String rawTemplate, NotificationDataDTO data, CardStrategyFactory factory) {
// 1. 获取并执行类型特定的策略(隔离的逻辑)
CardPlaceholderStrategy strategy = factory.getStrategy(this.strategyBeanName);
String processedContentTemplate = strategy.apply(rawTemplate, data);
// 2. 统一处理卡片头部标题的替换(通用逻辑)
// ... (此处涉及通用的JSON解析和修改逻辑,保证卡片标题一致性)
return processedContentTemplate;
}
// 静态方法:统一更新卡片链接和动作按钮 URL (通用逻辑)
public static String updateCardLinkAndActionButtonUrl(String cardJson, String detailUrl) {
// ... (此处涉及通用的JSON解析和修改逻辑)
return cardJson;
}
}
3. 模板存储
为了避免每次发送卡片时都进行文件 IO 操作,我们利用 Spring 的 InitializingBean 接口,在应用启动时将所有 JSON 模板从文件系统加载到内存中。
// package com.demo.notification.card.services;
@Component
public class CardTemplateStorage implements InitializingBean {
private final Map<NotificationTypeEnum, String> templates = new EnumMap<>(NotificationTypeEnum.class);
private static final String TEMPLATE_PATH = "lark/cards/";
/**
* 在 Spring Bean 初始化完成后执行,加载所有模板文件。
*/
@Override
public void afterPropertiesSet() throws Exception {
for (NotificationTypeEnum type : NotificationTypeEnum.values()) {
// 从 classpath 资源文件中加载 JSON 内容到内存
String jsonContent = loadTemplateFromFile(type.getTemplateFileName());
templates.put(type, jsonContent);
}
// ... 日志记录加载完成
}
/**
* 从内存中快速获取原始 JSON 模板。
*/
public String getTemplate(NotificationTypeEnum type) {
String template = templates.get(type);
// ... 异常检查
return template;
}
}
4. 核心服务
CardNotificationService 负责将上述组件串联起来,完成卡片的生成和发送流程。
// package com.demo.notification.card.services;
@Service
public class CardNotificationService {
private final CardTemplateStorage templateStorage;
private final CardStrategyFactory strategyFactory;
// ... LarkApiUtil (底层API调用工具)
// 依赖注入构造函数...
/**
* 生成并发送卡片通知
*/
@Async("NotificationTaskExecutor") // 使用异步线程池,避免阻塞业务主流程,这里需要根据业务场景配置
public void sendCard(NotificationTypeEnum cardType, NotificationDataDTO data, String detailUrl, List<String> recipientIds) {
try {
// 1. 获取原始模板 (高性能,从内存获取)
String rawTemplate = templateStorage.getTemplate(cardType);
// 2. 应用策略:填充卡片内容和标题 (逻辑隔离)
String filledTemplate = cardType.applyPlaceholders(rawTemplate, data, strategyFactory);
// 3. 统一更新卡片链接和按钮 URL (通用处理)
String finalCardJson = NotificationTypeEnum.updateCardLinkAndActionButtonUrl(filledTemplate, detailUrl);
// 4. 调用 API 工具发送 (高可靠性)
larkApiUtil.sendCustomCardMsg(recipientIds, finalCardJson);
} catch (Exception e) {
// ... 统一异常处理和日志记录
}
}
}
5. API 工具
在底层 API 调用工具中,我们引入 重试机制(例如使用 Guava Retryer)来处理瞬时网络错误或 API 临时限流,极大地提高了通知发送的成功率。
// package com.demo.notification.card.utils;
@Component
public class LarkApiUtil {
// 构造一个具有指数退避策略的重试器
private Retryer<CreateMessageRespBody> buildRetryer() {
return RetryerBuilder.<CreateMessageRespBody>newBuilder()
.retryIfException() // 遇到任何异常都重试
.withWaitStrategy(WaitStrategies.exponentialWait(1, 10, TimeUnit.SECONDS)) // 1s, 2s, 4s... 指数退避
.withStopStrategy(StopStrategies.stopAfterAttempt(3)) // 最多重试3次
.build();
}
/**
* 发送自定义卡片消息给单个用户,并实现重试
*/
public CreateMessageRespBody sendCustomCardMsgToSingleUser(@Nonnull String receiver, @Nonnull String cardJsonContent) {
final Retryer<CreateMessageRespBody> retryer = buildRetryer();
try {
return retryer.call(() -> {
// ... 实际的底层 SDK API 调用
// 如果 API 返回失败状态码,抛出自定义异常,触发重试
// 如果成功,返回结果
});
} catch (ExecutionException | RetryException e) {
// 达到最大重试次数,最终失败
throw new RuntimeException("发送卡片通知失败 - 达到最大重试次数。", e.getCause());
}
}
// ... 批量发送逻辑类似,可能需要处理批量接口的特殊响应
}
6.线程池配置参考
这里其实很难去设定,我目前是参考IO密集型/CPU密集型的大众思路设置的,由于这个项目并发量不是特别大,所以这个参数的精细控制不需要太操心,我也没有继续优化了,但是如果并发量很大的场景,动态配置线程池参数更加合理,并且线程池参数设置方法是支持热重载的,可以说非常贴心了。
总结
这套基于 策略模式的卡片管理架构,将卡片的配置、内容填充、模板获取和发送四个核心环节彻底解耦:
- 高可维护性: 新增卡片类型只需配置枚举和实现策略,符合开闭原则。
- 高内聚低耦合: 每个策略只关心自己的 JSON 替换逻辑,核心服务只负责流程编排。
- 高可靠性: 模板预加载保证性能,API 重试保障通知成功率。
更多推荐


所有评论(0)