你将学到:为什么微服务一定要做公共模块、common sdk 里装什么、它凭什么"一引入就能用"(自动配置 + 组件扫描)、为什么说它本身就是一个公共 SDK、以及依赖怎么分发和做版本管理。
一、先说痛点:为什么微服务一定要做公共模块
微服务把一个大系统拆成了一堆独立服务。比如一个仓储项目就拆成了 10 个服务:网关、系统、仓库、商品、承运商、入库、库存、出库、运输、计费报表。问题来了——这些服务里有大量"一模一样"的代码:
- 每个服务都要返回统一格式
{success, code, message, data}→ 都要写一个ApiResponse - 每个服务都要做全局异常处理 → 都要写一个
GlobalExceptionHandler - 每个服务的表都有
create_time / update_time / tenant_id→ 都要写一个BaseEntity - 每个服务都要用 JWT、分页对象、审计填充……
如果每个服务各写一套,会出现三个灾难:
| 问题 | 后果 |
|---|---|
| 重复造轮子 | 10 个服务写 10 遍 ApiResponse,浪费时间 |
| 风格不统一 | A 服务返回 code=0,B 服务返回 status=200,前端崩溃 |
| 改一处要改十处 | 统一响应想加个字段,要去 10 个服务里改 |
💡 解决思路:把这些公共的类和配置抽出来,做成一个独立模块,打成 jar 包,让所有服务都依赖它。这就是「公共模块封装」——也是一个最基础的「公共 SDK」。
二、整体结构:单仓多模块,common 是公共底座
示例项目用的是 单仓多模块(Maven 聚合 / reactor) 结构:一个父工程 xiangmu-cloud,下面挂公共模块 + 10 个业务服务。
真实的父 POM(xiangmu-cloud/pom.xml):
<groupId>com.xiangmu</groupId>
<artifactId>xiangmu-cloud</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>xiangmu-common</module>
<module>gateway-service</module>
<module>system-service</module>
<module>product-service</module>
<module>inventory-service</module>
</modules>
关键点:
xiangmu-common和各服务在同一个父工程下,所以服务可以直接以模块依赖的方式引用它(reactor 内部依赖),这是教学项目最简单直接的方式。企业里如果 common 要被多个独立仓库共享,才需要发到 Maven 私服——这点第六节细讲。
三、xiangmu-common 里到底装了什么
按"职责"分门别类,主要有这几类(均为真实包路径 com.xiangmu.common.*):
| 分类 | 包 | 代表类 | 作用 |
|---|---|---|---|
| 统一响应 | api | ApiResponse、ListResult、PageRequest | 所有接口返回同一种格式 |
| 基础实体 | entity | BaseEntity | 审计字段 + 租户字段 + 逻辑删除 |
| 统一异常 | exception | BusinessException | 全局统一的业务异常 |
| 全局处理 | web | GlobalExceptionHandler、TenantContextInterceptor | 异常兜底、请求拦截 |
| 自动配置 | config | xiangmuJwtAutoConfiguration 等 5 个 | 开箱即用的通用配置 |
| JWT | jwt | JwtUtil、JwtProperties、JwtPayload | 令牌生成/校验 |
| MyBatis | mybatis | AuditInterceptor、TenantLineInterceptor、BaseMapper | 审计填充、SQL 拦截 |
| 基础设施 | cache/lock/mq/idempotent | CacheService、DistributedLock、MqPublisher | 缓存/分布式锁/消息/幂等 |
🎥 真实代码 1:统一响应 ApiResponse
@Data
public class ApiResponse<T> implements Serializable {
private boolean success; // 是否成功
private String code; // 业务码:0=成功,非0=失败
private String message; // 提示信息
private T data; // 业务数据
public static <T> ApiResponse<T> success(T data) {
return success(data, "操作成功");
}
public static <T> ApiResponse<T> failure(String code, String message) {
ApiResponse<T> r = new ApiResponse<>();
r.setSuccess(false);
r.setCode(code);
r.setMessage(message);
return r;
}
}
有了它,所有服务的 Controller 都
return ApiResponse.success(data),前端永远只对接一种格式。
🎥 真实代码 2:基础实体 BaseEntity
@Data
public abstract class BaseEntity implements Serializable {
private Long tenantId; // 租户ID(多租户行级隔离,所有业务表必带)
private Instant createTime; // 创建时间
private Instant updateTime; // 更新时间
private String createBy; // 创建人
private String updateBy; // 更新人
private Boolean deleted; // 逻辑删除标记
}
各服务的实体只要
extends BaseEntity,这些公共字段就都有了。配合AuditInterceptor,写入时还能自动填值(create_time/update_time 等),不用手动 set。
🎥 真实代码 3:全局异常处理 GlobalExceptionHandler
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class) // 业务异常
public ApiResponse<Void> handleBusiness(BusinessException ex) {
return ApiResponse.failure(ex.getErrorCode(), ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class) // 参数校验异常
public ApiResponse<Void> handleValidation(MethodArgumentNotValidException ex) { ... }
@ExceptionHandler(Exception.class) // 系统异常兜底
public ApiResponse<Void> handleException(Exception ex) {
return ApiResponse.failure("SYSTEM_ERROR", "系统异常,请稍后重试");
}
}
业务代码里只管
throw new BusinessException("库存不足"),剩下"转成统一错误返回"的活由它兜底——每个服务都不用再写一遍。
四、为什么"引入依赖就能用"?
思考:
JwtUtil、AuditInterceptor、GlobalExceptionHandler这些都写在 xiangmu-common 里,为什么别的服务没写任何 @Bean,引入依赖后就自动生效了?
用了两种互补的装配机制:
4.1 机制①:自动配置(Auto Configuration)
这就是大家常说的「starter」原理。在 xiangmu-common 里放一个文件:
resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,内容是要加载的配置类清单:
com.xiangmu.common.config.xiangmuJwtAutoConfiguration
com.xiangmu.common.config.xiangmuWebMvcAutoConfiguration
com.xiangmu.common.config.xiangmuMyBatisAutoConfiguration
com.xiangmu.common.config.xiangmuCacheAutoConfiguration
com.xiangmu.common.config.xiangmuMqAutoConfiguration
这等于告诉 Spring Boot:「启动时请加载这几个配置类」。 (补充:Spring Boot 2.7 之前用的是
spring.factories;新项目统一用AutoConfiguration.imports)
🎥 真实配置类(注意 @AutoConfiguration + 一堆条件注解):
@AutoConfiguration
@ConditionalOnClass(SqlSessionFactory.class) // 类路径有 MyBatis 才生效
@ConditionalOnProperty(prefix = "xiangmu.tenant", name = "enabled",
havingValue = "true", matchIfMissing = true) // 配置开关可关
@EnableConfigurationProperties(TenantProperties.class)
public class xiangmuMyBatisAutoConfiguration {
@Bean
public TenantLineInterceptor tenantLineInterceptor(TenantProperties p) { ... }
@Bean
public AuditInterceptor auditInterceptor() { return new AuditInterceptor(); }
}
@AutoConfiguration
@EnableConfigurationProperties(JwtProperties.class)
public class xiangmuJwtAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 服务自己没定义才用我的
public JwtUtil jwtUtil(JwtProperties properties) {
return new JwtUtil(properties);
}
}
@ConditionalOnXxx 是自动配置的灵魂,让 SDK 既开箱即用又灵活:
| 注解 | 含义 | 好处 |
|---|---|---|
@ConditionalOnClass | 类路径有某个类才生效 | 没引相关依赖就不瞎配 |
@ConditionalOnMissingBean | 服务自己没定义才用我的 | 允许业务方覆盖默认实现 |
@ConditionalOnProperty | 配置开关打开才生效 | 一行配置就能关掉某功能 |
4.2 机制②:组件扫描
像 GlobalExceptionHandler 这种 @RestControllerAdvice(本质是 Spring 组件),靠的是业务服务启动类开启包扫描:
@SpringBootApplication(scanBasePackages = "com.xiangmu") // 扫描 com.xiangmu 装配公共组件
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
因为公共组件都在
com.xiangmu.common下,启动类把扫描范围设成com.xiangmu,就能把 common 里的@RestControllerAdvice、@Component一并扫进来。
✅ 一句话总结:自动配置负责"注册第三方/工具型 Bean",组件扫描负责"捡起公共注解组件",两者配合 → 服务只要依赖 common 就全部就位。
五、为什么说 xiangmu-common 本身就是一个「公共 SDK」
回头看 xiangmu-common 做的事:它把统一响应、统一异常、JWT、审计、多租户拦截这些通用能力封装成一个 jar,对外提供简单的类和注解,使用方加个依赖就能用——这正是 SDK 的本质:
SDK = 把通用/复杂的能力封装好,让使用方傻瓜式接入。
所以 xiangmu-common 就是这个项目里最基础的「公共 SDK」。
而且这种封装方式是可复制的:如果将来有别的通用能力需要沉淀,完全可以用同样的套路再建一个 SDK 模块。最典型的就是服务间调用的 client-sdk:
示例项目每个服务都有一个
xxx-client-sdk(如product-client-sdk)——别的服务想调商品服务,直接引它的 client-sdk 就行。它的封装思路和 common 一脉相承,是后面《Feign 服务间调用 SDK 封装》的内容,这里先建立"还能这样扩展"的概念即可。
六、依赖怎么分发 + 版本怎么管理
公共模块写好了,怎么让各个服务用上它? 分两种场景。
6.1 场景 A:单仓多模块(教学项目用的方式)
示例项目所有服务和 common 在同一个父工程里,所以服务直接模块依赖即可——连版本号都不用写(由父工程统一管理):
🎥 真实代码(product-service/product-common/pom.xml):
<dependency>
<groupId>com.xiangmu</groupId>
<artifactId>xiangmu-common</artifactId>
</dependency>
优点:简单、改完 common 立即生效,不用发包。适合一个仓库装下所有服务的项目(教学、中小项目)。
6.2 场景 B:跨独立仓库共享 → 发 Maven 私服
企业里服务往往拆在不同的 git 仓库里,没法 reactor 直接依赖。这时就要把 common 发布到 Maven 私服(私有仓库),其他仓库像引普通依赖一样引入。
为什么用私服而不是 Maven 中央仓库?
| 原因 | 说明 |
|---|---|
| 代码保密 | 公司内部包不能传到公开的中央仓库 |
| 统一管理 | 团队统一从私服拉依赖,版本可控可审计 |
| 加速缓存 | 私服会缓存中央仓库的包,内网拉取更快 |
常见私服:Nexus、Artifactory、阿里云效、GitHub Packages。原理一样:一个放私有 jar 的远程仓库。
发布配置示例(在公共模块 / 父 pom 加 distributionManagement):
<distributionManagement>
<repository>
<id>company-nexus</id>
<url>https://nexus.your-company.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>company-nexus</id>
<url>https://nexus.your-company.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
凭据写在 ~/.m2/settings.xml(id 要和上面对上):
<servers>
<server>
<id>company-nexus</id>
<username>你的账号</username>
<password>你的密码/Token</password>
</server>
</servers>
然后执行发布:
mvn clean deploy # 编译 → 打包 → 上传到私服
其他仓库的服务,在 pom 里加上 common 依赖(这次要写明 version)即可使用。
📌 示例项目是教学项目,采用的是 6.1 单仓模块依赖,没有发私服。6.2 是给你了解企业里跨仓库共享时的标准做法。
6.3 版本管理:集中管理 + SNAPSHOT/RELEASE
① 版本集中管理:示例项目在父 pom 用 <properties> 统一定义所有依赖版本,子模块只引坐标不写版本,避免"同一个库各服务版本不一致":
🎥 真实代码(xiangmu-cloud/pom.xml):
<properties>
<java.version>21</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
<mybatis.version>3.0.3</mybatis.version>
<redisson.version>3.27.2</redisson.version>
<jjwt.version>0.12.5</jjwt.version>
</properties>
配合
<dependencyManagement>,子模块引依赖时不写版本,版本号只在父 pom 改一处——这是生产环境管理依赖版本的标准方式。
② SNAPSHOT 与 RELEASE:
| 版本类型 | 例子 | 含义 | 特点 |
|---|---|---|---|
| SNAPSHOT | 1.0.0-SNAPSHOT | 开发中的快照版 | 同一版本号可反复覆盖,拉取取最新 |
| RELEASE | 1.0.0 | 正式发布版 | 不可覆盖,发了就锁死,保证稳定 |
示例项目是教学项目,全程用
SNAPSHOT(方便边改边用)。 生产实践:联调阶段用 SNAPSHOT(改完重发,别人拉到最新);正式上线打 RELEASE 版本并锁定,保证线上依赖不会被人偷偷改动。
七、其他服务接入:两步搞定
以商品服务 product-service 为例:
第一步:pom 引入依赖(单仓不写 version):
<dependency>
<groupId>com.xiangmu</groupId>
<artifactId>xiangmu-common</artifactId>
</dependency>
第二步:启动类设好扫描范围(让组件扫描 + 自动配置都生效):
@SpringBootApplication(scanBasePackages = "com.xiangmu")
public class ProductApplication { ... }
之后业务代码直接用,公共能力已就位:
@GetMapping("/{id}")
public ApiResponse<ProductVO> get(@PathVariable Long id) {
return ApiResponse.success(productService.get(id)); // 统一响应,开箱即用
}
八、小结
五句话带走:
- 微服务必做公共模块:否则重复造轮子、风格不统一、改一处要改十处。
- common sdk 是底座:统一响应、统一异常、基础实体、JWT、审计全在这。
- 装配是灵魂:自动配置(
AutoConfiguration.imports+@ConditionalOnXxx)注册 Bean,组件扫描(scanBasePackages)捡注解组件,做到"引入即用、又可覆盖"。 - 它本身就是一个公共 SDK:把通用能力封装成 jar;需要别的能力(如 client-sdk)可用同样套路再建。
- 分发与版本:教学项目单仓模块直接依赖;企业跨仓库发 Maven 私服;版本集中管理,SNAPSHOT 联调、RELEASE 上线。