# 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 '创建时间'
);
1
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>
1
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 +
                '}';
    }
}
1
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);
}
1
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);
    }
}
1
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);

}
1
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);      // 收款人余额增加
    }

}
1
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);
    }
}
1
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
1
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;
    }
}
1
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);
    }
}
1
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) { ... }
}
1
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) { ... }
}
1
2
3
4
5
6
7
8
9
10
11
  • 在父类上添加事务管理 @Transactional ,子类继承父类,那么每个子类就不需要单独添加了。
  • 子类方法可以单独添加配置,来覆盖父类的配置,例如添加 @Transactional(readOnly = true) ,让事务是只读的,提升性能。

# 3 使用AOP进行配置

但是上面的方式,继承 BaseService 的 Service 中的方法,每个方法被调用都会开启事务,我们所以还可以配置指定名称开头的方法才开启事务,例如配置 saveupdate 开头的方法,才开启读写事务,配置 findquery 开头的方法开启只读事务,其他方法不开启事务。

在配置类中添加如下配置:

/**
 * 定义事务拦截器(核心组件)
 */
@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);
}
1
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 配置的默认规则
1

具体使用哪种事务配置方式,根据需要选择。