1. 整合mybatis
1.1 整合步骤
【第一步】在pom.xml中导入SpringBoot整合mybatis的依赖和mysql驱动
<!--spring boot mybatis起步依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
【第二步】在application.yml配置文件中配置连接参数
# 配置数据库的连接信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?useSSl=false
username: root
password: 123456
【第三步】定义Mapper接口和方法,在Mapper接口上使用**@Mapper**注解表示该类mybatis的mapper接口
@Mapper//表示该类是mybatis的mapper接口类
public interface UserMapper {
@Select("select * from tb_user")
List<User> selectAll();
}
【第四步】在单元测试中调用mapper的方法进行测试
@SpringBootTest
class Spring03ApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
//1.调用userMapper的selectAll()方法,查询所有
List<User> list = userMapper.selectAll();
//2.遍历打印
list.forEach(user -> System.out.println(user));
}
}
1.2 注意事项
问题:很容易在Mapper接口上忘记写@Mapper注解
解决:可以在引导类上使用@MapperScan注解批量加载指定包结构中的所有Mapper接口。那么在mapper接口上就可以不使@Mapper注解
@SpringBootApplication
@MapperScan("com.hsl.mapper")//表示批量加载指定包结构中的所有mapper接口
public class Spring03Application {
public static void main(String[] args) {
SpringApplication.run(Spring03Application.class, args);
}
}
//@Mapper//表示该类是mybatis的mapper接口类
public interface UserMapper {
@Select("select * from tb_user")
List<User> selectAll();
}
1.3 Mybatis相关配置
问题:java 对象中的属性是驼峰命名法,而数据库的字段名为采用的是下划线命名法,二者并不一致
- 配置开启驼峰命名自动映射
mybatis:
configuration:
map-underscore-to-camel-case: true #开启驼峰命名自动映射
- 加载xml映射配置文件
- 映射文件配置别名:type-aliases-package: com.hsl.pojo
- 加载mybatis核心配置文件:config-location: classpath:mybatis-config.xml
- 加载mapper目录中所有的Mapper.xml文件:*mapper-locations: classpath:mapper/Mapper.xml
mybatis:
configuration:
map-underscore-to-camel-case: true #开启驼峰命名自动映射
type-aliases-package: com.hsl.pojo #pojo包中所有的类的类名就是别名,在映射配置文件中可以使用别名
config-location: classpath:mybatis-config.xml # 加载resources目录中mybatis核心配置文件
mapper-locations: classpath:mapper/*Mapper.xml #加载mapper目录中userMapper.xml文件。如果映射配置文件和mapper接口同名同位置,则会自动加载,不需要配置
1.4 日志级别配置
ALL 最低等级,用于打开所有日志记录。
TRACE 很低的日志级别,一般不会使用。
DEBUG 主要用于开发与测试过程中打印一些运行信息,不可以用于生产环境。
INFO 记录一些信息型消息比如服务器启动成功,输入的数据,输出的数据等等。
WARN 记录警告信息比如客户端和服务器之间的连接中断,数据库连接丢失等信息。
ERROR 用于记录ERROR和Exception信息。
FATAL 指可能导致程序终止的非常严重的信息。在这种事件之后你的应用很可能会崩溃。
OFF 最高等级,用于关闭所有日志记录。
① 设置整个项目日志级别
# 设置日志级别
logging:
level:
root: debug
② 查看运行过程中mapper包下的SQL信息
# 设置日志级别
logging:
level:
com.hsl.mapper: debug #mapper包中的所有日志输出级别为debug级别。可以看到sql语句
③ 如果只想看某个类或某个方法的SQL,就需要提供更为具体的类名或方法名
logging:
level:
com.hsl.mapper.UserMapper.selectAll: debug
1.5 数据源配置
Spring Boot 使用的连接池是 hikari,可以在 application.yml 中敲入 hikari 提示相关配置.
-
最大连接数和最小连接数
yaml文件
#hikari连接池配置信息
hikari:
maximum-pool-size: 10 # 最大连接数
minimum-idle: 3 # 最小连接数
properties文件
#hikari连接池配置信息
#最大连接数
spring.datasource.hikari.maximum-pool-size=10
#最小连接数
spring.datasource.hikari.minimum-idle=3
- 单元测试类中获取以上配置信息
@SpringBootTest//表示该类是一个SpringBoot单元测试类
class Spring03ApplicationTests {
@Autowired //指定类型是HikariDataSource,否则无法调用特有方法
private HikariDataSource dataSource;
@Test
void contextLoads() {
System.out.println("最大连接数:" + dataSource.getMaximumPoolSize());
System.out.println("最小连接数:" + dataSource.getMinimumIdle());
}
2. 事务管理
2.1 环境搭建
- 环境准备,初始化表和数据:在 init.sql 中加入
-- 删除tb_account表
drop table if exists tb_account;
-- 创建tb_account表
create table tb_account
(
id int primary key auto_increment, -- 账户id,主键自增
name varchar(20) not null, -- 账户名称
amount double not null -- 金额
);
-- 初始化数据
insert into tb_account values(null,'张三',1000),(null,'李四',1000);
- pojo
@Data
public class Account {
private Integer id; //账户id
private String name; //账户名称
private double amount; //金额
}
- mapper
public interface AccountMapper {
/**
* 根据id查询账户信息
* @param id 账户id
* @return
*/
@Select("select * from tb_account where id = #{id}")
Account findById(int id);
/**
* 根据id修改账户金额
* @param id 账户id
* @param amount 要加或者减的金额
*/
@Update("update tb_account set amount = amount + #{amount} where id = #{id}")
void updateAmount(@Param("id") int id,@Param("amount") double amount);
}
- service
public interface AccountService {
/**
* 转账方法
* @param fromId 转出账户id
* @param toId 转入账户id
* @param amount 转账金额
*/
void transfer(int fromId,int toId,double amount);
}
@Service //将Bean添加到spring容器中
@Slf4j //用于打印日志信息
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
/**
* 实现转账业务:
* 要求:转出账户金额充足才可以进行转账
*
* @param fromId 转出账户id
* @param toId 转入账户id
* @param amount 转账金额
*/
@Override
public void transfer(int fromId, int toId, double amount) throws FileNotFoundException {
Account fromAccount = accountMapper.findById(fromId);
//1 根据fromId查询转出账户信息,判断账户余额是否充足
if (fromAccount.getAmount() >= amount) {
//2 转出账户余额充足,开始进行转换
accountMapper.updateAmount(fromId, -amount);
accountMapper.updateAmount(toId, amount);
} else {
log.info("余额仅剩{},无法进行转账操作!", fromAccount.getAmount());
}
}
}
- 测试
@SpringBootTest
@Slf4j//日志打印
class Spring03TransactionalApplicationTests {
@Autowired
private AccountService accountService;
@Test
void contextLoads(){
log.info("转账开始...");
accountService.transfer(1,2,500);
log.info("转账结束...");
}
}
结果为:
2.2 问题
如果中间有异常的话,还能照常执行吗
public void transfer(int fromId, int toId, double amount) throws FileNotFoundException {
Account fromAccount = accountMapper.findById(fromId);
//1 根据fromId查询转出账户信息,判断账户余额是否充足
if (fromAccount.getAmount() >= amount) {
//2 转出账户余额充足,开始进行转换
accountMapper.updateAmount(fromId, -amount);
//造一个异常
int num = 1/0;
accountMapper.updateAmount(toId, amount);
} else {
log.info("余额仅剩{},无法进行转账操作!", fromAccount.getAmount());
}
}
结果报了算数异常:java.lang.ArhslcException: / by zero
表中的数据:张三转出了500,李四没有得到转入
因为执行到 int num = 1/0;
方法已经停止运行了,所有没有转入金额
2.3 解决
给需要事务管理的方法上加上@Transactional注解
@Transactional
public void transfer(int fromId, int toId, double amount) throws FileNotFoundException {
Account fromAccount = accountMapper.findById(fromId);
//1 根据fromId查询转出账户信息,判断账户余额是否充足
if (fromAccount.getAmount() >= amount) {
//2 转出账户余额充足,开始进行转换
accountMapper.updateAmount(fromId, -amount);
//造一个异常
int num = 1/0;
accountMapper.updateAmount(toId, amount);
} else {
log.info("余额仅剩{},无法进行转账操作!", fromAccount.getAmount());
}
}
重新运行 transfer 发现事务发生了回滚
可以通过日志配置来实现看数据库监测事务的运行情况
#配置springboot日志级别,输出运行过程中的SQL语句
logging:
level:
com.hsl.mapper: debug
org.springframework.jdbc.support.JdbcTransactionManager: debug
其中 spring boot 的 JdbcTransactionManager 中会显示事务的开始、提交、回滚等信息,只需要把它的日志级别调整为 debug 即可
- 日志解读信息
2022-06-13 11:10:50.290 INFO 1872 --- [ main] .i.Spring03TransactionalApplicationTests : 转账开始...
//创建新事务
2022-06-13 11:10:50.294 DEBUG 1872 --- [ main] o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [com.hsl.service.impl.AccountServiceImpl2.transfer2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
//打开数据库连接
2022-06-13 11:10:50.294 DEBUG 1872 --- [ main] o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@2a066689] for JDBC transaction
// 切换数据库连接为手动提交事务
2022-06-13 11:10:50.296 DEBUG 1872 --- [ main] o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2a066689] to manual commit
// 查询 转账账户余额
2022-06-13 11:10:50.322 DEBUG 1872 --- [ main] c.hsl.mapper.AccountMapper.findById : ==> Preparing: select * from tb_account where id = ?
2022-06-13 11:10:50.347 DEBUG 1872 --- [ main] c.hsl.mapper.AccountMapper.findById : ==> Parameters: 1(Integer)
2022-06-13 11:10:50.373 DEBUG 1872 --- [ main] c.hsl.mapper.AccountMapper.findById : <== Total: 1
// 修改 转账账户减钱
2022-06-13 11:10:50.375 DEBUG 1872 --- [ main] c.i.mapper.AccountMapper.updateAmount : ==> Preparing: update tb_account set amount = amount + ? where id = ?
2022-06-13 11:10:50.376 DEBUG 1872 --- [ main] c.i.mapper.AccountMapper.updateAmount : ==> Parameters: -500.0(Double), 1(Integer)
2022-06-13 11:10:50.377 DEBUG 1872 --- [ main] c.i.mapper.AccountMapper.updateAmount : <== Updates: 1
// 回滚事务
2022-06-13 11:10:50.377 DEBUG 1872 --- [ main] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction rollback
2022-06-13 11:10:50.377 DEBUG 1872 --- [ main] o.s.jdbc.support.JdbcTransactionManager : Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@2a066689]
// 归还数据库连接
2022-06-13 11:10:50.517 DEBUG 1872 --- [ main] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2a066689] after transaction
一旦我们进行了事务管理,我们从容器中获取到的service就不是目标对象了,而是代理对象,其内部通过AOP的方式对目标对象进行了增强,代理对象内部使用JdbcTransactionManager对象在切点方法执行前后进行事务管理
2.4 初始化库
每次执行测试之后,数据库表中的数据都会发生变化,为了方便测试,需要手动恢复表中的数据.
解决思路:把 DDL 和 DML 的 sql 分别存储于 resources/schema.sql 和 resources/data.sql 这两个文件中,然后 application.properties 中配置
- schema.sql
-- 删除tb_account表
drop table if exists tb_account;
-- 创建tb_account表
create table tb_account
(
id int primary key auto_increment, -- 账户id,主键自增
name varchar(20) not null, -- 账户名称
amount double not null -- 金额
);
- data.sql
-- 初始化数据
insert into tb_account values(null,'张三',1000),(null,'李四',1000);
配置文件配置
- application.properties
spring.datasource.initialization-mode=always
- application.yml
sql:
init:
mode: always # 总是初始化关系型数据库
2.5 进阶
@Transactional 注解,它其中有很多配置,称为事务属性,摘录如下
属性名称 | 含义 | 补充说明 |
---|---|---|
propagation | 传播行为 | - |
isolation | 隔离级别 | - |
timeout | 超时时间 | 当规定时间内,事务仍未完成时,会回滚并结束事务 |
readOnly | 是否只读 | 某些数据库驱动会对只读事务做优化 |
rollbackFor | 遇到什么异常回滚事务 | - |
noRollbackFor | 遇到什么异常不回滚事务 | - |
1) 事务失效(器异常所致)
将异常缓存编译期异常,程序抛出异常,运行之后发现事务仍然提交。
@Override
@Transactional
public void transfer2(int fromId, int toId, double amount) throws FileNotFoundException {
Account fromAccount = accountMapper.findById(fromId);
//1 根据fromId查询转出账户信息,判断账户余额是否充足
if (fromAccount.getAmount() >= amount) {
//2 转出账户余额充足,开始进行转换
accountMapper.updateAmount(fromId, -amount);
//造一个异常
//int num = 1 / 0;
new FileInputStream("");//抛出了编译异常
accountMapper.updateAmount(toId, amount);
} else {
log.info("余额仅剩{},无法进行转账操作!", fromAccount.getAmount());
}
}
-
原因:
- Spring 默认只有抛出运行时异常(即 RuntimeException 及子类)或 Error 及子类时,才会回滚事务
- 如果抛出的是其它编译时异常,仍然会提交事务
-
解决办法:配置 rollbackFor = Exception.class
@Transactional(rollbackFor = Exception.class)
2) 事务失效(捕获异常所致)
在业务层被事务管理的方法内的异常不能捕获, 如果try…catch…最终事务还是提交了
-
原因:
- 事务提交、回滚,都是代理对象调用事务通知来完成的,如果代理对象不知道出现了异常,也就没有机会去执行回滚
- 自己 try-catch 异常,意味着代理对象认为【没有发生异常】,因此也会提交事务
-
解决办法:业务方法内不要捕获异常、或者将捕获的异常重新抛出
3) 事务属性 - propagation
两个受事务控制的业务方法之间进行调用时,事务是合二为一、还是另开新事务,这可以通过传播行为进行控制,对其中最常用的 REQUIRED 和 REQUIRES_NEW 说明如下
@Service
public class ServiceA {
private static final Logger log = LoggerFactory.getLogger(ServiceA.class);
@Autowired
private ServiceB b;
@Transactional(propagation = 传播行为)
public void foo() {
log.debug("foo");
b.bar();
}
}
@Service
public class ServiceB {
private static final Logger log = LoggerFactory.getLogger(ServiceB.class);
@Transactional(propagation = 传播行为)
public void bar() {
log.debug("bar");
}
}
ServiceA.foo() 调用 ServiceB.bar(),其传播行为组合测试如下
测试1
- foo - @Transactional(propagation = Propagation.REQUIRED)
- bar - @Transactional(propagation = Propagation.REQUIRED)
- 结论 foo bar 同一事务
测试2
- foo - @Transactional(propagation = Propagation.REQUIRED)
- bar - @Transactional(propagation = Propagation.REQUIRES_NEW)
- 结论 foo bar 不同事务
测试3
- foo - @Transactional(propagation = Propagation.REQUIRED)
- bar - @Transactional(propagation = Propagation.REQUIRES_NEW)
- bar - 出现异常、foo 不捕获
- 结论 bar 回滚,foo 也会回滚
测试4
- foo - @Transactional(propagation = Propagation.REQUIRED)
- bar - @Transactional(propagation = Propagation.REQUIRES_NEW)
- bar - 出现异常、foo 捕获
- 结论 bar 回滚,foo 因为捕获了异常,因此会提交
其它传播行为
属性名称 | 含义 | 补充说明 |
---|---|---|
REQUIRED | 需要事务,有则加入,无则创建新事务 | |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 | |
SUPPORTS | 支持事务,有则加入,无则在独立的连接中运行 SQL | 结合 Hibernate、JPA 时有用,配在查询方法上 |
NOT_SUPPORTED | 不支持事务,不加入,在独立的连接中运行 SQL | - |
MANDATORY | 必须有事务,否则抛异常 | - |
NEVER | 必须没事务,否则抛异常 | - |
NESTED | 嵌套事务 | 仅对 DataSourceTransactionManager 有效 |
4) 传播行为失效
@Service
public class ServiceC {
private static final Logger log = LoggerFactory.getLogger(ServiceC.class);
@Transactional(propagation = Propagation.REQUIRED)
public void foo() {
log.debug("foo");
bar();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bar() {
log.debug("bar");
}
}
在 foo 方法内调用 bar 方法,因为两个方法都配置了 @Transactional,现在测试 foo 方法,根据配置,期望有两个事务
@SpringBootTest
public class TransactionalTests {
// ...
@Autowired
private ServiceC serviceC;
@Test
void test5() throws Exception {
serviceC.foo();
}
}
观察发现,自始至终只有一个事务,并没有像预期的在 bar 方法调用时由 @Transactional(propagation = Propagation.REQUIRES_NEW)
创建一个新事务
原因:
这实际就是前面讲代理时的通知失效问题,同类的两个方法调用不走代理
解决方法1:
引导类中新增 @EnableAspectJAutoProxy(exposeProxy = true)
,同时添加 aop 依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
调用时修改为
@Service
public class ServiceC {
private static final Logger log = LoggerFactory.getLogger(ServiceC.class);
@Transactional(propagation = Propagation.REQUIRED)
public void foo() {
log.debug("foo");
((ServiceC) AopContext.currentProxy()).bar();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bar() {
log.debug("bar");
}
}
解决方法2:
自己注入自己(其实注入了代理),其它不用动
@Service
public class ServiceC {
private static final Logger log = LoggerFactory.getLogger(ServiceC.class);
@Autowired
private ServiceC serviceC;
@Transactional(propagation = Propagation.REQUIRED)
public void foo() {
log.debug("foo");
serviceC.bar();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bar() {
log.debug("bar");
}
}
ebug(“foo”);
((ServiceC) AopContext.currentProxy()).bar();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bar() {
log.debug("bar");
}
}
解决方法2:
自己注入自己(其实注入了代理),其它不用动
```java
@Service
public class ServiceC {
private static final Logger log = LoggerFactory.getLogger(ServiceC.class);
@Autowired
private ServiceC serviceC;
@Transactional(propagation = Propagation.REQUIRED)
public void foo() {
log.debug("foo");
serviceC.bar();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bar() {
log.debug("bar");
}
}