[Day13] 微服务架构下的共享基础库设计:contract-common 模块实践

目录

  1. 前言
  2. 为什么需要共享基础库
  3. 模块整体架构
  4. 核心功能详解
  5. 依赖管理
  6. 使用场景
  7. 最佳实践总结
  8. 写在最后

前言

在构建智能审查平台的进程中,随着微服务数量不断递增,各服务间涌现出大量重复代码,诸如 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 行)后再考虑拆分,并且要重点权衡拆分后带来的收益与拆分所付出的代价(例如调试多模块的复杂度增加)。

相关推荐
十三画者18 小时前
【文献分享】SpatialZ弥合从平面空间转录组学到三维细胞图谱之间的维度差距
人工智能·数据挖掘·数据分析·数据可视化
十五年专注C++开发18 小时前
VS2019编译的C++程序,在win10正常运行,在win7上Debug正常运行,Release运行报错0xC0000005,进不了main函数
开发语言·c++·报错c0x0000005
童欧巴18 小时前
DeepSeek V4,定档春节
人工智能·aigc
爱学习的张大18 小时前
深度学习中稀疏专家模型研究综述 A REVIEW OF SPARSE EXPERT MODELS IN DEEP LEARNING
人工智能·深度学习
隐退山林18 小时前
JavaEE:多线程初阶(一)
java·开发语言·jvm
C_心欲无痕18 小时前
ts - 模板字面量类型与 `keyof` 的魔法组合:`keyof T & `on${string}`使用
linux·运维·开发语言·前端·ubuntu·typescript
爱打代码的小林18 小时前
CNN 卷积神经网络 (MNIST 手写数字数据集的分类)
人工智能·分类·cnn
最贪吃的虎18 小时前
Redis其实并不是线程安全的
java·开发语言·数据库·redis·后端·缓存·lua
川西胖墩墩18 小时前
游戏NPC的动态决策与情感模拟
人工智能