Spring 的聲明式事務管理與 JdbcTemplate 是 Java 數據訪問層的經典組合。本文將從傳統 XML 配置入手,逐步演進至現代化註解驅動配置,涵蓋數據源、事務管理器、JdbcTemplate、DAO 實現、事務傳播行為、異常回滾規則、測試驗證全鏈路,並給出生產環境優化建議。
DataSource (連接池)
↓ 注入
DataSourceTransactionManager (事務管理器)
↓ 注入
JdbcTemplate (JDBC 操作模板)
↓ 注入
UserDao / OrderDao ... (業務 DAO)
↓ @Transactional
Service Layer (事務邊界)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="https://www.springframework.org/schema/tx"
xsi:schemaLocation="
https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
https://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 1. 連接池 (DBCP2 示例) -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.1.10:3306/test?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="xx"/>
<property name="maxTotal" value="20"/>
<property name="maxIdle" value="10"/>
<property name="maxWaitMillis" value="5000"/>
</bean>
<!-- 2. 事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 3. JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 4. DAO Bean -->
<bean id="userDao" class="com.example.dao.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!-- 5. 聲明式事務 (AOP 切面) -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="update*" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="delete*" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceMethods" expression="execution(* com.example.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
</aop:config>
</beans>
@Configuration
@EnableTransactionManagement // 開啟註解事務支持
@ComponentScan(basePackages = "com.example")
@PropertySource("classpath:jdbc.properties")
public class DataConfig {
@Autowired Environment env;
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource(); // 生產級推薦 HikariCP
ds.setDriverClassName(env.getProperty("jdbc.driver"));
ds.setJdbcUrl(env.getProperty("jdbc.url"));
ds.setUsername(env.getProperty("jdbc.username"));
ds.setPassword(env.getProperty("jdbc.password"));
ds.setMaximumPoolSize(20);
ds.setMinimumIdle(5);
return ds;
}
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
@Repository
public class UserDaoImpl implements UserDao {
private final JdbcTemplate jdbcTemplate;
@Autowired
public UserDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public int insert(User user) {
String sql = "INSERT INTO users(username, email, created_at) VALUES (?, ?, ?)";
return jdbcTemplate.update(sql, user.getUsername(), user.getEmail(), new Timestamp(System.currentTimeMillis()));
}
@Override
public User findById(Long id) {
String sql = "SELECT id, username, email, created_at FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
}
@Override
public List findAll() {
String sql = "SELECT id, username, email, created_at FROM users";
return jdbcTemplate.query(sql, new UserRowMapper());
}
private static class UserRowMapper implements RowMapper {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User u = new User();
u.setId(rs.getLong("id"));
u.setUsername(rs.getString("username"));
u.setEmail(rs.getString("email"));
u.setCreatedAt(rs.getTimestamp("created_at"));
return u;
}
}
}
@Service
@Transactional // 類級默認 REQUIRED, rollbackFor=Exception
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@Autowired
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = BusinessException.class)
public void registerWithBonus(User user, Coupon coupon) {
userDao.insert(user); // 寫入用戶
couponService.bind(user.getId(), coupon.getCode()); // 綁定優惠券(可能拋出異常)
// 任一異常導致整個事務回滾
}
@Override
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public User getUser(Long id) {
return userDao.findById(id);
}
}
| 屬性 | 可選值 | 默認值 | 說明 |
|---|---|---|---|
| propagation | REQUIRED, SUPPORTS, MANDATORY, REQUIRES_NEW, NOT_SUPPORTED, NEVER, NESTED | REQUIRED | REQUIRED: 有事務加入,無則新建。REQUIRES_NEW: 掛起當前事務,強制新建獨立事務。 |
| isolation | DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE | DEFAULT (數據庫默認) | MySQL 默認 REPEATABLE_READ,Oracle/PostgreSQL 默認 READ_COMMITTED。 |
| timeout | int (秒) | -1 (無超時) | 超時自動回滾,需數據庫支持。 |
| readOnly | boolean | false | 提示數據庫優化(如不加鎖),僅作為優化建議,不強制禁止寫操作。 |
| rollbackFor | Class[] | RuntimeException, Error | 指定觸發回滾的受檢異常,常用 Exception.class 覆蓋所有。 |
| noRollbackFor | Class[] | 空 | 指定不觸發回滾的異常(極少用)。 |
List
Listlist = jdbcTemplate.query( "SELECT id, username, email FROM users WHERE status = ?", (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("username"), rs.getString("email")), UserStatus.ACTIVE);
jdbcTemplate.execute((ConnectionCallback) con -> { try (CallableStatement cs = con.prepareCall("{call complex_proc(?, ?)}")) { cs.setLong(1, userId); cs.registerOutParameter(2, Types.INTEGER); cs.execute(); return cs.getInt(2); } });
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = DataConfig.class)
@Transactional // 測試方法默認回滾,保持數據庫乾淨
public class UserServiceTxTest {
@Autowired UserService userService;
@Autowired UserDao userDao;
@Test
public void testRegisterWithBonus_RollbackOnException() {
User user = new User("tx_user", "tx@test.com");
Coupon coupon = new Coupon("INVALID_CODE"); // 會導致綁定失敗拋出異常
assertThrows(BusinessException.class, () -> userService.registerWithBonus(user, coupon));
// 驗證事務回滾:用戶不應存在
assertNull(userDao.findByUsername("tx_user"));
}
}
dataSource.setDataSourceProperties(Map.of("prepStmtCacheSize", "250", "prepStmtCacheSqlLimit", "2048"))(MySQL)。@Transactional,DAO/Repository 層不標註;查詢方法加 readOnly=true。@ControllerAdvice 捕獲 DataAccessException 統一轉為業務異常,避免 SQLException 泄露。spring.jdbc.template.query-timeout 或接入 Druid/P6Spy 監控執行時間超閾值 SQL。/actuator/health 自動檢測 DataSource 連通性。Spring 事務與 JdbcTemplate 的整合核心在於:DataSource → TransactionManager → JdbcTemplate → DAO → @Transactional Service 五層依賴鏈。從 XML 遷移到註解驅動不僅簡化配置,更利於 IDE 重構、編譯期檢查、單元測試隔離。掌握事務傳播、隔離、回滾規則、批量操作、回調執行等進階用法,配合 HikariCP、讀寫分離、分庫分表中間件,即可構建高性能、強一致性的數據訪問層。