Spring Boot 的便利不只来自自动配置。项目中的父 POM、Starter、DevTools、事务代理和数据源配置共同构成了一套完整的工程化体系。本文基于 Spring Boot 3.x,讲清这些能力如何使用,以及它们背后的工作原理。
一、为什么使用 spring-boot-starter-parent?
一个典型 Maven 项目会继承:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.x.x</version>
<relativePath/>
</parent>
它的主要价值不是引入业务依赖,而是提供统一的构建约定。
1. 管理依赖版本
Spring Boot 为 Spring Framework、Jackson、Logback、数据库驱动等常见组件维护了一套经过兼容性测试的版本组合。因此,引入受管理依赖时通常不用单独写版本:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
升级 Spring Boot 版本时,相关依赖也会按统一组合升级,可以减少版本冲突。
2. 提供插件与构建默认值
Parent 还提供 Maven 插件管理、Java 编译参数、资源过滤等默认配置,并配合 spring-boot-maven-plugin 生成可执行 JAR。团队仍可在自己的 POM 中覆盖需要调整的属性,但不建议无理由覆盖 Spring Framework 等核心版本。
3. Parent 与 BOM 的区别
有些公司项目必须继承自己的父 POM,而 Maven 一个项目只能直接继承一个 Parent。此时可以在 dependencyManagement 中导入 Spring Boot BOM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.x.x</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
二、Spring Boot Starter 的工作原理
Starter 可以理解为一个"依赖集合入口"。它通常没有大量业务代码,而是通过 Maven 传递依赖,把实现某项能力所需的组件一次性引入。
以 spring-boot-starter-web 为例,它会带入 Spring MVC、JSON 转换、日志以及默认内嵌 Tomcat 等依赖。开发者不需要逐个查找并协调版本。
但要注意:Starter 主要负责聚合依赖,自动配置负责创建 Bean。 两者配合后才形成"引入依赖即可使用"的体验。
Spring Boot 3 的自动配置候选通常登记在:
META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports
自动配置常使用:
-
@ConditionalOnClass:某个类存在时生效; -
@ConditionalOnMissingBean:用户没有自定义 Bean 时提供默认 Bean; -
@ConditionalOnProperty:配置满足条件时生效。
因此,Starter 让依赖到位,自动配置根据 classpath、已有 Bean 和配置属性决定具体创建什么。用户自定义同类型 Bean 时,默认配置通常会退让。
三、Spring Boot 如何实现热部署?
开发阶段可以引入 DevTools:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
当 classpath 中的文件发生变化时,DevTools 会触发应用快速重启。代码修改后必须先由 IDE 或构建工具编译,只有 .class 等 classpath 内容更新,DevTools 才能检测到。
双 ClassLoader 原理
DevTools 使用两个类加载器:
-
Base ClassLoader:加载基本不变的第三方依赖;
-
Restart ClassLoader:加载正在开发的项目代码。
重启时只丢弃并重建 Restart ClassLoader,第三方依赖无需重新加载,因此比完整冷启动更快。
四、如何在 Spring Boot 中使用事务管理?
项目引入 JDBC、MyBatis 或 JPA Starter 并配置数据源后,Spring Boot 通常会自动配置合适的 PlatformTransactionManager。业务方法上使用 @Transactional 即可声明事务:
@Service
public class TransferService {
private final AccountRepository accountRepository;
public TransferService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@Transactional(rollbackFor = Exception.class)
public void transfer(Long from, Long to, BigDecimal amount) {
accountRepository.decrease(from, amount);
accountRepository.increase(to, amount);
}
}
事务原理
Spring 通常为 Bean 创建 AOP 代理。外部调用事务方法时,代理在方法执行前通过事务管理器开启事务,成功后提交,异常时按回滚规则回滚,并在结束后释放连接等资源。
默认情况下:
-
传播行为是
REQUIRED; -
隔离级别使用数据库默认值;
-
RuntimeException和Error触发回滚; -
受检异常默认不回滚。
如果业务希望任何 Exception 都回滚,可以明确配置 rollbackFor = Exception.class。
常见传播行为
REQUIRED 是默认值:外部已有事务就加入,没有就新建,适合一个业务入口统一操作多个 Repository。REQUIRES_NEW 会挂起外部事务并创建独立事务,内外可以分别提交或回滚,但会额外占用数据库连接。NESTED 通常基于 JDBC Savepoint 实现局部回滚,并不等同于真正独立事务。
传播行为只有在调用经过 Spring 代理时才会生效。同一个对象内部直接调用带 REQUIRES_NEW 的方法,仍会绕过代理,无法创建预期的新事务。实际项目中通常把需要不同事务边界的方法拆到另一个 Service。
常见事务失效场景
-
同类方法自调用,实际是
this.method(),没有经过代理; -
异常被
catch后吞掉,代理感知不到异常; -
方法未通过 Spring Bean 调用;
-
使用错误的事务管理器;
-
数据库表或存储引擎本身不支持事务。
在默认代理模式下,建议把事务放在 Service 的 public 业务入口,并由其他 Bean 调用。不要在事务中执行耗时远程请求,否则会长时间占用连接和数据库锁。
五、Spring Boot 如何配置多数据源?
假设系统同时访问订单库和用户库。
1. 配置文件
app:
datasource:
order:
url: jdbc:mysql://localhost:3306/order_db
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
user:
url: jdbc:mysql://localhost:3306/user_db
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
真实项目应通过环境变量或密钥服务提供密码,不要把生产凭据提交到代码仓库。
2. 创建两个数据源
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("app.datasource.order")
public DataSourceProperties orderDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource orderDataSource(
@Qualifier("orderDataSourceProperties")
DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
@Bean
@ConfigurationProperties("app.datasource.user")
public DataSourceProperties userDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource userDataSource(
@Qualifier("userDataSourceProperties")
DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
@Primary 用于存在多个同类型 Bean 时指定默认选择。使用 DataSourceProperties 可以处理通用 url 与具体连接池属性之间的差异。
3. 配置 JdbcTemplate 和事务管理器
@Bean
@Primary
public JdbcTemplate orderJdbcTemplate(
@Qualifier("orderDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public JdbcTemplate userJdbcTemplate(
@Qualifier("userDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
@Primary
public JdbcTransactionManager orderTransactionManager(
@Qualifier("orderDataSource") DataSource dataSource) {
return new JdbcTransactionManager(dataSource);
}
@Bean
public JdbcTransactionManager userTransactionManager(
@Qualifier("userDataSource") DataSource dataSource) {
return new JdbcTransactionManager(dataSource);
}
业务方法可以指定事务管理器:
@Transactional(transactionManager = "userTransactionManager")
public void updateUser() {
// 只操作用户库
}
MyBatis 项目还需为每个数据源分别配置 SqlSessionFactory、SqlSessionTemplate 和 Mapper 扫描路径;JPA 项目则通常需要独立的 EntityManagerFactory、Repository 包和事务管理器。
4. 跨数据源事务问题
一个 JdbcTransactionManager 只管理对应数据源的一次本地事务。如果同一个业务方法同时写订单库和用户库,给它指定其中一个事务管理器,并不能让两个数据库共同提交或回滚。
跨库强一致需要评估 JTA/XA 或分布式事务方案;很多业务也会使用本地事务配合消息、事务表、幂等和补偿实现最终一致性。不能把两个 @Transactional 简单叠加就当作跨库原子事务。
固定多数据源与动态路由
本文配置的是固定多数据源:业务代码通过 @Qualifier、不同 Mapper 包或不同 Repository 明确选择数据库。另一种方式是继承 AbstractRoutingDataSource,根据租户、读写标记等上下文动态选择目标数据源。
动态路由减少了显式注入,但要特别注意切换时机。事务开始后,连接往往已经从当前数据源取得,此时再修改路由标记通常不会让同一事务切换到另一数据库。路由上下文还需要在请求结束后清理,避免线程池复用导致数据源串用。业务数据库固定且数量较少时,显式配置通常更容易理解和排查。