Spring 整合 MyBatis
一、目标与回顾
1.1.本章目标
- 掌握使用Spring框架整合MyBatis
- 掌握使用映射器实现整合
- 掌握Spring声明式事务
1.2.Maven依赖包
<?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.aiden</groupId>
<artifactId>T133-Spring</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<!--父级项目公用依赖包-->
<dependencies>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--日志依赖-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--用于简化实体类的编写-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!--spring-webmvc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!--spring-jdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!--支持切入点表达式等等-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<!--mybatis-spring整合包【重要】-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
</dependencies>
</project>
1.3.MyBatis开发环境搭建
使用MyBatis的开发步骤
步骤1、在maven中添加Mybatis依赖包
步骤2、编写MyBatis核心配置文件(configuration.xml)
步骤3、创建实体类-POJO
步骤4、DAO(mapper)层-SQL映射文件(mapper.xml)
步骤5、创建测试类
① 读取核心配置文件mybatis-config.xml
② 创建SqlSessionFactory对象,读取配置文件
③ 创建SqlSession对象
④ 调用mapper文件进行数据操作<!--步骤1、在maven中添加Mybatis依赖包--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency>
<!--步骤2、编写MyBatis核心配置文件(configuration.xml)--> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <!--配置--> <configuration> <!--引入配置文件--> <properties resource="database.properties"></properties> <!--配置日志输出--> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <!--配置类别名--> <typeAliases> <package name="com.cvs.pojo"/> </typeAliases> <!--环境配置 default默认--> <environments default="development"> <!--可配置多个开发环境:本地环境、线上测试环境、灰度环境、生产环境--> <!--开发环境配置--> <environment id="development"> <!--事务管理:JDBC--> <transactionManager type="JDBC"/> <!--配置数据源--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/cvs_db?useSSL=true&useUnicode=true&characterEncoding=UTF-8&Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> <!-- 测试环境--> <environment id="test"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <mappers> <!--映射文件必须要配置--> <mapper resource="com/cvs/mapper/SysRoleMapper.xml"></mapper> </mappers> </configuration>
#database.properties配置文件 driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/cvs_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username=root password=root
//步骤3、创建实体类-POJO package com.cvs.pojo; import java.util.Date; /** * 角色类 */ public class SysRole { private Integer id; //id private String code; //角色编码 private String roleName; //角色名称 private Integer createdUserId; //创建者 private Date createdTime; //创建时间 private Integer updatedUserId; //更新者 private Date updatedTime; //更新时间 //省略getter/setter... }
//步骤4、DAO层 (mapper)接口 package com.cvs.mapper; import com.cvs.pojo.SysRole; import java.util.List; /** * 系统用户角色接口 * @author Aiden */ public interface SysRoleMapper { /** * 查询所有系统角色 * @return */ List<SysRole> getSysRoleAll(); }
<!--步骤4、创建与接口对应的-SQL映射文件(SysRoleMapper.xml)--> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!--namespace:命名空间 java接口文件全包名--> <mapper namespace="com.cvs.mapper.SysRoleMapper"> <!--查询所有系统角色--> <select id="getSysRoleAll" resultType="com.cvs.pojo.SysRole"> select * from t_sys_role </select> </mapper>
import com.cvs.mapper.SysRoleMapper; import com.cvs.pojo.SysRole; import com.cvs.utils.MyBatisUtils; import org.apache.ibatis.session.SqlSession; import org.junit.Test; import java.util.List; /** * 单元测试类 * @author Aiden */ public class SysRoleMapperTest { /** * 步骤5、创建测试类 *①读取核心配置文件mybatis-config.xml *②创建SqlSessionFactory对象,读取配置文件 *③创建SqlSession对象 *④调用mapper文件进行数据操作 */ @Test public void getSysRoleAll() { SqlSession sqlSession = MyBatisUtils.getSqlSession(); List<SysRole> sysRoleList = sqlSession.getMapper(SysRoleMapper.class).getSysRoleAll(); sysRoleList.forEach(r->{ System.out.println(r.getId()+"\t"+r.getRoleName()); }); MyBatisUtils.closeSqlSession(sqlSession); } }
package com.cvs.utils; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; /** * MyBatis工具辅助类 * @author Aiden */ public class MyBatisUtils { /** * 获取 SqlSession * @return */ public static SqlSession getSqlSession() { SqlSession sqlSession = null; String resource = "mybatis-config.xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); sqlSession = sqlSessionFactory.openSession(); } catch (IOException e) { e.printStackTrace(); } return sqlSession; } /** * 关闭 sqlSession * @param sqlSession */ public static void closeSqlSession(SqlSession sqlSession) { if (sqlSession != null) { sqlSession.close(); } } }
1.4.MyBatis-Spring
MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。
当使用maven构建时必须引入以下配置
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency>
当我们想要一起使用Spring和MyBatis时,需要在 Spring 应用上下文中定义
SqlSessionFactory
和一个数据映射器类
。在 MyBatis-Spring 中,可使用SqlSessionFactoryBean来创建 SqlSessionFactory。要配置这个工厂 bean,只需要把下面代码放在 Spring 的 XML 配置文件中:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"></property> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <!--注入SqlSessionFactoryBean--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--配置数据源--> <property name="dataSource" ref="dataSource"></property> <!-- 配置别名--> <property name="typeAliasesPackage" value="com.cvs.pojo"></property> <!--引用mybatis核心配置文件--> <property name="configLocation" value="classpath:mybatis-config2.xml"></property> <!-- 配置mapper映射文件--> <property name="mapperLocations"> <list> <value>com/cvs/mapper/*.xml</value> </list> </property> </bean>
二、基本整合方式
2.1.基本整合方式2-1
思路梳理
- 以上流程可以全部移交给Spring来处理
- 读取配置文件、组件的创建、组件之间的依赖关系以及整个框架的生命周期都由Spring容器统一管理
2.2.基本整合方式2-2
Spring和MyBatis的整合步骤
- 整合所需的依赖及配置
- 通过Spring配置文件配置数据源
- 通过Spring配置文件创建SqlSessionFactory
- 通过SqlSessionTemplate操作数据库
使用SqlSessionDaoSupport简化编码
使用Spring配置数据源时,首先选择一种具体的数据源实现技术
dbcp
c3p0
proxool
druid
dbcp:DBCP是一个依赖Jakarta commons-pool对象池机制的数据库连接池.DBCP可以直接的在应用程序中使用,Tomcat的数据源使用的就是DBCP。
c3p0:c3p0是一个开放源代码的JDBC连接池,它在lib目录中与Hibernate一起发布,包括了实现jdbc3和jdbc2扩展规范说明的Connection 和Statement 池的DataSources 对象。
druid :阿里出品,淘宝和支付宝专用数据库连接池,但它不仅仅是一个数据库连接池,它还包含一个ProxyDriver,一系列内置的JDBC组件库,一个SQL Parser。支持所有JDBC兼容的数据库,包括 Oracle、MySql、Derby、Postgresql、SQL Server、H2等等。
Druid针对Oracle和MySql做了特别优化,比如Oracle的PS Cache内存占用优化,MySql的ping检测优化。
Druid提供了MySql、Oracle、Postgresql、SQL-92的SQL的完整支持,这是一个手写的高性能SQL Parser,支持Visitor模式,使得分析SQL的抽象语法树很方便。
简单SQL语句用时10微秒以内,复杂SQL用时30微秒。
通过Druid提供的SQL Parser可以在JDBC层拦截SQL做相应处理,比如说分库分表、审计等。Druid防御SQL注入攻击的WallFilter就是通过Druid的SQL Parser分析语义实现的。
2.3.实现Spring对MyBatis的整合
log4j配置
log4j.rootLogger=DEBUG,CONSOLE,file #log4j.rootLogger=ERROR,ROLLING_FILE log4j.logger.cn.cvs.dao=debug log4j.logger.com.ibatis=debug log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=debug log4j.logger.com.ibatis.common.jdbc.ScriptRunner=debug log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate=debug log4j.logger.java.sql.Connection=debug log4j.logger.java.sql.Statement=debug log4j.logger.java.sql.PreparedStatement=debug log4j.logger.java.sql.ResultSet=debug log4j.logger.org.tuckey.web.filters.urlrewrite.UrlRewriteFilter=debug ################################################################################## # Console Appender 日志在控制输出配置 ################################################################################## log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender log4j.appender.Threshold=error log4j.appender.CONSOLE.Target=System.out log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern= [%p] %d %c - %m%n ################################################################################## # DailyRolling File 每天产生一个日志文件,文件名格式:log2021-12-11 ################################################################################## log4j.appender.file=org.apache.log4j.DailyRollingFileAppender log4j.appender.file.DatePattern=yyyy-MM-dd log4j.appender.file.File=log.log log4j.appender.file.Append=true log4j.appender.file.Threshold=error log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{yyyy-M-d HH:mm:ss}%x[%5p](%F:%L)%m%n log4j.logger.com.opensymphony.xwork2=error
mybatis配置
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--类型别名 --> <typeAliases> <package name="cn.cvs.pojo" /> </typeAliases> </configuration>
spring配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd "> <!--配置数据源--> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/cvs_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root" /> <property name="password" value="root" /> </bean> <!-- 配置SqlSessionFactoryBean --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 引用数据源组件 --> <property name="dataSource" ref="dataSource" /> <!-- 引用MyBatis配置文件中的配置 --> <property name="configLocation" value="classpath:mybatis-config.xml" /> <!-- 配置SQL映射文件信息 --> <property name="mapperLocations"> <list> <value>classpath:cn/cvs/dao/*/*.xml</value> </list> </property> </bean> <!-- 配置SqlSessionTemplate --> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean> <!-- 配置DAO --> <bean id="sysUserMapper" class="cn.cvs.dao.sysUser.SysUserMapperImpl"> <property name="sqlSession" ref="sqlSessionTemplate" /> </bean> <!-- 配置业务Bean --> <bean id="sysUserService" class="cn.cvs.service.sysUser.SysUserServiceImpl"> <property name="sysUserMapper" ref="sysUserMapper" /> </bean> </beans>
pojo.SysUser实体类
package cn.cvs.pojo; import java.util.Date; //用户实体类 public class SysUser { private Integer id; //id private String account; //用户编码 private String realName; //用户名称 private String password; //用户密码 private Integer sex; //性别 private Date birthday; //出生日期 private String phone; //电话 private String address; //地址 private Integer roleId; //用户角色ID private Integer createdUserId; //创建者 private Date createdTime; //创建时间 private Integer updatedUserId; //更新者 private Date updatedTime; //更新时间 private Integer age;//年龄 private String roleIdName; //用户角色名称 //省略getter/setter .... } }
dao接口
package cn.cvs.dao; import cn.cvs.pojo.SysUser; import java.util.List; //dao接口 public interface SysUserMapper { /** * 查询用户列表(参数:对象入参) * @param sysUser * @return */ public List<SysUser> selectSysUserList(SysUser sysUser); }
dao.impl
ackage cn.cvs.dao; import java.util.List; import cn.cvs.pojo.SysUser; import org.mybatis.spring.SqlSessionTemplate; //用户数据层实现 public class SysUserMapperImpl implements SysUserMapper { private SqlSessionTemplate sqlSession; @Override public List<SysUser> selectSysUserList(SysUser sysUser) { return sqlSession.selectList( "cn.cvs.dao.SysUserMapper.selectSysUserList", sysUser); } //getter public SqlSessionTemplate getSqlSession() { return sqlSession; } //setter public void setSqlSession(SqlSessionTemplate sqlSession) { this.sqlSession = sqlSession; } }
mapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.cvs.dao.SysUserMapper"> <!-- 当数据库中的字段信息与对象的属性不一致时需要通过resultMap来映射 --> <resultMap type="SysUser" id="SysUserResult"> <result property="roleIdName" column="roleName"/> </resultMap> <!-- 查询用户列表-roleId为null时查询到0条数据 --> <select id="selectSysUserList" parameterType="SysUser"resultMap="SysUserResult"> select u.*,r.roleName from t_sys_user u,t_sys_role r where u.roleId = r.id and u.roleId = #{roleId} and u.realName like CONCAT ('%',#{realName},'%') </select> </mapper>
service
package cn.cvs.service; import cn.cvs.pojo.SysUser; import java.util.List; //业务接口 public interface SysUserService { public List<SysUser> getList(SysUser sysUser); }
service.impl
package cn.cvs.service; import cn.cvs.dao.SysUserMapper; import cn.cvs.pojo.SysUser; import java.util.List; //业务实现类 public class SysUserServiceImpl implements SysUserService { private SysUserMapper sysUserMapper; @Override public List<SysUser> getList(SysUser sysUser) { try { return sysUserMapper.selectSysUserList(sysUser); } catch (RuntimeException e) { e.printStackTrace(); throw e; } } public SysUserMapper getSysUserMapper() { return sysUserMapper; } public void setSysUserMapper(SysUserMapper sysUserMapper) { this.sysUserMapper = sysUserMapper; } }
测试类
package cn.cvs.test; import java.util.ArrayList; import java.util.List; import cn.cvs.pojo.SysUser; import org.apache.log4j.Logger; import org.junit.Before; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import cn.cvs.service.SysUserService; public class SysUserTest { private Logger logger = Logger.getLogger(SysUserTest.class); @Test public void testGetUserList() { ApplicationContext ctx = new ClassPathXmlApplicationContext( "applicationContext.xml"); SysUserService userService = (SysUserService) ctx.getBean("sysUserService"); List<SysUser> userList = new ArrayList<SysUser>(); SysUser sysUser = new SysUser(); sysUser.setRealName("赵"); sysUser.setRoleId(2); userList = userService.getList(sysUser); for (SysUser user : userList) { logger.debug("realName: "+ user.getRealName()); } } }
案例1中存在的问题描述:
- 在roleId为null时,期望可以正常查询到realName中包含"赵"的用户,但是查询并没有获取正确的结果。这是什么原因造成的呢?
分析原因:
- 将控制台输出的SQL语句与参数拼接,会发现其中包含过滤语句 and u.roleId = null,表示查询要满足条件roleId = null的数据。显然不符合最初的需求。
如何解决:
- 使用if标签改造
三、映射器整合方式
3.1.映射器整合方式
- MyBatis中可以使用SqlSession的
getMapper(Class<T> type)
方法,根据指定的映射器和映射文件直接生成实现类- MyBatis-Spring提供的MapperFactoryBean能够以配置的方式生成映射器的实现类
<bean id="sysUserMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <!--指定映射器,只能是接口类型--> <property name="mapperInterface" value="cn.cvs.dao.SysUserMapper"/> <!-- 注入SqlSessionFactory以提供SqlSessionTemplate实例--> <property name="sqlSessionFactory" ref="sqlSessionFactory"/> </bean>
- 注意: 映射器对应的SQL映射文件与映射器的类路径相同,该映射文件可以自动被MapperFactoryBean解析
3.2.使用MapperFactoryBean注入映射器
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd "> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://127.0.0.1:3306/cvs_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root" /> <property name="password" value="root" /> </bean> <!-- 配置SqlSessionFactoryBean --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 引用数据源组件 --> <property name="dataSource" ref="dataSource" /> <!-- 引用MyBatis配置文件中的配置 --> <property name="configLocation" value="classpath:mybatis-config.xml" /> <!-- 配置SQL映射文件信息 --> <property name="mapperLocations"> <list> <value>classpath:cn/cvs/dao/*/*.xml</value> </list> </property> </bean> <!-- 配置DAO --> <bean id="sysUserMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="cn.cvs.dao.sysUser.SysUserMapper"/> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean> <!-- 配置业务Bean --> <bean id="sysUserService" class="cn.cvs.service.sysUser.SysUserServiceImpl"> <property name="sysUserMapper" ref="sysUserMapper" /> </bean> </beans>
3.3.映射器整合方式
问题:
- 如果映射器很多的话,相应的配置项也会很多,如何简化配置工作量?
分析:
- 使用MapperScannerConfigurer注入映射器
- 自动扫描指定包下的Mapper接口,并将它们直接注册为MapperFactoryBean
3.4.映射器整合方式3-3
MapperScannerConfigurer注入映射器
- MapperScannerConfigurer递归扫描基准包下所有接口,若它们在SQL映射文件中定义过,则动态注册为映射器实现类
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="cn.cvs.dao"/> </bean> <context:component-scan base-package="cn.cvs.service">
@Service("sysUserService") public class SysUserServiceImpl implements SysUserService { //MapperScannerConfigurer与@Autowired或@Resource注解配合使用,自动创建映射器实现并注入业务组件 @Autowired // @Resource private SysUserMapper sysUserMapper; //……代码省略 }
四、配置声明式事务
4.1.配置声明式事务
问题:
- 如何在业务方法中进行事务控制?
分析:
- 硬编码方式
- 弊端
- 事务相关代码分散在业务方法中难以重用
- 复杂事务的编码难度高,增加了开发难度
- Spring基于AOP实现声明式事务处理机制
- 优势
- 全在配置文件完成,将事务处理与业务代码分离,降低了开发和维护难度
4.2.配置声明式事务
- 声明式事务关注的核心问题是
- 对哪些方法,采取什么样的事务策略
配置步骤
- 导入命名空间
- 定义事务管理器
- 设置事务属性
- 定义事务切面
五、事务属性说明
5.1.事务属性说明
propagation:事务传播机制
REQUIRED(默认值) : 如果存在一个事务则支持当前事务;如果当前没有事务,则开启一个新的事务。该值可满足大多数的事务需求,可以作为首选的事务传播行为
REQUIRES_NEWS :总是开启一个新的事务,如果已存在,则先挂起,然后开启新的事务执行该方法
MANDATORY:如果存在一个事务则支持当前事务;如果当前没有事务,则抛出异常
NESTED:如果当前存在一个活动事务,则创建一个事务作为当前事物的嵌套事务运行,如果没有当前事务,该取值与REQUIRED相同。
SUPPORTS:如果存在一个事务,则支持当前事务,如果当前没有事务,则按非事务方式执行。
NOT_SUPPORTED:总以非事务方式执行,如果已存在,则先挂起,然后开启新的事务执行该方法
NEVER :总以非事务方式执行如果存在一个活动事务,则抛出异常。
isolation:事务隔离级别
- DEFAULT(默认值)
- READ_UNCOMMITTED (读未提交)
- READ_COMMITTED (提交已读)
- REPEATABLE_READ (可重复读)
- SERIALIZABLE (串行读)
5.2.事务属性说明
timeout:事务超时时间
- 允许事务运行的最长时间,以秒为单位
- 超过给定的时间自动回滚,防止事务执行时间过长而影响系统性能
- 在此我们分析下DataSourceTransactionManager;首先开启事物会调用其doBegin方法:
@Override protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; try { if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { Connection newCon = obtainDataSource().getConnection(); if (logger.isDebugEnabled()) { logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); } txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } txObject.getConnectionHolder().setSynchronizedWithTransaction(true); con = txObject.getConnectionHolder().getConnection(); Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); txObject.setReadOnly(definition.isReadOnly()); // Switch to manual commit if necessary. This is very expensive in some JDBC drivers, // so we don't want to do it unnecessarily (for example if we've explicitly // configured the connection pool to set it already). if (con.getAutoCommit()) { txObject.setMustRestoreAutoCommit(true); if (logger.isDebugEnabled()) { logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); } con.setAutoCommit(false); } prepareTransactionalConnection(con, definition); txObject.getConnectionHolder().setTransactionActive(true); int timeout = determineTimeout(definition); if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { //======================== txObject.getConnectionHolder().setTimeoutInSeconds(timeout); } // Bind the connection holder to the thread. if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); } } catch (Throwable ex) { if (txObject.isNewConnectionHolder()) { DataSourceUtils.releaseConnection(con, obtainDataSource()); txObject.setConnectionHolder(null, false); } throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); } }
- 其中determineTimeout用来获取我们设置的事务超时时间;然后设置到ConnectionHolder对象上(其是ResourceHolder子类),接着看ResourceHolderSupport的setTimeoutInSeconds实现:
/** * Set the timeout for this object in seconds. * @param seconds number of seconds until expiration */ public void setTimeoutInSeconds(int seconds) { setTimeoutInMillis(seconds * 1000L); } /** * Set the timeout for this object in milliseconds. * @param millis number of milliseconds until expiration */ public void setTimeoutInMillis(long millis) { this.deadline = new Date(System.currentTimeMillis() + millis); }
- 可以看到,其会设置一个deadline时间;用来判断事务超时时间的;那什么时候调用呢?首先检查该类中的代码,会发现:
/** * Return the time to live for this object in seconds. * Rounds up eagerly, e.g. 9.00001 still to 10. * @return number of seconds until expiration * @throws TransactionTimedOutException if the deadline has already been reached */ public int getTimeToLiveInSeconds() { double diff = ((double) getTimeToLiveInMillis()) / 1000; int secs = (int) Math.ceil(diff); checkTransactionTimeout(secs <= 0); return secs; } /** * Return the time to live for this object in milliseconds. * @return number of milliseconds until expiration * @throws TransactionTimedOutException if the deadline has already been reached */ public long getTimeToLiveInMillis() throws TransactionTimedOutException{ if (this.deadline == null) { throw new IllegalStateException("No timeout specified for this resource holder"); } long timeToLive = this.deadline.getTime() - System.currentTimeMillis(); checkTransactionTimeout(timeToLive <= 0); return timeToLive; } /** * Set the transaction rollback-only if the deadline has been reached, * and throw a TransactionTimedOutException. */ private void checkTransactionTimeout(boolean deadlineReached) throws TransactionTimedOutException { if (deadlineReached) { setRollbackOnly(); throw new TransactionTimedOutException("Transaction timed out: deadline was " + this.deadline); } }
会发现在调用getTimeToLiveInSeconds和getTimeToLiveInMillis,会检查是否超时,如果超时设置事务回滚,并抛出TransactionTimedOutException异常。到此我们只要找到调用它们的位置就好了,那什么地方调用的它们呢? 最简单的办法使用如“IntelliJ IDEA”中的“Find Usages”找到get***的使用地方;会发现:
DataSourceUtils.applyTransactionTimeout会调用DataSourceUtils.applyTimeout,
- DataSourceUtils.applyTimeout代码如下:
public static void applyTimeout(Statement stmt, @Nullable DataSource dataSource, int timeout) throws SQLException { Assert.notNull(stmt, "No Statement specified"); ConnectionHolder holder = null; if (dataSource != null) { holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); } if (holder != null && holder.hasTimeout()) { // Remaining transaction timeout overrides specified value. stmt.setQueryTimeout(holder.getTimeToLiveInSeconds()); } else if (timeout >= 0) { // No current transaction timeout -> apply specified value. stmt.setQueryTimeout(timeout); } }
结论:Spring事务超时 = 事务开始时到最后一个Statement创建时时间 + 最后一个Statement的执行时超时时间(即其queryTimeout)。
read-only:事务是否为只读
- 默认值为false,对于仅执行查询功能的事务设置为true,提高事务处理性能
rollback-for:设定能够触发回滚的异常类型
Spring默认只在抛出RuntimeException时才标识事务回滚
- 可以通过全限定类名自行指定需要回滚事务的异常
no-rollback-for:设定不触发回滚的异常类型
- Spring默认CheckedException不会触发事务回滚
- 可以通过全限定类名自行指定不需回滚事务的异常
六、使用注解实现声明式事务
6.1.使用注解实现声明式事务
在Spring配置文件中配置事务管理类,并开启注解处理事务功能
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!--事务管理器的id被定义为transactionManager,则<tx:annotation-driven/>不需要指定transaction-manager属性的值--> <tx:annotation-driven />
使用==@Transactional==为方法添加事务支持
@Transactional @Service("sysUserService") public class SysUserServiceImpl implements SysUserService { …… }
6.2.使用注解实现声明式事务
属性 类型 说明 propagation 枚举型:Propagation 可选的传播性设置。使用举例:@Transactional(propagation=Propagation.REQUIRES_NEW) isolation 枚举型:Isolation 可选的隔离性级别。使用举例:@Transactional(isolation=Isolation.READ_COMMITTED) timeout int型(以秒为单位) 事务超时。使用举例:@Transactional(timeout=10) readOnly 布尔型 是否为只读型事务。使用举例:@Transactional(readOnly=true)
6.3.使用注解实现声明式事务3-3
属性 类型 说明 rollbackFor 一组Class类的实例,必须是Throwable的子类 一组异常类,遇到时必须进行回滚。使用举例:@Transactional(rollbackFor={SQLException.class} ),多个异常可用英文逗号隔开 rollbackForClassName 一组Class类的实例,必须是Throwable的子类 一组异常类名,遇到时必须进行回滚。使用举例:@Transactional(rollbackForClassName={“SQLException”} ),多个异常可用英文逗号隔开 noRollbackFor 一组Class类的实例,必须是Throwable的子类 一组异常类,遇到时必须不回滚 noRollbackForClassName 一组Class类的实例,必须是Throwable的子类 一组异常类名,遇到时必须不回滚 示例代码
1、Mapper接口
package cn.cvs.dao.sysUser; import cn.cvs.pojo.SysUser; import java.util.List; public interface SysUserMapper { /** * 查询用户列表(参数:对象入参) * * @param sysUser * @return */ public List<SysUser> selectSysUserList(SysUser sysUser); /** * 保存用户 * * @param sysUser * @return */ public int add(SysUser sysUser); }
2、Mapper.xml映射文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.cvs.dao.sysUser.SysUserMapper"> <!-- 当数据库中的字段信息与对象的属性不一致时需要通过resultMap来映射 --> <resultMap type="SysUser" id="SysUserResult"> <result property="roleIdName" column="roleName"/> </resultMap> <!-- 查询用户列表-roleId为null时查询到0条数据 --> <select id="selectSysUserList" parameterType="cn.cvs.pojo.SysUser" resultMap="SysUserResult"> select u.*,r.roleName from t_sys_user u,t_sys_role r where u.roleId = r.id and u.roleId = #{roleId} and u.realName like CONCAT ('%',#{realName},'%') </select> <!-- 增加用户 --> <insert id="add" parameterType="SysUser"> insert into t_sys_user (account,realName,password,sex ,birthday,phone,address,roleId,createdUserId,createdTime) values (#{account},#{realName},#{password},#{sex},#{birthday} ,#{phone},#{address},#{roleId},#{createdUserId},#{createdTime}) </insert> </mapper>
3、service接口
package cn.cvs.service.sysUser; import cn.cvs.pojo.SysUser; import java.util.List; public interface SysUserService { List<SysUser> getList(SysUser sysUser); boolean add(SysUser sysUser); }
4、service实现类
package cn.cvs.service.sysUser; import cn.cvs.dao.sysUser.SysUserMapper; import cn.cvs.pojo.SysUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * 用户模块业务层实现 */ @Transactional @Service("sysUserService") public class SysUserServiceImpl implements SysUserService { @Autowired // @Resource private SysUserMapper sysUserMapper; @Override public List<SysUser> getList(SysUser sysUser) { try { return sysUserMapper.selectSysUserList(sysUser); } catch (RuntimeException e) { e.printStackTrace(); throw e; } } @Override public boolean add(SysUser sysUser) { boolean result = false; try { if (sysUserMapper.add(sysUser) == 1){ result = true; // 测试事物回滚时,打开注释 throw new RuntimeException(); } } catch (RuntimeException e) { e.printStackTrace(); throw e; } return result; } }
5、Spring核心配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/cvs_db?useUnicode=true &characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <!-- 配置SqlSessionFactoryBean --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 引用数据源组件 --> <property name="dataSource" ref="dataSource"/> <!-- 引用MyBatis配置文件中的配置 --> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean> <!-- 配置DAO --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> --> <property name="basePackage" value="cn.cvs.dao"/> </bean> <!-- 配置业务Bean --> <!-- <bean id="userService" class="cn.cvs.service.user.UserServiceImpl"> <property name="userMapper" ref="userMapper" /> </bean> --> <context:component-scan base-package="cn.cvs.service"/> <!--1.定义事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!--2.启注解处理事务功能--> <tx:annotation-driven/> <!--3.配置事务增强,设置事务属性--> <tx:advice id="txAdvice"> <tx:attributes> <tx:method name="get*" propagation="SUPPORTS"/> <tx:method name="add*" propagation="REQUIRED"/> <tx:method name="del*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <!-- 定义切面 --> <aop:config> <aop:pointcut id="serviceMethod" expression="execution(* cn.cvs.service..*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod"/> </aop:config> </beans>
6、测试类
package cn.cvs.test.sysUser; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import cn.cvs.pojo.SysUser; import org.apache.log4j.Logger; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import cn.cvs.service.sysUser.SysUserService; public class SysUserTest { private Logger logger = Logger.getLogger(SysUserTest.class); @Test public void testAddUser() throws ParseException { ApplicationContext ctx = new ClassPathXmlApplicationContext( "applicationContext.xml"); SysUserService userService = (SysUserService) ctx.getBean("sysUserService"); SysUser user = new SysUser(); user.setAccount("test001"); user.setRealName("测试用户001"); user.setPassword("1234567"); Date birthday =new SimpleDateFormat("yyyy-MM-dd").parse("2004-12-12"); user.setBirthday(birthday); user.setAddress("地址测试"); user.setSex(1); user.setPhone("13688783697"); user.setRoleId(1); user.setCreatedUserId(1); user.setCreatedTime(new Date()); boolean result = userService.add(user); logger.debug("testAdd result : " + result); } }
七、本章总结
扩展 配置druid数据源
druid的maven依赖配置
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.9</version> </dependency>
druid的数据源配置文件(database_druid.properties)
druid.driverClassName=com.mysql.cj.jdbc.Driver druid.url=jdbc:mysql://localhost:3306/cvs_db?useUnicode=true&allowPublicKeyRetrieval=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai druid.username=root druid.password=root druid.initialSize=10 druid.minIdle=6 druid.maxActive=50 druid.maxWait=60000 druid.timeBetweenEvictionRunsMillis=60000 druid.minEvictableIdleTimeMillis=300000 druid.validationQuery=SELECT 'x' druid.testWhileIdle=true druid.testOnBorrow=false druid.testOnReturn=false druid.poolPreparedStatements=false druid.maxPoolPreparedStatementPerConnectionSize=20 druid.filters=wall,stat
druid在Spring核心配置文件部分
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.aiden.mybatisspring.mapper,com.aiden.mybatisspring.service"></context:component-scan> <!--1.引入druid配置文件 database_druid.properties文件在resources下--> <context:property-placeholder location="classpath:database_druid.properties"></context:property-placeholder> <!--2.配置Druid数据源--> <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <!--高版本的Driver可以自动识别数据库 而不再需要指定具体是哪一个Driver了--> <property name="driverClassName" value="${druid.driverClassName}"/> <property name="url" value="${druid.url}"/> <property name="username" value="${druid.username}"/> <property name="password" value="${druid.password}"/> <!-- 初始化连接数量 --> <property name="initialSize" value="${druid.initialSize}"/> <!-- 最小空闲连接数 --> <property name="minIdle" value="${druid.minIdle}"/> <!-- 最大并发连接数 --> <property name="maxActive" value="${druid.maxActive}"/> <!-- 配置获取连接等待超时的时间 --> <property name="maxWait" value="${druid.maxWait}"/> <!--以下暂时可以不需要配置--> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}"/> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}"/> <property name="validationQuery" value="${druid.validationQuery}"/> <property name="testWhileIdle" value="${druid.testWhileIdle}"/> <property name="testOnBorrow" value="${druid.testOnBorrow}"/> <property name="testOnReturn" value="${druid.testOnReturn}"/> <!-- 打开PSCache,并且指定每个连接上PSCache的大小 如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。 --> <property name="poolPreparedStatements" value="${druid.poolPreparedStatements}"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="${druid.maxPoolPreparedStatementPerConnectionSize}"/> <!-- 配置监控统计拦截的filters --> <property name="filters" value="${druid.filters}"/> </bean> <!--3.配置SqlSessionFactoryBean--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--3.1引用数据源组件druidDataSource --> <property name="dataSource" ref="druidDataSource"></property> <!--3.2引用MyBatis配置文件中的配置 --> <property name="configLocation" value="classpath:mybatis-config2.xml"></property> <!--3.3设置别名--> <property name="typeAliasesPackage" value="com.aiden.mybatisspring.pojo"></property> <!--3.4配置SQL映射文件信息 --> <property name="mapperLocations"> <list> <value>classpath:com/aiden/mybatisspring/mapper/*.xml</value> </list> </property> </bean> <!--4.配置SqlSessionTemplate --> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"></constructor-arg> </bean> </beans>