目录
前言
在构建智能审查平台的进程中,随着微服务数量不断递增,各服务间涌现出大量重复代码,诸如 DTO 定义、Feign 接口、常量枚举等。若每个服务都对这些代码进行复制,后续维护工作无疑将困难重重。基于此,我决定提取出一个共享基础模块,这便是 contract - common 的诞生契机。本文将与大家分享我在设计这个共享库时的思考与实践经验。
为什么需要共享基础库
目的与定位
其目的在于解耦 management 和 engine 的通用 entity 定义 jar 包。明确定位为仅存放 Feign 接口以及相关定义的 DTO。其他 domain 实体遵循"谁拥有数据,谁管理数据"的原则。controller 的具体实现和 domain 实体均在各自模块内,对外暴露的皆为经过许可的 DTO。当前实际存储内容包括 DTO(Feign 专用)、constant、controller。
微服务架构下的问题
在微服务架构体系中,服务之间通信极为频繁。若每个服务都自行定义一套 Feign 客户端和 DTO,很快便会引发一系列问题:
- 代码重复:相同的 DTO 在多个服务中反复定义。这就好比在不同的办公室都重复制作同一份文件,不仅浪费资源,还增加了管理成本。例如,在一个电商微服务系统中,订单相关的 DTO 在订单服务、库存服务、物流服务中都重复定义,这无疑是一种资源浪费。
- 类型不一致:字段名或类型稍有差异,就可能导致序列化出现问题。这类似于两个人对同一事物的描述方式不同,在信息传递时就容易产生误解。例如,一个服务将用户年龄定义为整数类型,另一个服务定义为字符串类型,在数据交互时就可能出现错误。
- 维护困难:当接口发生变更时,需要在多个地方进行修改,牵一发而动全身。
- 版本冲突:不同服务所使用的 DTO 版本可能不一致。
而共享基础库的存在,正是为了有效解决这些问题。
模块整体架构
目录结构设计
contract - common 的目录结构如下:
contract - common/
├── config/ # 配置类
│ └── FeignConfig.java
├── constant/ # 常量定义
│ ├── ContractStatus.java
│ ├── ContractType.java
│ ├── FeignApiPath.java
│ └── FeignServiceName.java
├── dto/ # 通用 DTO
├── enums/ # 枚举类
│ └── ReviewTypeDetail.java
├── feign/ # Feign 客户端
│ ├── ContractFeignClient.java
│ ├── PromptFeignClient.java
│ ├── ReviewRuleFeignClient.java
│ ├── dto/ # Feign 专用 DTO
│ └── fallback/ # 降级实现
└── util/ # 工具类
└── DateUtils.java
架构图
外部依赖
业务服务层
依赖
依赖
依赖
使用
使用
contract - common 共享库
配置
定义
传输数据
异常降级
FeignConfig
认证配置
Feign客户端接口
常量与枚举
API路径定义
数据传输对象
降级实现
工具类
业务管理服务
审查引擎服务
其他服务...
服务发现
Spring Cloud OpenFeign
核心功能详解
1. Feign 客户端统一管理
Feign 客户端作为服务间通信的关键桥梁,我将所有服务的 Feign 接口统一定义在 contract - common 中:
ContractFeignClient
+createContract() : Long
+updateContract() : Boolean
+getContractById() : ContractDTO
+searchContracts() : PageResult
+deleteContract() : Boolean
PromptFeignClient
+createPrompt() : Long
+updatePrompt() : Boolean
+getPromptById() : PromptDTO
ReviewRuleFeignClient
+createRule() : Long
+updateRule() : Boolean
+searchRules() : PageResult
ClauseFeignClient
+searchByContract() : List<ClauseDTO>
+searchByTask() : List<ClauseDTO>
ClauseExtractionFeignClient
+triggerExtraction() : Response
+deleteExtraction() : Response
<<interface>>
FeignClient
如此设计带来诸多好处:
- 接口统一:所有服务调用同一接口定义,如同大家都遵循相同的交通规则,通行更加顺畅。
- 类型安全:在编译期就能察觉类型错误,提前避免问题发生。
- 文档清晰:接口本身即充当文档,方便开发人员查阅。
2. 认证上下文传播
在微服务之间进行调用时,传递用户认证信息至关重要。我在 FeignConfig 中实现了自动转发功能:
java
@Configuration
public class FeignConfig implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 转发认证相关 Header
String token = request.getHeader("Authorization");
String userId = request.getHeader("userId");
String username = request.getHeader("username");
String tenantId = request.getHeader("tenantId");
if (token != null) template.header("Authorization", token);
if (userId != null) template.header("userId", userId);
// ... 其他 header
}
}
}
该设计具备以下亮点:
- 无感知传递:业务代码无需手动传递认证信息,开发人员无需为此操心,提高开发效率。
- 上下文保持:整条调用链的用户身份始终一致,保证数据的准确性和安全性。
- 安全性:支持内部免鉴权密钥,进一步增强系统安全性。
3. API 路径集中管理
为防止硬编码 URL 带来的诸多问题,创建了 FeignApiPath 类,将所有 API 路径进行集中管理:
java
public class FeignApiPath {
// 业务管理相关
public static final String CONTRACT_BASE = "/api/contracts";
public static final String CONTRACT_BY_ID = CONTRACT_BASE + "/{id}";
public static final String CONTRACT_SEARCH = CONTRACT_BASE + "/search";
// 提示词管理相关
public static final String PROMPT_BASE = "/api/prompts";
// ... 更多路径定义
}
这样做的优势在于:
- 统一管理:路径发生变更时,仅需在一处进行修改,便捷高效。
- 避免拼写错误:在编译期即可进行检查,减少错误发生概率。
- 易于维护:能够迅速定位所有 API 定义,方便后续维护。
4. 容错降级机制
考虑到服务调用可能出现失败的情况,为每个 Feign 客户端都配备了降级实现:
是
否
服务调用
目标服务可用?
正常返回
Fallback降级
记录日志
返回默认值
降级实现的典型模式如下:
java
@Component
public class ContractFeignClientFallback implements ContractFeignClient {
@Override
public ContractDTO getContractById(Long id) {
log.warn("Contract service unavailable, returning null for id: {}", id);
return null; // 或返回缓存数据
}
@Override
public PageResult searchContracts(QueryDTO query) {
log.warn("Contract service unavailable, returning empty result");
return PageResult.empty();
}
}
5. DTO 定义规范
DTO 作为服务间数据传递的重要载体,我遵循了以下设计原则:
DTO设计原则
无业务逻辑
序列化友好
版本兼容
文档完整
只包含字段和getter/setter
使用Jackson注解
新增字段使用JsonIgnore
添加JavaDoc注释
典型 DTO 示例:
java
@Data
public class ContractFeignDTO {
@JsonProperty("id")
private Long id;
@JsonProperty("name")
private String name;
@JsonProperty("party_info")
private PartyInfoFeignDTO partyInfo;
@JsonProperty("created_at")
private LocalDateTime createdAt;
// 嵌套 DTO
@Data
public static class PartyInfoFeignDTO {
private String partyA;
private String partyB;
}
}
6. 工具类与常量
共享库还提供了一些通用工具类和常量定义:
日期工具:
java
@UtilityClass
public class DateUtils {
private static final String DEFAULT_FORMAT = "yyyy - MM - dd HH:mm:ss";
public String format(LocalDateTime dateTime) {
return dateTime.format(DateTimeFormatter.ofPattern(DEFAULT_FORMAT));
}
public LocalDateTime parse(String dateTimeStr) {
return LocalDateTime.parse(dateTimeStr,
DateTimeFormatter.ofPattern(DEFAULT_FORMAT));
}
.....
}
枚举定义:
java
public enum ContractType {
SALES("销售类"),
PURCHASE("采购类"),
SERVICE("服务类"),
// ... 更多类型
private final String displayName;
public static ContractType of(String displayName) {
return Arrays.stream(values())
.filter(t -> t.displayName.equals(displayName))
.findFirst()
.orElse(OTHER);
}
}
依赖管理
共享库的依赖管理至关重要,必须秉持"最小依赖"原则:
xml
<dependencies>
<!-- Jakarta Persistence API - JPA 注解支持 -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
</dependency>
<!-- Jackson - JSON 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson -databind</artifactId>
</dependency>
<!-- Validation - 参数校验 -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<!-- OpenFeign - 服务间通信 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring - cloud - starter - openfeign</artifactId>
</dependency>
<!-- Lombok - 代码生成(provided) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
需注意 provided scope 的运用,这些依赖由使用方提供,共享库不会将其打包进去。
使用场景
场景一:新增业务服务
当需要新增一个服务时,仅需引入 contract - common 依赖,即可直接调用其他服务的接口:
xml
<dependency>
<groupId>com.saltyfish</groupId>
<artifactId>contract - common</artifactId>
<version>0.0.1</version>
</dependency>
随后直接注入 Feign 客户端:
java
@Service
public class MyService {
@Autowired
private ContractFeignClient contractClient;
public void doSomething() {
ContractDTO contract = contractClient.getContractById(123L);
// 处理业务逻辑...
}
}
场景二:接口变更
当某个服务需要新增接口时,只需在 contract - common 中更新对应的 Feign 客户端:
java
// ContractFeignClient.java 新增方法
@GetMapping(FeignApiPath.CONTRACT_STATISTICS)
StatisticsDTO getStatistics();
接着升级共享库版本,所有使用方便能获取新接口。
场景三:跨服务复用枚举
一些业务枚举在多个服务中均有使用需求,将其放置在共享库中能够确保一致性:
java
// 在任何服务中都可以使用
if (contract.getType() == ContractType.SALES) {
// 销售类合同的特殊处理
}
最佳实践总结
- 保持稳定:共享库的变更务必谨慎,尽可能做到向后兼容,如同建造房屋,尽量不破坏原有结构。
- 版本管理:采用语义化版本,一旦出现 breaking change,应升级主版本,便于管理和维护。
- 文档先行:接口变更前,先更新文档,再修改代码,确保开发人员都能清楚了解变更内容。
- 最小依赖:仅引入必需的依赖,有效减小 JAR 包大小,提高系统性能。
- 充分测试:共享库的 bug 会对所有服务产生影响,因此测试必须全面充分,确保质量。
写在最后
contract - common 虽只是一个简洁的共享库,但在微服务架构中却占据着举足轻重的地位。它宛如系统的"神经系统",紧密连接着各个服务,确保数据能够顺利、安全地传输。一个出色的共享库设计,能够显著提升团队的开发效率。当然,也要避免过度设计,毕竟它只是服务于业务的工具,达成业务目标才是最终宗旨。倘若你也在从事微服务项目,希望这些经验能为你提供有益的参考与帮助!
同时,我在实践过程中,对 engine - common - management 的拆分产生了一些思考。起初,觉得其业务耦合度较高,似乎存在过度拆分的情况。经过向 AI 咨询并自我反思后发现,拆分本身的方向是正确的,职责边界也很清晰。然而,当前代码量和业务完成度较低,此阶段的拆分实际意义不大,更像是为了拆分而拆分。或许应等到业务场景明晰、稳定迭代且代码量达到一定规模(例如 10w 行)后再考虑拆分,并且要重点权衡拆分后带来的收益与拆分所付出的代价(例如调试多模块的复杂度增加)。