ruoyi-vue-pro框架架构剖析
本文深入剖析ruoyi-vue-pro框架的Maven多模块工程设计、模块职责划分、依赖管理以及自动装配机制,结合企业级实践讲解核心原理。
1.1 Maven多模块工程结构设计
为什么需要多模块工程?
在传统的单体应用中,所有代码都在一个工程里,随着业务复杂度增加会出现:
- 编译慢:修改一个类需要重新编译整个项目
- 职责不清:业务代码、框架代码、工具类混在一起
- 无法复用:其他项目想用某个模块,只能复制代码
- 团队协作难:多人修改同一个工程,冲突频繁
多模块工程解决方案: 将一个大工程拆分成多个Maven模块,每个模块独立编译、独立打包,通过Maven依赖关系组织在一起。
典型的企业级多模块结构
以电商业务系统为例,展示ruoyi-vue-pro的典型模块划分:
demo-platform/ # 根目录(聚合工程)
├── pom.xml # 父POM(版本管理中心)
├── demo-framework/ # 框架层(技术组件封装)
│ ├── demo-spring-boot-starter-web/
│ ├── demo-spring-boot-starter-mybatis/
│ └── demo-spring-boot-starter-redis/
├── demo-module-system/ # 系统模块(用户、角色、菜单)
│ ├── demo-module-system-api/
│ └── demo-module-system-biz/
├── demo-module-order/ # 订单模块
│ ├── demo-module-order-api/ # 订单API(接口定义)
│ └── demo-module-order-biz/ # 订单业务实现
├── demo-module-payment/ # 支付模块
│ ├── demo-module-payment-api/ # 支付API
│ └── demo-module-payment-biz/ # 支付业务实现
└── demo-server/ # 应用启动模块
└── src/main/resources/
└── application.yml
三层架构分层原则
ruoyi-vue-pro采用三层分层架构,每层职责明确:
1. framework层(框架层)
- 职责:封装技术组件,提供开箱即用的能力
- 包含:MyBatis配置、Redis配置、Web配置、统一异常处理等
- 特点:与业务无关,可跨项目复用
- 原理:通过Spring Boot的AutoConfiguration机制实现自动装配
2. module层(业务层)
- 职责:实现具体业务逻辑,按业务域拆分
- 包含:订单模块、支付模块、商品模块等
- 特点:每个模块内部又分api和biz子模块
- 原理:通过Maven依赖管理模块间调用关系
3. server层(启动层)
- 职责:整合所有模块,提供应用启动入口
- 包含:启动类、配置文件、打包配置
- 特点:只有这一层最终打成可执行jar包
- 原理:依赖所有biz模块,Spring Boot自动扫描并装配
父POM配置详解
父POM是整个工程的版本控制中心,所有子模块都继承它:
xml
<!-- demo-platform/pom.xml -->
<groupId>com.company</groupId>
<artifactId>demo-platform</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging> <!-- 注意:父POM必须是pom类型 -->
<!-- 聚合所有子模块 -->
<modules>
<module>demo-framework</module>
<module>demo-module-system</module>
<module>demo-module-order</module>
<module>demo-module-payment</module>
<module>demo-server</module>
</modules>
关键点说明:
| 配置项 | 作用 | 必要性 |
|---|---|---|
<packaging>pom</packaging> |
声明这是聚合工程,不产生jar包 | 必须 |
<modules> |
聚合子模块,执行mvn install时会依次构建 |
必须 |
<parent> |
子模块通过parent继承父POM的配置 | 子模块必须 |
为什么要这样设计?
- 统一构建 :执行
mvn clean install一次性编译所有模块 - 版本统一:所有子模块的版本号统一由父POM管理
- 依赖继承:子模块自动继承父POM的依赖管理配置
模块分层的核心价值
| 维度 | 单体应用 | 多模块工程 |
|---|---|---|
| 编译速度 | 修改一处需要全量编译(耗时长) | 只编译修改的模块(快速) |
| 代码复用 | 复制粘贴代码 | Maven依赖引入 |
| 团队协作 | 代码冲突频繁 | 模块独立开发,减少冲突 |
| 职责划分 | 代码混乱,难以维护 | 职责清晰,易于维护 |
| 持续集成 | 全量打包部署 | 只部署变更模块 |
1.2 模块职责划分(api/biz分离)
为什么要api/biz分离?
在微服务架构或模块化开发中,经常遇到跨模块调用的场景:
- 订单模块需要调用支付模块查询支付状态
- 商品模块需要调用库存模块扣减库存
- 用户模块需要调用权限模块验证权限
不分离的问题:
xml
<!-- 订单模块依赖整个支付模块 -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-payment</artifactId> <!-- 依赖整个biz模块 -->
</dependency>
这会导致:
- 依赖臃肿:订单模块只需要支付查询接口,却引入了支付模块的所有实现类、Mapper、配置等
- 循环依赖风险:支付模块如果也依赖订单模块,就会形成循环依赖,Maven无法构建
- 启动冲突:两个模块都扫描到对方的@Service、@Mapper,导致Bean重复定义
api/biz分离方案:
xml
<!-- 订单模块只依赖支付的api -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-payment-api</artifactId> <!-- 只依赖接口定义 -->
</dependency>
api模块:接口契约层
api模块是模块对外暴露的接口规范,只包含接口定义,不包含任何实现。
标准目录结构:
java
demo-module-order-api/
├── OrderApi.java // 核心API接口
├── dto/ // 数据传输对象
│ ├── OrderCreateReqDTO.java // 创建订单请求DTO
│ ├── OrderRespDTO.java // 订单响应DTO
│ └── OrderQueryReqDTO.java // 查询订单请求DTO
├── enums/ // 枚举定义
│ ├── OrderStatusEnum.java // 订单状态枚举
│ └── OrderTypeEnum.java // 订单类型枚举
└── constants/ // 常量定义
└── OrderConstants.java // 订单相关常量
核心API接口示例:
java
package com.company.module.order.api;
import com.company.module.order.api.dto.*;
import java.util.Collection;
import java.util.List;
/**
* 订单API接口
*
* <p>设计原则:
* 1. 只定义接口方法,不包含任何实现逻辑
* 2. 参数和返回值都使用DTO对象,不暴露DO(数据库实体)
* 3. 方法命名清晰,见名知义
* 4. 添加必要的JavaDoc注释
*/
public interface OrderApi {
/**
* 根据订单号查询订单
*
* @param orderNo 订单号
* @return 订单信息,不存在返回null
*/
OrderRespDTO getByOrderNo(String orderNo);
/**
* 创建订单
*
* @param reqDTO 创建订单请求参数
* @return 订单ID
*/
Long createOrder(OrderCreateReqDTO reqDTO);
/**
* 批量查询订单
*
* @param ids 订单ID集合
* @return 订单列表
*/
List<OrderRespDTO> listByIds(Collection<Long> ids);
/**
* 验证订单是否存在
*
* @param orderId 订单ID
* @return true-存在 false-不存在
*/
Boolean validateOrderExists(Long orderId);
}
DTO对象设计:
java
package com.company.module.order.api.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单响应DTO
*
* <p>关键点:
* 1. 实现Serializable接口,支持序列化(RPC调用需要)
* 2. 只包含必要字段,不暴露敏感信息
* 3. 使用包装类型(Integer而非int),避免null值问题
*/
@Data
public class OrderRespDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** 订单ID */
private Long id;
/** 订单号 */
private String orderNo;
/** 用户ID */
private Long userId;
/** 订单金额 */
private BigDecimal amount;
/** 订单状态 1-待支付 2-已支付 3-已取消 */
private Integer status;
/** 创建时间 */
private LocalDateTime createTime;
}
枚举类设计:
java
package com.company.module.order.api.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单状态枚举
*
* <p>为什么要定义枚举?
* 1. 类型安全:避免使用魔法值(1、2、3)
* 2. 自文档化:枚举名称清晰表达含义
* 3. IDE提示:编码时有智能提示
* 4. 统一管理:状态值统一维护,避免重复定义
*/
@Getter
@AllArgsConstructor
public enum OrderStatusEnum {
WAIT_PAY(1, "待支付"),
PAID(2, "已支付"),
CANCELLED(3, "已取消"),
COMPLETED(4, "已完成"),
REFUNDED(5, "已退款");
/** 状态值 */
private final Integer status;
/** 状态描述 */
private final String desc;
/**
* 根据状态值获取枚举
*/
public static OrderStatusEnum valueOf(Integer status) {
for (OrderStatusEnum e : values()) {
if (e.getStatus().equals(status)) {
return e;
}
}
return null;
}
}
biz模块:业务实现层
biz模块是具体业务逻辑的实现,包含完整的MVC三层架构。
标准目录结构:
java
demo-module-order-biz/
├── controller/ // 控制层(HTTP接口)
│ ├── admin/ // 后台管理接口
│ │ └── OrderController.java
│ └── app/ // 前台用户接口
│ └── AppOrderController.java
├── service/ // 服务层(业务逻辑)
│ ├── OrderService.java // 服务接口
│ └── impl/
│ └── OrderServiceImpl.java // 服务实现
├── dal/ // 数据访问层(Data Access Layer)
│ ├── dataobject/ // 数据库实体对象(DO)
│ │ └── OrderDO.java
│ └── mapper/ // MyBatis Mapper接口
│ └── OrderMapper.java
├── convert/ // 对象转换器
│ └── OrderConvert.java // DTO/DO/VO之间转换
└── api/ // API接口实现
└── OrderApiImpl.java // 实现api模块定义的接口
Controller层示例:
java
package com.company.module.order.biz.controller.admin;
import com.company.framework.common.pojo.CommonResult;
import com.company.framework.common.pojo.PageResult;
import com.company.module.order.biz.controller.admin.vo.*;
import com.company.module.order.biz.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 订单管理Controller
*
* <p>职责:
* 1. 接收HTTP请求,参数校验
* 2. 调用Service处理业务
* 3. 返回统一格式响应
*/
@Tag(name = "管理后台 - 订单管理")
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
@Validated
public class OrderController {
private final OrderService orderService;
@GetMapping("/get")
@Operation(summary = "查询订单详情")
public CommonResult<OrderRespVO> getOrder(@RequestParam("id") Long id) {
OrderDO order = orderService.getById(id);
return CommonResult.success(OrderConvert.INSTANCE.convert(order));
}
@PostMapping("/create")
@Operation(summary = "创建订单")
public CommonResult<Long> createOrder(@Valid @RequestBody OrderCreateReqVO reqVO) {
return CommonResult.success(orderService.createOrder(reqVO));
}
@GetMapping("/page")
@Operation(summary = "分页查询订单列表")
public CommonResult<PageResult<OrderRespVO>> getOrderPage(@Valid OrderPageReqVO pageVO) {
PageResult<OrderDO> pageResult = orderService.getOrderPage(pageVO);
return CommonResult.success(OrderConvert.INSTANCE.convertPage(pageResult));
}
}
Service层实现:
java
package com.company.module.order.biz.service.impl;
import com.company.module.order.biz.dal.dataobject.OrderDO;
import com.company.module.order.biz.dal.mapper.OrderMapper;
import com.company.module.order.biz.service.OrderService;
import com.company.module.payment.api.PaymentApi;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 订单Service实现类
*
* <p>职责:
* 1. 实现业务逻辑
* 2. 调用Mapper操作数据库
* 3. 调用其他模块的API
* 4. 事务管理
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
// 注入支付模块的API(通过api依赖实现跨模块调用)
private final PaymentApi paymentApi;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createOrder(OrderCreateReqVO reqVO) {
// 1. 构建订单DO对象
OrderDO order = OrderConvert.INSTANCE.convert(reqVO);
order.setOrderNo(generateOrderNo());
order.setStatus(OrderStatusEnum.WAIT_PAY.getStatus());
// 2. 插入数据库
orderMapper.insert(order);
log.info("[创建订单] 订单号={}", order.getOrderNo());
// 3. 调用支付模块创建支付单(跨模块调用)
paymentApi.createPayment(order.getId(), order.getAmount());
return order.getId();
}
@Override
public OrderDO getByOrderNo(String orderNo) {
return orderMapper.selectByOrderNo(orderNo);
}
/**
* 生成订单号
* 规则:yyyyMMddHHmmss + 6位随机数
*/
private String generateOrderNo() {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String random = String.format("%06d", ThreadLocalRandom.current().nextInt(1000000));
return timestamp + random;
}
}
API接口实现类:
java
package com.company.module.order.biz.api;
import com.company.module.order.api.OrderApi;
import com.company.module.order.api.dto.*;
import com.company.module.order.biz.convert.OrderConvert;
import com.company.module.order.biz.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
/**
* 订单API实现类
*
* <p>关键点:
* 1. 实现api模块定义的接口
* 2. 供其他模块通过Spring依赖注入调用
* 3. 如果是微服务架构,通过Feign远程调用
* 4. 返回值必须是DTO对象,不能返回DO
*/
@Service
@RequiredArgsConstructor
public class OrderApiImpl implements OrderApi {
private final OrderService orderService;
@Override
public OrderRespDTO getByOrderNo(String orderNo) {
OrderDO order = orderService.getByOrderNo(orderNo);
// 将DO转换为DTO(不暴露数据库字段)
return OrderConvert.INSTANCE.convertToApi(order);
}
@Override
public Long createOrder(OrderCreateReqDTO reqDTO) {
return orderService.createOrderByApi(reqDTO);
}
@Override
public List<OrderRespDTO> listByIds(Collection<Long> ids) {
List<OrderDO> orders = orderService.listByIds(ids);
return OrderConvert.INSTANCE.convertListToApi(orders);
}
@Override
public Boolean validateOrderExists(Long orderId) {
return orderService.getById(orderId) != null;
}
}
api/biz分离的核心价值
1. 依赖解耦
支付模块 ───依赖──> 订单api模块
↓ 实现
订单biz模块
- 支付模块只依赖订单的api,不依赖biz
- 订单的biz内部实现如何变化,支付模块都不受影响
- 减小依赖传递范围,避免引入不必要的jar包
2. 接口稳定性
- api接口一旦发布,尽量保持向后兼容
- biz内部可以重构、优化,只要api不变即可
- 类似于Java的interface和implements的关系
3. 支持RPC调用
java
// 单体应用:直接注入
@Autowired
private OrderApi orderApi; // Spring注入本地实现类
// 微服务:通过Feign远程调用
@FeignClient(name = "order-service")
public interface OrderApi {
// 接口定义完全一致
}
4. 版本管理清晰
- api版本升级影响面可控
- 可以同时维护多个api版本
- 平滑过渡,逐步迁移
对比总结:
| 维度 | 不分离(单一模块) | api/biz分离 |
|---|---|---|
| 依赖复杂度 | 依赖整个模块,臃肿 | 只依赖轻量api,简洁 |
| 循环依赖 | 容易出现 | 几乎不会出现 |
| 启动冲突 | Bean重复定义 | 不会冲突 |
| 接口稳定性 | 实现变动影响调用方 | api稳定,biz可任意调整 |
| 远程调用 | 不支持 | 天然支持Feign/Dubbo |
1.3 父子POM继承与版本管理
Maven依赖管理的痛点
在企业级项目中,通常会有几十个甚至上百个依赖,如果不统一管理会出现:
1. 版本冲突问题
xml
<!-- 订单模块 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version> <!-- 使用1.2.70版本 -->
</dependency>
<!-- 支付模块 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version> <!-- 使用1.2.83版本 -->
</dependency>
后果:最终打包时Maven会选择其中一个版本,可能导致某个模块运行异常。
2. 升级困难
- 要升级Spring Boot版本,需要修改所有子模块的pom.xml
- 容易遗漏某个模块,导致版本不一致
3. 重复定义
- 每个模块都要写相同的依赖配置
- 代码冗余,维护成本高
Maven依赖管理机制
Maven提供了两个核心机制解决版本管理问题:
1. 继承(Inheritance)
- 子POM通过
<parent>继承父POM的配置 - 子POM自动获得父POM的所有配置(dependencyManagement、properties等)
2. 依赖管理(Dependency Management)
- 父POM通过
<dependencyManagement>声明依赖版本 - 子POM引用依赖时无需指定版本号,自动使用父POM声明的版本
父POM完整配置详解
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<!-- ==================== 基本信息 ==================== -->
<groupId>com.company</groupId>
<artifactId>demo-platform</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging> <!-- 父POM必须是pom类型 -->
<name>Demo Platform</name>
<!-- ==================== 聚合子模块 ==================== -->
<modules>
<module>demo-framework</module>
<module>demo-module-system</module>
<module>demo-module-order</module>
<module>demo-module-payment</module>
<module>demo-server</module>
</modules>
<!-- ==================== 统一版本管理 ==================== -->
<properties>
<!-- 核心版本 -->
<revision>1.0.0</revision> <!-- 使用变量统一内部模块版本 -->
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Spring Boot版本 -->
<spring-boot.version>2.7.18</spring-boot.version>
<!-- 数据库相关 -->
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<druid.version>1.2.20</druid.version>
<mysql.version>8.0.33</mysql.version>
<!-- 缓存相关 -->
<redisson.version>3.25.2</redisson.version>
<!-- 工具类 -->
<hutool.version>5.8.23</hutool.version>
<guava.version>32.1.3-jre</guava.version>
<lombok.version>1.18.30</lombok.version>
<!-- 文件处理 -->
<easyexcel.version>3.3.3</easyexcel.version>
<poi.version>5.2.5</poi.version>
<!-- 其他 -->
<swagger.version>2.2.19</swagger.version>
<transmittable.version>2.14.5</transmittable.version>
</properties>
<!-- ==================== 依赖版本管理(核心) ==================== -->
<dependencyManagement>
<dependencies>
<!-- ========== Spring Boot BOM(物料清单) ========== -->
<!--
type=pom: 这是一个聚合POM,包含所有Spring Boot相关依赖的版本
scope=import: 导入BOM中定义的依赖版本
作用:一次性引入Spring Boot全家桶的版本管理
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- ========== 数据库相关 ========== -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- ========== Redis相关 ========== -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- ========== 工具类 ========== -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- ========== 内部模块版本管理 ========== -->
<!-- framework模块 -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-spring-boot-starter-mybatis</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-spring-boot-starter-redis</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务模块api -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-order-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-payment-api</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- ==================== 构建配置 ==================== -->
<build>
<plugins>
<!-- Maven编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<!-- Flatten插件:处理${revision}变量 -->
<!--
问题:Maven install时,${revision}变量不会被替换
解决:使用flatten插件,生成.flattened-pom.xml,将变量替换为实际值
-->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.5.0</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
关键配置说明
1. <packaging>pom</packaging>
xml
<packaging>pom</packaging>
- 作用:声明这是一个聚合工程,不会产生jar包
- 必要性:父POM必须是pom类型,否则Maven会报错
- 对比 :子模块使用
<packaging>jar</packaging>
2. <modules>:聚合模块
xml
<modules>
<module>demo-framework</module>
<module>demo-module-order</module>
</modules>
- 作用:聚合所有子模块,一键构建
- 效果 :执行
mvn clean install时,按依赖顺序依次构建各模块 - 顺序:Maven自动分析依赖关系,确定构建顺序
3. <properties>:版本集中管理
xml
<properties>
<revision>1.0.0</revision>
<spring-boot.version>2.7.18</spring-boot.version>
</properties>
- 好处:只需修改一处,所有模块生效
- 命名规范 :使用
xxx.version格式 - ${revision}变量:用于统一内部模块版本,需配合flatten插件使用
4. <dependencyManagement>:依赖版本锁定
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
核心原理:
dependencyManagement只声明版本,不会实际引入依赖- 子模块引用时不需要指定版本号,自动使用父POM声明的版本
- 如果子模块显式指定版本,会覆盖父POM的版本
对比:
| 配置项 | <dependencies> |
<dependencyManagement> |
|---|---|---|
| 作用域 | 当前POM和所有子POM | 仅声明版本,不实际引入 |
| 是否引入依赖 | 是,会实际引入jar包 | 否,只声明版本 |
| 子模块是否继承 | 是,子模块会自动获得依赖 | 否,子模块需主动引用 |
| 使用场景 | 所有子模块都需要的公共依赖 | 版本管理,避免版本冲突 |
子模块POM配置
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<!-- ==================== 继承父POM ==================== -->
<parent>
<groupId>com.company</groupId>
<artifactId>demo-platform</artifactId>
<version>1.0.0</version>
<relativePath>../../pom.xml</relativePath> <!-- 指定父POM路径 -->
</parent>
<!-- ==================== 当前模块信息 ==================== -->
<!-- groupId和version继承自父POM,无需重复定义 -->
<artifactId>demo-module-order-biz</artifactId>
<packaging>jar</packaging>
<name>Demo Order Business Module</name>
<!-- ==================== 依赖声明 ==================== -->
<dependencies>
<!-- ========== 依赖自己的api模块 ========== -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-order-api</artifactId>
<!-- 版本号继承自父POM的dependencyManagement,无需指定 -->
</dependency>
<!-- ========== 依赖其他模块api ========== -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-payment-api</artifactId>
<!-- 版本号继承自父POM -->
</dependency>
<!-- ========== 依赖framework ========== -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-spring-boot-starter-mybatis</artifactId>
<!-- 版本号继承自父POM -->
</dependency>
<!-- ========== Spring Boot依赖 ========== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 版本号来自父POM导入的spring-boot-dependencies -->
</dependency>
<!-- ========== 数据库依赖 ========== -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<!-- 版本号继承自父POM -->
</dependency>
<!-- ========== 工具类 ========== -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<!-- 版本号继承自父POM -->
</dependency>
<!-- ========== Lombok(编译期依赖) ========== -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope> <!-- 只在编译期有效,运行时不需要 -->
</dependency>
</dependencies>
</project>
版本管理最佳实践
1. 使用${revision}统一内部模块版本
xml
<!-- 父POM -->
<properties>
<revision>1.0.0</revision>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-order-api</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
好处:
- 升级版本时只需修改一处
- 避免内部模块版本不一致
- 配合flatten插件,发布时自动替换变量
2. 使用BOM(Bill of Materials)管理第三方依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
原理:
- Spring Boot提供了一个BOM,包含所有组件的版本号
- 使用
scope=import导入BOM中的版本管理 - 子模块引用Spring Boot组件时无需指定版本
3. 使用<optional>true</optional>避免依赖传递
xml
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-third-api</artifactId>
<optional>true</optional> <!-- 不传递给依赖方 -->
</dependency>
场景:
- A模块依赖B模块(optional=true)
- C模块依赖A模块
- 结果:C模块不会自动获得B模块的依赖
4. 使用<exclusions>排除冲突依赖
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
场景:
- EasyExcel内部依赖slf4j 1.7.x
- 项目已经统一使用slf4j 2.0.x
- 解决:排除EasyExcel传递的slf4j,使用项目统一版本
依赖冲突解决原则
Maven使用最短路径优先 和最先声明优先原则解决版本冲突:
1. 最短路径优先
项目 -> A 1.0 -> C 2.0 (路径长度=2)
项目 -> B 1.0 -> D 1.0 -> C 1.0 (路径长度=3)
结果:使用C 2.0(路径更短)
2. 最先声明优先
项目 -> A 1.0 -> C 2.0 (路径长度=2)
项目 -> B 1.0 -> C 1.0 (路径长度=2)
结果:使用C 2.0(A先声明)
解决方案:
- 查看依赖树 :
mvn dependency:tree - 显式声明版本:在dependencyManagement中锁定版本
- 排除冲突依赖 :使用
<exclusions>排除不需要的版本
1.4 模块间依赖关系梳理
依赖层级与调用链路
在多模块架构中,理清模块间的依赖关系至关重要,否则容易陷入循环依赖的泥潭。
完整的依赖层级图:
┌─────────────────────────────────────────────────────────┐
│ demo-server (启动层) │
│ 职责:应用启动、整合所有模块、打包部署 │
│ 依赖:所有biz模块 │
└─────────────────────────────────────────────────────────┘
↓ 依赖
┌──────────────────┬──────────────────┬──────────────────┐
│ order-biz │ payment-biz │ system-biz │ (业务实现层)
│ 订单业务实现 │ 支付业务实现 │ 系统业务实现 │
└──────────────────┴──────────────────┴──────────────────┘
↓ 依赖 ↓ 依赖 ↓ 依赖
┌──────────────────┬──────────────────┬──────────────────┐
│ order-api │ payment-api │ system-api │ (API接口层)
│ 订单接口定义 │ 支付接口定义 │ 系统接口定义 │
└──────────────────┴──────────────────┴──────────────────┘
↓ 依赖
┌─────────────────────────────────────────────────────────┐
│ demo-framework (框架层) │
│ mybatis-starter, redis-starter, web-starter │
│ 职责:技术组件封装、自动装配 │
└─────────────────────────────────────────────────────────┘
↓ 依赖
┌─────────────────────────────────────────────────────────┐
│ Spring Boot + 第三方依赖 │
│ MyBatis-Plus、Redisson、Hutool等 │
└─────────────────────────────────────────────────────────┘
模块依赖的三大原则
原则一:严禁循环依赖
循环依赖会导致Maven构建失败,出现类似错误:
[ERROR] The projects in the reactor contain a cyclic reference
常见循环依赖场景:
❌ 错误示例:
order-biz 依赖 payment-api → payment-biz 依赖 order-api → 形成环
解决方案:
- 单向依赖:A依赖B,B不能依赖A
- 抽取公共api:如果两个模块互相需要,抽取公共接口到独立的api模块
- 事件驱动:通过消息队列解耦,避免直接依赖
xml
<!-- ✅ 正确做法:订单模块只依赖支付api -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-payment-api</artifactId>
</dependency>
<!-- ❌ 绝不能再依赖支付的biz -->
<!-- <dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-payment-biz</artifactId>
</dependency> -->
原则二:最小化依赖
模块应该只依赖必要的内容,避免引入过多无关的jar包。
api模块依赖配置示例:
xml
<!-- demo-module-order-api/pom.xml -->
<dependencies>
<!-- ✅ 只需要基础工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- ✅ 需要序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<!-- ❌ 不应该依赖实现相关的组件 -->
<!-- <dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency> -->
<!-- ❌ 不应该依赖Spring Web -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> -->
</dependencies>
为什么api模块要保持轻量?
- 减少依赖传递:其他模块依赖api时,不会带入不必要的jar包
- 降低冲突概率:依赖越少,版本冲突概率越低
- 提升构建速度:依赖少,Maven构建更快
- 明确职责边界:api就是接口定义,不应该有业务实现
原则三:控制依赖传递
Maven的依赖是传递性的:
A依赖B,B依赖C → A自动获得C的依赖(传递依赖)
传递依赖的问题:
- 引入不需要的jar包,增大应用体积
- 可能产生版本冲突
- 依赖关系复杂,难以排查问题
解决方案:
1. 使用<optional>true</optional>
xml
<!-- framework模块可能依赖某些可选组件 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<optional>true</optional> <!-- 标记为可选依赖 -->
</dependency>
效果:
- 业务模块依赖framework时,不会自动获得caffeine依赖
- 如果业务模块需要使用caffeine,需要显式声明依赖
2. 使用<exclusions>排除冲突
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.3</version>
<exclusions>
<!-- 排除EasyExcel自带的slf4j,使用项目统一版本 -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<!-- 排除旧版本的POI -->
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 显式声明统一版本 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.5</version>
</dependency>
实战案例:订单业务依赖支付API
需求场景: 订单创建后,需要调用支付模块创建支付单。
依赖配置:
xml
<!-- demo-module-order-biz/pom.xml -->
<dependencies>
<!-- 1. 依赖自己的api -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-order-api</artifactId>
</dependency>
<!-- 2. 依赖支付模块api(跨模块调用) -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-module-payment-api</artifactId>
</dependency>
<!-- 3. 依赖框架starter -->
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-spring-boot-starter-redis</artifactId>
</dependency>
<!-- 4. Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
业务代码:
java
package com.company.module.order.biz.service.impl;
import com.company.module.order.api.dto.*;
import com.company.module.payment.api.PaymentApi;
import com.company.module.payment.api.dto.PaymentCreateReqDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
// 注入支付模块的API(通过依赖支付api实现跨模块调用)
private final PaymentApi paymentApi;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createOrder(OrderCreateReqVO reqVO) {
// 1. 创建订单
OrderDO order = buildOrder(reqVO);
orderMapper.insert(order);
log.info("[创建订单] 订单号={}, 金额={}", order.getOrderNo(), order.getAmount());
// 2. 调用支付模块创建支付单(跨模块调用)
PaymentCreateReqDTO paymentReq = new PaymentCreateReqDTO();
paymentReq.setOrderId(order.getId());
paymentReq.setOrderNo(order.getOrderNo());
paymentReq.setAmount(order.getAmount());
Long paymentId = paymentApi.createPayment(paymentReq);
log.info("[创建支付单] 支付ID={}", paymentId);
return order.getId();
}
}
调用链路分析:
- Controller调用Service:同模块内部调用
- OrderService调用PaymentApi:跨模块调用(通过依赖payment-api)
- Spring容器自动装配:PaymentApiImpl作为PaymentApi的实现类被注入
- 最终调用PaymentService:PaymentApiImpl内部调用PaymentService
1.5 starter自动装配机制
什么是Spring Boot Starter?
Spring Boot Starter是开箱即用的依赖包 ,遵循约定优于配置的理念。
传统Spring配置的痛点:
xml
<!-- 需要手动配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/db"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!-- 需要手动配置SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 需要手动配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
Spring Boot Starter的优势:
xml
<!-- 只需引入starter依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- 配置文件中简单配置 -->
<!-- application.yml -->
spring:
datasource:
url: jdbc:mysql://localhost:3306/db
username: root
password: 123456
自动装配的魔法:
- Spring Boot启动时自动扫描jar包中的
META-INF/spring.factories - 加载配置类,自动创建Bean
- 开发者只需引入依赖,无需手动配置
自定义Starter结构设计
以MyBatis-Plus Starter为例,展示自定义starter的标准结构:
demo-spring-boot-starter-mybatis/
├── pom.xml
└── src/main/
├── java/com/company/framework/mybatis/
│ ├── config/
│ │ └── MybatisAutoConfiguration.java # 自动配置类(核心)
│ ├── core/
│ │ ├── TableYearContext.java # 分表上下文(ThreadLocal)
│ │ └── TableYearInterceptor.java # 分表拦截器
│ └── properties/
│ └── MybatisProperties.java # 配置属性类
└── resources/
└── META-INF/
└── spring.factories # SPI配置文件(关键)
核心组件详解
1. 自动配置类(AutoConfiguration)
java
package com.company.framework.mybatis.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* MyBatis-Plus自动配置类
*
* <p>关键注解说明:
* - @AutoConfiguration: 标记为自动配置类(Spring Boot 2.7+新注解,替代@Configuration)
* - @ConditionalOnClass: 当classpath存在指定类时才加载(判断依赖是否存在)
* - @ConditionalOnMissingBean: 当Spring容器中不存在指定Bean时才创建(避免重复)
* - @EnableConfigurationProperties: 启用配置属性类
*/
@Slf4j
@AutoConfiguration // Spring Boot 2.7+新注解
@ConditionalOnClass(MybatisPlusInterceptor.class) // 只有引入了MyBatis-Plus依赖才加载
@EnableConfigurationProperties(MybatisProperties.class) // 启用配置属性绑定
public class MybatisAutoConfiguration {
/**
* 配置MyBatis-Plus拦截器
*
* <p>核心拦截器包括:
* 1. 动态表名拦截器(支持按年分表)
* 2. 分页拦截器(自动分页)
*/
@Bean
@ConditionalOnMissingBean // 如果用户自定义了拦截器,就不创建默认的
public MybatisPlusInterceptor mybatisPlusInterceptor(MybatisProperties properties) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 动态表名拦截器(必须放第一个)
if (properties.getDynamicTable().getEnabled()) {
interceptor.addInnerInterceptor(dynamicTableNameInterceptor());
log.info("[MyBatis-Plus] 动态表名拦截器已加载");
}
// 2. 分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
log.info("[MyBatis-Plus] 分页拦截器已加载");
return interceptor;
}
/**
* 动态表名拦截器
* 实现按年份分表:backend_order_2024, backend_order_2025
*/
@Bean
@ConditionalOnProperty(prefix = "mybatis-plus.dynamic-table", name = "enabled",
havingValue = "true", matchIfMissing = true)
public DynamicTableNameInnerInterceptor dynamicTableNameInterceptor() {
DynamicTableNameInnerInterceptor interceptor = new DynamicTableNameInnerInterceptor();
// 设置动态表名处理器(核心逻辑)
interceptor.setTableNameHandler((sql, tableName) -> {
// 从ThreadLocal获取年份
Integer year = TableYearContext.getYear();
// 判断是否需要分表(这里简化处理,实际应该配置化)
if (needPartition(tableName)) {
// 如果没有设置年份,默认使用当前年份
int targetYear = (year != null) ? year : LocalDate.now().getYear();
String newTableName = tableName + "_" + targetYear;
log.debug("[动态表名] {} -> {}", tableName, newTableName);
return newTableName;
}
return tableName; // 不需要分表,返回原表名
});
return interceptor;
}
/**
* 判断表是否需要分表
* 实际项目中应该通过配置文件配置需要分表的表名
*/
private boolean needPartition(String tableName) {
// 示例:订单表、支付表需要分表
return tableName.equals("backend_order")
|| tableName.equals("backend_payment")
|| tableName.equals("backend_bill");
}
}
关键注解详解:
| 注解 | 作用 | 场景 |
|---|---|---|
@AutoConfiguration |
标记自动配置类 | 替代@Configuration |
@ConditionalOnClass |
类存在时才加载 | 判断依赖是否引入 |
@ConditionalOnMissingBean |
Bean不存在时才创建 | 允许用户自定义覆盖 |
@ConditionalOnProperty |
配置存在且匹配时才加载 | 根据配置开关功能 |
@EnableConfigurationProperties |
启用配置属性绑定 | 读取application.yml配置 |
2. 配置属性类(Properties)
java
package com.company.framework.mybatis.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* MyBatis配置属性
*
* <p>绑定application.yml中的配置:
* mybatis-plus:
* dynamic-table:
* enabled: true
*/
@Data
@ConfigurationProperties(prefix = "mybatis-plus")
public class MybatisProperties {
/** 动态表名配置 */
private DynamicTableConfig dynamicTable = new DynamicTableConfig();
@Data
public static class DynamicTableConfig {
/** 是否启用动态表名 */
private Boolean enabled = true;
/** 需要分表的表名列表 */
private List<String> tables = Arrays.asList("backend_order", "backend_payment");
}
}
3. ThreadLocal分表上下文
java
package com.company.framework.mybatis.core;
import lombok.extern.slf4j.Slf4j;
import java.util.function.Supplier;
/**
* 分表年份上下文
*
* <p>核心原理:
* 1. 使用ThreadLocal存储当前线程的年份信息
* 2. 在Service方法中设置年份,Mapper执行时自动读取
* 3. finally块中清除年份,防止内存泄漏
*/
@Slf4j
public class TableYearContext {
/** 线程变量:存储年份 */
private static final ThreadLocal<Integer> YEAR_CONTEXT = new ThreadLocal<>();
/**
* 设置年份
*
* @param year 年份(如2024)
*/
public static void setYear(Integer year) {
YEAR_CONTEXT.set(year);
log.debug("[TableYearContext] 设置年份: {}", year);
}
/**
* 获取年份
*
* @return 年份,未设置则返回null
*/
public static Integer getYear() {
return YEAR_CONTEXT.get();
}
/**
* 清除年份
* 重要:防止ThreadLocal内存泄漏
*/
public static void clear() {
YEAR_CONTEXT.remove();
log.debug("[TableYearContext] 清除年份");
}
/**
* 在指定年份中执行查询操作(有返回值)
*
* <p>使用示例:
* OrderDO order = TableYearContext.callInYear(2024, () -> {
* return orderMapper.selectById(id);
* });
*
* @param year 年份
* @param supplier 查询逻辑
* @return 查询结果
*/
public static <T> T callInYear(Integer year, Supplier<T> supplier) {
try {
setYear(year); // 设置年份
return supplier.get(); // 执行查询
} finally {
clear(); // 清除年份(重要!)
}
}
/**
* 在指定年份中执行更新操作(无返回值)
*
* <p>使用示例:
* TableYearContext.runInYear(2024, () -> {
* orderMapper.updateById(order);
* });
*
* @param year 年份
* @param runnable 更新逻辑
*/
public static void runInYear(Integer year, Runnable runnable) {
try {
setYear(year); // 设置年份
runnable.run(); // 执行更新
} finally {
clear(); // 清除年份(重要!)
}
}
}
ThreadLocal原理说明:
java
// 线程A执行
TableYearContext.setYear(2024);
// ThreadLocal内部:Thread-A -> 2024
// 线程B执行
TableYearContext.setYear(2025);
// ThreadLocal内部:Thread-A -> 2024, Thread-B -> 2025
// 每个线程都有自己独立的年份值,互不干扰
为什么要在finally中clear()?
- 防止内存泄漏:ThreadLocal变量不清除,线程池复用线程时会残留旧值
- 避免数据错乱:下次使用该线程时,可能读取到上次的年份
- 最佳实践:所有ThreadLocal使用都应该在finally中清除
4. SPI配置文件(spring.factories)
properties
# demo-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories
# 自动配置类(Spring Boot会自动扫描并加载)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.company.framework.mybatis.config.MybatisAutoConfiguration
spring.factories详解:
- 位置 :必须在
META-INF/spring.factories - 格式 :
接口全限定名=实现类全限定名1,实现类全限定名2 - 换行 :使用
\换行,等号后不能有空格 - 原理 :Spring Boot启动时通过
SpringFactoriesLoader加载所有jar包的spring.factories
常见错误:
properties
# ❌ 等号后有空格(错误)
org.springframework.boot.autoconfigure.EnableAutoConfiguration= \
com.company.framework.mybatis.config.MybatisAutoConfiguration
# ✅ 等号后无空格(正确)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.company.framework.mybatis.config.MybatisAutoConfiguration
Starter使用示例
业务模块只需引入依赖,无需任何配置:
1. pom.xml引入依赖
xml
<dependency>
<groupId>com.company</groupId>
<artifactId>demo-spring-boot-starter-mybatis</artifactId>
</dependency>
2. application.yml配置(可选)
yaml
mybatis-plus:
dynamic-table:
enabled: true # 启用动态表名
tables: # 需要分表的表
- backend_order
- backend_payment
3. 业务代码直接使用
java
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
/**
* 查询2024年的订单
* SQL自动路由到:backend_order_2024
*/
public OrderDO getOrderIn2024(Long id) {
return TableYearContext.callInYear(2024, () -> {
return orderMapper.selectById(id);
});
}
/**
* 更新2025年的订单
* SQL自动路由到:backend_order_2025
*/
public void updateOrderIn2025(OrderDO order) {
TableYearContext.runInYear(2025, () -> {
orderMapper.updateById(order);
});
}
/**
* 跨年查询:查询2024和2025两年的订单
*/
public List<OrderDO> getOrdersCrossYear(Long userId) {
List<OrderDO> result = new ArrayList<>();
// 查询2024年
List<OrderDO> orders2024 = TableYearContext.callInYear(2024, () -> {
return orderMapper.selectList(new LambdaQueryWrapper<OrderDO>()
.eq(OrderDO::getUserId, userId));
});
result.addAll(orders2024);
// 查询2025年
List<OrderDO> orders2025 = TableYearContext.callInYear(2025, () -> {
return orderMapper.selectList(new LambdaQueryWrapper<OrderDO>()
.eq(OrderDO::getUserId, userId));
});
result.addAll(orders2025);
return result;
}
}
自动装配流程总结
1. Spring Boot启动
↓
2. SpringFactoriesLoader扫描所有jar包的META-INF/spring.factories
↓
3. 加载EnableAutoConfiguration对应的配置类
↓
4. 根据@Conditional条件判断是否加载
↓
5. 创建Bean并注入Spring容器
↓
6. 业务代码通过@Autowired使用
核心优势:
- 开箱即用:引入依赖即可使用,无需手动配置
- 约定优于配置:提供合理的默认配置
- 可覆盖:用户可以通过配置或自定义Bean覆盖默认行为
- 模块化:技术组件独立封装,业务模块只需关注业务逻辑
总结
ruoyi-vue-pro框架通过Maven多模块 + api/biz分离 + 自动装配构建了一套完整的企业级开发体系:
核心架构设计
| 层次 | 职责 | 关键技术 | 核心价值 |
|---|---|---|---|
| 框架层 | 技术组件封装 | Spring Boot AutoConfiguration | 开箱即用、统一技术栈 |
| API层 | 接口契约定义 | DTO、枚举、常量 | 依赖解耦、接口稳定 |
| BIZ层 | 业务逻辑实现 | Controller/Service/Mapper | 职责清晰、易于维护 |
| 启动层 | 应用启动入口 | Spring Boot Application | 一键启动、整合模块 |
关键技术要点
1. Maven多模块管理
<packaging>pom</packaging>:父POM必须是pom类型<dependencyManagement>:统一版本管理,子模块无需指定版本${revision}变量:统一内部模块版本号flatten-maven-plugin:处理变量占位符
2. api/biz分离设计
- api模块:只定义接口、DTO、枚举,不包含实现
- biz模块:完整的MVC三层架构,实现业务逻辑
- 跨模块调用:通过依赖api实现,避免循环依赖
- RPC友好:api接口天然支持Feign/Dubbo远程调用
3. 依赖管理原则
- 严禁循环依赖:A依赖B,B不能依赖A
- 最小化依赖:api模块保持轻量,只引入必要依赖
- 控制传递 :使用
<optional>和<exclusions>控制依赖传递
4. 自动装配机制
META-INF/spring.factories:SPI配置文件@AutoConfiguration:标记自动配置类@ConditionalOnXxx:条件装配,按需加载@EnableConfigurationProperties:绑定配置文件
实战应用场景
这套架构在企业级系统中广泛应用:
- 电商系统:订单模块、支付模块、库存模块、商品模块
- 金融系统:账户模块、交易模块、风控模块、清算模块
- SaaS平台:租户模块、权限模块、审计模块、报表模块
通过模块化设计,实现了高内聚、低耦合、可复用、易维护的目标,为企业级应用开发奠定了坚实基础。
面试题精选
Q1: 为什么要使用Maven多模块结构?相比单体应用有什么优势?
参考答案:
单体应用的痛点:
- 编译慢:修改一个类需要重新编译整个项目,大型项目编译可能需要几分钟
- 职责不清:业务代码、框架代码、工具类混在一起,难以维护
- 无法复用:其他项目想用某个功能,只能复制代码
- 团队协作难:多人修改同一个工程,代码冲突频繁
- 部署耦合:任何小改动都需要重新部署整个应用
多模块结构的优势:
- 模块隔离:按业务域拆分模块,职责清晰,降低耦合
- 编译提速:只编译修改的模块,大幅提升编译速度
- 代码复用:通过Maven依赖引入,避免代码重复
- 团队协作:不同团队负责不同模块,减少代码冲突
- 持续集成:只部署变更模块,加快部署速度
- 版本管理:统一管理依赖版本,避免版本冲突
实际案例:
某电商系统有订单、支付、库存、商品四个核心模块。使用多模块后:
- 订单团队修改订单模块,只需编译订单模块(10秒),无需编译整个项目(5分钟)
- 库存模块升级,只部署库存服务,不影响订单、支付等模块
- 新项目需要支付功能,直接引入支付模块依赖即可
Q2: api/biz分离的核心原理是什么?为什么能避免循环依赖?
参考答案:
核心原理:接口与实现分离
api模块定义接口契约(interface),biz模块提供具体实现(implementation)。这是经典的**依赖倒置原则(DIP)**的应用。
依赖关系:
订单biz ---依赖---> 支付api (只依赖接口)
↑
实现关系
↑
支付biz
为什么能避免循环依赖?
- 单向依赖:订单biz只依赖支付api,不依赖支付biz
- 实现隔离:支付biz的任何变化,只要api接口不变,订单biz都不受影响
- 接口稳定:api接口一旦发布,保持向后兼容,biz内部可任意重构
反例(会产生循环依赖):
订单biz ---依赖---> 支付biz
↑ ↓
└────────依赖────────┘
Maven构建失败:cyclic reference detected
其他好处:
- 轻量依赖:api模块只包含DTO、枚举,体积小,依赖少
- RPC友好:微服务架构下,api可以作为Feign Client定义
- 版本管理:api版本独立升级,平滑过渡
Q3: <dependencies>和<dependencyManagement>有什么区别?什么时候用哪个?
参考答案:
核心区别:
| 维度 | <dependencies> |
<dependencyManagement> |
|---|---|---|
| 是否实际引入依赖 | 是,会下载jar包到classpath | 否,只声明版本号 |
| 子模块是否继承 | 是,子模块自动获得依赖 | 否,子模块需主动引用 |
| 是否需要指定版本 | 是 | 是(在父POM中) |
| 作用域 | 当前POM和所有子POM | 仅声明版本,不实际引入 |
使用场景:
1. <dependencies>:所有子模块都需要的公共依赖
xml
<!-- 父POM -->
<dependencies>
<!-- 所有子模块都需要Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
- 效果:所有子模块自动获得Lombok依赖,无需重复声明
2. <dependencyManagement>:统一版本管理
xml
<!-- 父POM -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 子POM -->
<dependencies>
<!-- 只需声明groupId和artifactId,版本继承自父POM -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
</dependencies>
- 效果:版本统一管理,子模块无需指定版本号
最佳实践:
- 父POM的
<dependencies>:只放所有子模块都需要的依赖(如Lombok、SLF4J) - 父POM的
<dependencyManagement>:声明所有可能用到的依赖版本 - 子POM的
<dependencies>:引用需要的依赖,无需指定版本
Q4: Spring Boot的自动装配原理是什么?@AutoConfiguration和spring.factories是如何工作的?
参考答案:
自动装配流程:
1. Spring Boot应用启动
@SpringBootApplication
└─ @EnableAutoConfiguration(核心注解)
↓
2. AutoConfigurationImportSelector生效
扫描classpath下所有jar包的META-INF/spring.factories
↓
3. 加载spring.factories中的配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.company.framework.mybatis.config.MybatisAutoConfiguration
↓
4. 根据@Conditional条件判断是否加载
@ConditionalOnClass:类存在时才加载
@ConditionalOnMissingBean:Bean不存在时才创建
@ConditionalOnProperty:配置匹配时才加载
↓
5. 创建Bean并注入Spring容器
配置类中@Bean方法返回的对象注册到容器
↓
6. 业务代码通过@Autowired使用
关键组件详解:
1. spring.factories
properties
# 位置:META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.company.framework.mybatis.config.MybatisAutoConfiguration,\
com.company.framework.redis.config.RedisAutoConfiguration
- 作用:SPI(Service Provider Interface)机制,声明自动配置类
- 加载时机 :Spring Boot启动时通过
SpringFactoriesLoader加载 - 注意事项 :等号后不能有空格,使用
\换行
2. @AutoConfiguration
java
@AutoConfiguration
@ConditionalOnClass(MybatisPlusInterceptor.class)
public class MybatisAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建拦截器
}
}
- 作用:标记自动配置类(Spring Boot 2.7+新注解,替代@Configuration)
- 条件装配:配合@Conditional注解,按需加载
3. 常用Conditional注解
| 注解 | 作用 | 示例 |
|---|---|---|
@ConditionalOnClass |
classpath存在指定类时加载 | @ConditionalOnClass(SqlSessionFactory.class) |
@ConditionalOnMissingBean |
容器中不存在指定Bean时创建 | @ConditionalOnMissingBean(DataSource.class) |
@ConditionalOnProperty |
配置存在且匹配时加载 | @ConditionalOnProperty(prefix="mybatis", name="enabled") |
@ConditionalOnBean |
容器中存在指定Bean时加载 | @ConditionalOnBean(DataSource.class) |
为什么这样设计?
- 开箱即用:引入starter依赖即可使用,无需手动配置
- 约定优于配置:提供合理的默认配置,减少配置工作量
- 可覆盖:用户可以通过自定义Bean或配置覆盖默认行为
- 按需加载:通过条件判断,只加载需要的组件
Q5: ThreadLocal在分表场景中是如何使用的?为什么要在finally中清除?
参考答案:
ThreadLocal原理:
ThreadLocal为每个线程提供独立的变量副本,不同线程之间互不干扰。
java
// 线程A执行
TableYearContext.setYear(2024); // Thread-A -> 2024
// 线程B执行
TableYearContext.setYear(2025); // Thread-B -> 2025
// 每个线程都有自己独立的年份值
在分表场景中的应用:
java
public class TableYearContext {
private static final ThreadLocal<Integer> YEAR_CONTEXT = new ThreadLocal<>();
public static <T> T callInYear(Integer year, Supplier<T> supplier) {
try {
YEAR_CONTEXT.set(year); // 设置年份
return supplier.get(); // 执行查询(Mapper会读取这个年份)
} finally {
YEAR_CONTEXT.remove(); // 清除年份(重要!)
}
}
}
调用链路:
1. Service调用:callInYear(2024, () -> mapper.selectById(id))
↓
2. 设置ThreadLocal:YEAR_CONTEXT.set(2024)
↓
3. 执行Mapper:selectById(id)
↓
4. MyBatis拦截器:读取YEAR_CONTEXT.get() = 2024
↓
5. 动态替换表名:backend_order -> backend_order_2024
↓
6. finally清除:YEAR_CONTEXT.remove()
为什么必须在finally中清除?
1. 防止内存泄漏
java
// 假设没有清除
public void method1() {
TableYearContext.setYear(2024);
// 执行查询...
// 忘记清除
}
// 线程池复用线程
public void method2() {
// 这个方法没有设置年份,但ThreadLocal还残留2024
Integer year = TableYearContext.getYear(); // 返回2024(错误!)
}
问题:
- Tomcat线程池默认200个线程,线程复用
- ThreadLocal变量没有清除,会一直存在于线程中
- 下次使用该线程时,读取到上次的旧值
2. 避免数据错乱
java
// 场景:处理2024年订单
TableYearContext.setYear(2024);
orderMapper.selectById(1); // 查询 backend_order_2024
// 线程池复用,处理默认年份订单(期望当前年份2025)
// 但ThreadLocal残留2024,导致查询了错误的表
orderMapper.selectById(2); // 错误地查询了 backend_order_2024
3. ThreadLocal内存泄漏原理
Thread -> ThreadLocalMap -> Entry(ThreadLocal, Value)
↓
弱引用ThreadLocal,强引用Value
- Entry的Key:弱引用ThreadLocal,GC时会被回收
- Entry的Value:强引用,只有手动remove()才会释放
- 泄漏原因:ThreadLocal对象被回收,但Value还在,且无法访问
正确使用模式:
java
public static <T> T callInYear(Integer year, Supplier<T> supplier) {
try {
setYear(year);
return supplier.get();
} finally {
clear(); // 无论是否异常,都要清除
}
}
最佳实践:
- 所有ThreadLocal使用都要在finally中清除
- 使用工具类封装:避免每次都手动set/remove
- 线程池场景尤其重要:线程复用会放大问题
- 可以用InheritableThreadLocal:父子线程传递,但仍需清除
Q6: Maven依赖冲突是如何解决的?如何排查依赖冲突问题?
参考答案:
Maven依赖冲突解决机制:
1. 最短路径优先(Nearest Definition)
项目 -> A 1.0 -> C 2.0 (路径长度=2)
项目 -> B 1.0 -> D 1.0 -> C 1.0 (路径长度=3)
结果:使用 C 2.0(路径更短)
2. 最先声明优先(First Declaration)
项目 -> A 1.0 -> C 2.0 (路径长度=2,先声明)
项目 -> B 1.0 -> C 1.0 (路径长度=2,后声明)
结果:使用 C 2.0(A先声明)
排查依赖冲突的方法:
方法1:查看依赖树
bash
# 查看完整依赖树
mvn dependency:tree
# 查看指定依赖的冲突
mvn dependency:tree -Dverbose -Dincludes=com.alibaba:fastjson
# 输出示例
[INFO] +- com.alibaba:easyexcel:jar:3.3.3:compile
[INFO] | +- com.alibaba:fastjson:jar:1.2.70:compile
[INFO] +- com.company:demo-common:jar:1.0.0:compile
[INFO] | \- com.alibaba:fastjson:jar:1.2.83:compile (version managed from 1.2.70)
方法2:使用IDEA的依赖分析
- 打开pom.xml
- 右键 -> Diagrams -> Show Dependencies
- 可视化展示依赖关系,红线表示冲突
方法3:Maven Helper插件
- IDEA安装插件:Maven Helper
- 打开pom.xml
- 点击底部"Dependency Analyzer"
- 红色表示冲突,右键可直接Exclude
解决依赖冲突的方法:
方法1:显式声明版本(推荐)
xml
<!-- 父POM的dependencyManagement中锁定版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
</dependencyManagement>
方法2:排除冲突依赖
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<exclusions>
<!-- 排除EasyExcel自带的fastjson -->
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 显式引入统一版本 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
方法3:调整依赖顺序
xml
<!-- 将期望的版本放在前面(利用最先声明优先原则) -->
<dependencies>
<!-- 先声明统一版本 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- 后声明其他依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
</dependencies>
常见冲突案例:
- SLF4J冲突:多个框架带不同版本的slf4j-api
- Jackson冲突:Spring Boot和其他库的Jackson版本不一致
- Netty冲突:Redisson、RocketMQ等都依赖Netty
- Guava冲突:版本跨度大,API不兼容
最佳实践:
- 使用BOM统一版本:引入Spring Boot Dependencies、Spring Cloud Dependencies
- 父POM锁定核心依赖版本:在dependencyManagement中声明
- 定期排查依赖冲突:CI/CD中增加依赖检查
- 升级依赖要谨慎:先在测试环境验证
Q7: 如何设计一个高质量的Starter?有哪些关键要素?
参考答案:
Starter设计的关键要素:
1. 清晰的目录结构
demo-spring-boot-starter-xxx/
├── pom.xml
└── src/main/
├── java/
│ └── com/company/framework/xxx/
│ ├── config/ # 配置包
│ │ └── XxxAutoConfiguration.java
│ ├── properties/ # 属性包
│ │ └── XxxProperties.java
│ ├── core/ # 核心功能包
│ └── support/ # 支持工具包
└── resources/
└── META-INF/
├── spring.factories # SPI配置(必须)
└── additional-spring-configuration-metadata.json # 配置元数据(可选)
2. 规范的命名约定
# 官方Starter
spring-boot-starter-{name}
例如:spring-boot-starter-web
# 第三方Starter
{name}-spring-boot-starter
例如:mybatis-spring-boot-starter
3. 完善的AutoConfiguration
java
@AutoConfiguration
@ConditionalOnClass(Xxx.class) // 判断依赖是否存在
@EnableConfigurationProperties(XxxProperties.class) // 绑定配置
public class XxxAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 允许用户自定义覆盖
public XxxService xxxService(XxxProperties properties) {
return new XxxServiceImpl(properties);
}
@Bean
@ConditionalOnProperty(prefix = "xxx", name = "enabled",
havingValue = "true", matchIfMissing = true)
public XxxInterceptor xxxInterceptor() {
return new XxxInterceptor();
}
}
4. 灵活的配置属性
java
@Data
@ConfigurationProperties(prefix = "xxx")
public class XxxProperties {
/** 是否启用(提供开关) */
private Boolean enabled = true;
/** 超时时间(提供默认值) */
private Duration timeout = Duration.ofSeconds(30);
/** 自定义配置(嵌套对象) */
private CustomConfig custom = new CustomConfig();
@Data
public static class CustomConfig {
private String host = "localhost";
private Integer port = 8080;
}
}
5. 智能的条件装配
java
// 根据类是否存在
@ConditionalOnClass(Redis.class)
// 根据Bean是否存在
@ConditionalOnBean(DataSource.class)
@ConditionalOnMissingBean(RedisTemplate.class)
// 根据配置是否存在
@ConditionalOnProperty(prefix = "xxx", name = "enabled")
// 根据表达式
@ConditionalOnExpression("${xxx.enabled:true} and ${xxx.mode} == 'advanced'")
// 组合条件
@ConditionalOnClass(Xxx.class)
@ConditionalOnProperty(prefix = "xxx", name = "enabled", havingValue = "true")
6. 配置元数据文件(提升IDE体验)
json
{
"groups": [
{
"name": "xxx",
"type": "com.company.framework.xxx.properties.XxxProperties",
"description": "XXX configuration properties."
}
],
"properties": [
{
"name": "xxx.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable XXX.",
"defaultValue": true
},
{
"name": "xxx.timeout",
"type": "java.time.Duration",
"description": "Timeout for XXX operations.",
"defaultValue": "30s"
}
],
"hints": [
{
"name": "xxx.mode",
"values": [
{"value": "simple"},
{"value": "advanced"}
]
}
]
}
效果: 在application.yml中编写配置时,IDE会提供智能提示和文档说明。
7. 完善的文档和示例
markdown
# XXX Starter使用文档
## 快速开始
### 1. 引入依赖
```xml
<dependency>
<groupId>com.company</groupId>
<artifactId>xxx-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
2. 配置参数
yaml
xxx:
enabled: true
timeout: 30s
custom:
host: localhost
port: 8080
3. 使用示例
java
@Autowired
private XxxService xxxService;
public void demo() {
xxxService.doSomething();
}
设计原则总结:
- 开箱即用:引入依赖即可使用,提供合理默认值
- 约定优于配置:大部分场景使用默认配置即可
- 可覆盖:允许用户自定义Bean或配置覆盖
- 按需加载:通过条件注解,只加载必要的组件
- 向后兼容:配置变更要考虑向后兼容性
- 文档完善:提供清晰的使用文档和示例代码
下一篇预告: 《Spring Boot企业级配置详解》- 深入解析多环境配置、属性绑定、配置加密等实战技巧。