# Spring教程 - 13 基于注解Spring项目完整配置
前面讲解了 Spring 的 IoC、AOP 和事务,配置都是逐渐添加的,可能比较乱,这里整理一个比较完整的 Spring 项目配置。
从新建项目开始,实现从数据库读取和修改数据库的数据。
下面就来实现。
# 13.1 完整的Spring项目配置实现
# 13.1.1 准备工作
新建数据库和数据库表,还是使用前面声明式事务中的表,如果从前面过来的,跳过就行了。
-- 创建数据库
CREATE DATABASE foooor_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 创建一张用户表
CREATE TABLE tb_user (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
balance INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '余额',
create_time DATETIME NOT NULL COMMENT '创建时间'
);
2
3
4
5
6
7
8
9
10
# 13.1.2 新建项目
新的 IDEA 可以选择 Java 项目就可以了。
# 13.1.3 引入依赖
在项目的 pom.xml 中添加依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.foooor</groupId>
<artifactId>hello-spring</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 引入spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.2.9</version>
</dependency>
<!-- AOP相关 引入spring aop依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.2.9</version>
</dependency>
<!-- AOP相关 引入spring aspects依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.2.9</version>
</dependency>
<!-- 引入spring jdbc依赖,用于数据库操作 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.2.9</version>
</dependency>
<!-- 引入mysql驱动依赖,用于连接mysql数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 引入druid依赖,提供数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.27</version>
</dependency>
<!-- 引入logback依赖,日志框架实现 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
</dependency>
<!-- 引入slf4j依赖,日志框架接口门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
<!-- 引入junit5依赖,用于单元测试 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.13.4</version>
</dependency>
<!-- 引入spring 对 junit的支持依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.2.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
- 在上面的代码中添加了 Spring 的核心依赖、AOP依赖、Spring JDBC、日志、以及 junit 测试相关的依赖。
添加完成,右键 pom.xml 文件,Maven -> Reload project
,下载一下依赖。
# 13.1.4 创建Java类
这里创建实体类 User、Dao层、Service层、Controller层,我们还是按照 Controller --> Service --> Dao
的方式,分层调用来实现。
创建相关的包,并在包下创建相关的类。
# 1 创建实体类
package com.foooor.hellospring.pojo;
import java.util.Date;
public class User {
private String id;
private String username;
private Integer balance;
private Date createTime;
// ...getters and setter 略
/**
* 重写 toString 方法,方便打印输出
*/
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", username='" + username + '\'' +
", balance='" + balance + '\'' +
", createTime=" + createTime +
'}';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 2 创建Dao
接口:IUserDao.java
添加两个方法,一个是查询用户,一个是修改用户的余额。
package com.foooor.hellospring.dao;
import com.foooor.hellospring.pojo.User;
public interface IUserDao {
/**
* 根据id查询用户
*/
User selectById(int userId);
/**
* 修改用户余额
*/
void updateBalance(int userId, int balance);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
实现类:UserDaoImpl.java
记得添加注解。
package com.foooor.hellospring.dao.impl;
import com.foooor.hellospring.dao.IUserDao;
import com.foooor.hellospring.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class UserDaoImpl implements IUserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 根据id获取用户
*/
@Override
public User selectById(int userId) {
// 1.准备sql
String sql = "select balance from tb_user where id = ?";
// 2.执行查询
List<User> userList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class), userId);
// 3.返回结果
return userList.isEmpty() ? null : userList.get(0);
}
/**
* 修改用户余额
*/
public void updateBalance(int userId, int balance) {
// 1.准备sql
String sql = "update tb_user set balance = balance + ? where id = ?";
// 2.执行更新
jdbcTemplate.update(sql, balance, userId);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- 上面添加了两个方法,一个是查询用户,这里我为了不让查询不到的时候报错,所以使用了列表的方式查询。
# 3 创建Service
接口:IUserService.java
我们是在 Service 中实现业务逻辑,所以也就在这一层实现转账操作。
正如在开始描述的,我们会实现针对 save 和 update 开头的方法,配置开启声明式事务,所以这里两个方法是一样的,但是最终一个会开启事务,一个不会开启事务。
package com.foooor.hellospring.service;
public interface IUserService {
/**
* 转账
*/
void transfer(int fromUserId, int toUserId, int amount);
}
2
3
4
5
6
7
8
9
10
实现类:UserServiceImpl.java
记得添加注解。
package com.foooor.hellospring.service.impl;
import com.foooor.hellospring.dao.IUserDao;
import com.foooor.hellospring.pojo.User;
import com.foooor.hellospring.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(rollbackFor = Exception.class) // 声明式事务配置,任何异常都回滚
public class UserServiceImpl implements IUserService {
@Autowired
private IUserDao userDao;
/**
* 转账
*/
@Override
public void transfer(int fromUserId, int toUserId, int amount) {
// 1.转账前的检查
if (amount <= 0) {
throw new IllegalArgumentException("转账金额必须大于0");
}
if (fromUserId == toUserId) {
throw new IllegalArgumentException("转账方和收款方不能相同");
}
User fromUser = userDao.selectById(fromUserId);
if (fromUser == null) {
throw new IllegalArgumentException("转账方不存在");
}
if (userDao.selectById(toUserId) == null) {
throw new IllegalArgumentException("收款方不存在");
}
// 2.检查转账方是否有足够的余额
if (fromUser.getBalance() < amount) {
throw new IllegalArgumentException("转账方余额不足");
}
// 3.更新转账方和收款方的余额
userDao.updateBalance(fromUserId, -amount); // 转账人余额减少
// 在两次更新之间模拟一个异常
// int a = 2 / 0;
userDao.updateBalance(toUserId, amount); // 收款人余额增加
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 4 创建Controller
UserController.java
记得添加注解。
package com.foooor.hellospring.controller;
import com.foooor.hellospring.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Autowired
private IUserService userService;
/**
* 转账
*/
public void transfer(int fromUserId, int toUserId, int amount) {
userService.transfer(fromUserId, toUserId, amount);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- Controller 是接口层,是提供给前端调用的,所以调用 Service 就可以了。
# 13.1.5 配置项目
# 1 创建数据库配置文件
为了方便管理,单独为数据库配置信息创建一个配置文件。
在项目的 resources
目录下创建 jdbc.properties
文件,配置数据的连接信息,如下:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/foooor_db?useSSL=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
2
3
4
# 2 创建Spring配置类
创建包,并在包下创建配置类,我这里新建了 com.foooor.hellospring.config
包,并在其下创建 SpringConfig.java
(名称自定义)
package com.foooor.hellospring.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration // 配置类
@EnableAspectJAutoProxy // 开启基于 @AspectJ 注解的 AOP
@EnableTransactionManagement // 开启事务管理
@ComponentScan("com.foooor.hellospring") // 开启组件扫描,并指定扫描的包
@PropertySource("classpath:jdbc.properties") // 加载外部配置文件
public class SpringConfig {
@Value("${jdbc.driver}") // 读取从配置文件注入的属性值。
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 配置 Druid 数据源
*/
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
/**
* 配置 JdbcTemplate
* 方法的参数,Spring IoC 容器会自动注入对应的 Bean
*/
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
/**
* 配置事务管理器
*/
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager txManager = new DataSourceTransactionManager();
txManager.setDataSource(dataSource);
return txManager;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- 首先在类上添加各种注解,包括配置类表示、配置Spring扫描的路径、和外部配置文件的路径、AOP、事务相关的注解;
- 并在配中配置 Druid 的数据源,
JdbcTemplate
、事务管理器。
# 13.1.5 测试
创建测试类,并注入 Controller 进行测试:
package com.foooor.hellospring;
import com.foooor.hellospring.config.SpringConfig;
import com.foooor.hellospring.controller.UserController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(classes = {SpringConfig.class}) // 指定 Spring 配置文件
public class SpringTest {
@Autowired
private UserController userController;
/**
* 测试转账功能
*/
@Test
public void testTransfer() {
userController.transfer(2, 3, 100);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
最终的项目结构如下:
# 13.2 事务实践
# 1 基本配置方式
上面在使用声明式事务的时候,我们可以在每个 Service 类上添加 @Transactional
注解用来开启事务,在需要单独处理的方法上,可以单独添加 @Transactional
注解进行单独的配置。
举个栗子:
@Service
@Transactional(rollbackFor = Exception.class)
public class UserService {
@Transactional(readOnly = true) // 针对方法单独配置为只读事务
public User getUser(Long id) { ... }
public void saveUser(User u) { ... }
}
2
3
4
5
6
7
8
9
- 这种方式简洁直观,也是最常见的使用方式。
但是每个 Service 或 Service 中的方法都添加注解,还是有点麻烦,同一个项目需要不同的开发者自觉维护一致性。
# 2 使用父类进行配置
所以这里我们可以创建一个 BaseService
,在 BaseService
上添加 @Transactional
注解,让需要开启事务的 Service 继承 BaseService
即可。
举个栗子:
@Transactional(rollbackFor = Exception.class)
public abstract class BaseService { }
@Service
public class UserService extends BaseService {
@Transactional(readOnly = true) // 子类可以针对方法单独配置
public User getUser(Long id) { ... }
public void saveUser(User u) { ... }
}
2
3
4
5
6
7
8
9
10
11
- 在父类上添加事务管理
@Transactional
,子类继承父类,那么每个子类就不需要单独添加了。 - 子类方法可以单独添加配置,来覆盖父类的配置,例如添加
@Transactional(readOnly = true)
,让事务是只读的,提升性能。
# 3 使用AOP进行配置
但是上面的方式,继承 BaseService
的 Service 中的方法,每个方法被调用都会开启事务,我们所以还可以配置指定名称开头的方法才开启事务,例如配置 save
或 update
开头的方法,才开启读写事务,配置 find
、query
开头的方法开启只读事务,其他方法不开启事务。
在配置类中添加如下配置:
/**
* 定义事务拦截器(核心组件)
*/
@Bean
public TransactionInterceptor transactionInterceptor(PlatformTransactionManager txManager) {
// 1. 创建事务属性源,根据方法名模式匹配不同的事务规则
NameMatchTransactionAttributeSource nameMatchSource = new NameMatchTransactionAttributeSource();
// 2. 定义“只读事务”属性(给查询类方法使用)
RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
readOnlyTx.setReadOnly(true); // 不修改数据库
// PROPAGATION_SUPPORTS 表示:如果当前有事务,就加入;没有事务,也可以非事务运行
readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
// 3. 定义"读写事务"属性(给更新方法使用 save/update/delete)
RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
// 遇到 Exception 或其子类时回滚
requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
// PROPAGATION_REQUIRED 表示:必须在事务中运行,没有就新建一个
requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 4. 根据方法名前缀匹配不同的事务属性
Map<String, TransactionAttribute> txMap = new HashMap<>();
// 查询类方法:只读事务
txMap.put("get*", readOnlyTx);
txMap.put("find*", readOnlyTx);
txMap.put("list*", readOnlyTx);
txMap.put("query*", readOnlyTx);
// 修改类方法:读写事务(会触发回滚)
txMap.put("save*", requiredTx);
txMap.put("update*", requiredTx);
txMap.put("delete*", requiredTx);
txMap.put("insert*", requiredTx);
nameMatchSource.setNameMap(txMap);
// 注解规则
AnnotationTransactionAttributeSource annotationSource = new AnnotationTransactionAttributeSource();
// 设置事务属性源,这样可以实现,方法注解 > 类注解 > NameMatch规则
CompositeTransactionAttributeSource compositeSource = new CompositeTransactionAttributeSource(annotationSource, nameMatchSource);
// 5. 创建事务拦截器
TransactionInterceptor interceptor = new TransactionInterceptor();
// 注入事务管理器
interceptor.setTransactionManager(txManager);
interceptor.setTransactionAttributeSource(compositeSource);
return interceptor;
}
/**
* 定义 AOP 切点 + 事务拦截器的绑定(即 Advisor)
* 用于告诉 Spring 哪些包下的方法需要自动应用事务规则。
*/
@Bean
public Advisor txAdvisor(TransactionInterceptor interceptor) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// 匹配所有 com.foooor.hellospring.service 包及子包下的所有方法
pointcut.setExpression("execution(* com.foooor.hellospring.service..*(..))");
return new DefaultPointcutAdvisor(pointcut, interceptor);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- 上面创建了事务拦截器,会拦截事务。针对不同名称开头的方法进行不同的处理(
get/find/list/query
开启只读事务、save/update/delete/insert
开头的方法开启读写事务)。 - 如果不是上面开头的方法是不会开启事务的,需要注意不开启事务和开启只读事务的区别,开启只读事务,同一个方法中多次相同的查询,结果是一样的,对于一些第三方的ORM框架,还可以使用缓存,提高性能。
- 需要注意,添加了上面的配置,Service和方法上就不需要添加
@Transactional
注解了。
另外还需要注意,使用上面的方式,要非常注意方法的命名才可以,否则不会开启事务。
如果有方法名称就是不以上面的名称开头,或者某个 save
开头的方法就是想使用 REQUIRES_NEW
,而不是默认的 REQUIRED
,那么可以单独在在方法上添加 @Transactional
注解即可。
通过上面的配置,Spring 的事务拦截器会按以下优先级决定最终事务属性:
方法上的 @Transactional > 类上的 @Transactional > AOP 配置的默认规则
具体使用哪种事务配置方式,根据需要选择。