文章目录
Spring学习记录(二)——Spring结合MyBatis、事务
在Spring框架中结合MyBatis框架访问数据库
一、Spring结合MyBatis开发步骤与实现
基本的实现步骤:
- 创建mysql,表结构Student(id,name,email,age)
- 新建maven
- 加入依赖
- spring依赖(spring-context)
- mybatis依赖(mybatis)
- mysql的驱动(mysql-connector-java)
- spring的事务支持(事务是自动提交)(spring-tx)
- 连接池,使用阿里公司的druid(druid)
- mybatis和spring一起使用的整合依赖(mybatis-spring)
- mybatis框架提供的一个jar,用来在spring项目中创建mybatis的SqlSessionFactory对象和dao对象
- 单元测试依赖(junit)
- 创建表对应的实体类Student
- 创建dao接口
- 创建mapper文件,写sql语句(也可以用注解直接在dao接口中写sql语句)
- 创建mybatis的主配置文件
- 创建service接口和他的实现类
- 创建spring的配置文件
- 声明数据源DataSource
- 声明SqlSessionFactoryBean,创建SqlSessionFactory对象
- 声明mybatis的扫描器对象,创建Dao对象(使用mybatis的动态代理sqlSession.getMapper(StudentDao.class))
- 声明自定义的service
- 测试
1. Maven的配置文件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.bjpowernode</groupId>
<artifactId>ch11-spring-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--spring框架依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
<!--spring有关事务的-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<!--mybatis和spring整合依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory><!--所在的目录-->
<includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. Spring配置文件applicationContext.xml
注意:mybatis创建出来的dao对象,默认为对象名为dao接口名的首字母小写,可以通过指定id值来修改创建出来的对象名
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd ">
<!--数据源DataSource,访问数据库的
内部是创建Connection对象-->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/springdb"/>
<property name="username" value="root"/>
<property name="password" value="123" />
</bean>
<!--声明SqlSessionFactoryBean,创建SqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--指定数据源-->
<property name="dataSource" ref="myDataSource" />
<!--指定mybatis主配置文件,经过测试发现,这个参数可以不指定-->
<property name="configLocation" value="classpath:mybatis.xml" />
</bean>
<!--声明mybatis的扫描器,创建dao对象,对象名为dao接口名的首字母小写-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定SqlSessionFactory-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<!--指定dao接口所在的包名,
MapperScannerConfigurer这个类会创建这个包中所有接口的dao对象-->
<property name="basePackage" value="com.bjpowernode.dao" />
</bean>
<!--创建扫描器,自动创建指定包下带有注解的Bean-->
<context:component-scan base-package="com.bjpowernode.service"/>
</beans>
3. Spring配置文件mybatis.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>
<!--配置mybatis日志-->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!--指定mapper文件的位置-->
<mappers>
<package name="com.bjpowernode.dao" />
</mappers>
</configuration>
与之前单独的MyBatis的程序相比,除了这些文件有区别外,其余都一样,详见:MyBatis学习记录(一)之实现基本的增删改查
二、事务
事务原本是数据库中的概念,在 Dao 层。但一般情况下,需要将事务提升到业务层,即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。
在 Spring 中通常可以通过以下两种方式来实现对事务的管理:
- 使用 Spring 的事务注解管理事务
- 使用 AspectJ 的 AOP 配置管理事务
1. Spring 事务管理 API
(1) 事务管理器接口(重点)
事务管理器是 PlatformTransactionManager
接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。接口中的方法主要有三个:
- 该接口有两个常用的实现类:
DataSourceTransactionManager
:使用 JDBC 或 MyBatis 进行数据库操作时使用。HibernateTransactionManager
:使用 Hibernate 进行持久化数据时使用。
- Spring 事务的默认回滚方式是:
- 发生运行时异常和 error 时回滚,发生受查(编译)异常时提交。 不过,对于受查异常,程序员也可以手工设置其回滚方式。
(2) 事务定义接口
事务定义接口 TransactionDefinition
中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作。
A、五个事务隔离级别常量(掌握)
这些常量均是以 ISOLATION_
开头。即形如 ISOLATION_XXX。
DEFAULT
: 采用 DB 默认的事务隔离级别。 MySql 的默认为 REPEATABLE_READ; Oracle默认为 READ_COMMITTED。READ_UNCOMMITTED
: 读未提交。未解决任何并发问题。READ_COMMITTED
: 读已提交。解决脏读,存在不可重复读与幻读。REPEATABLE_READ
: 可重复读。解决脏读、不可重复读,存在幻读SERIALIZABLE
: 串行化。不存在并发问题。
也许有很多读者会对上述隔离级别中提及到的 脏读、不可重复读、幻读 的理解有点吃力,我在这里尝试使用通俗的方式来解释这三种语义:
脏读:所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。也就是说,当前事务读到的数据是别的事务想要修改成为的但是没有修改成功的数据。
不可重复读:事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。也就是说,当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义。
可重复读:与不可重复读相反,两次读到的数据是相同的
幻读:事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读。也就是说,当前事务读第一次取到的数据比后来读取到数据条目少。
幻读与脏读的区别是,幻读是读取数据的条数出错,而脏读是读取数据的值出错
B、七个事务传播行为常量(掌握)
所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。通俗了说,就是说明方法是否有事务的,如果有事务是哪个事务,怎么执行。事务传播行为是加在方法上的。
事务传播行为常量都是以 PROPAGATION_
开头,形如 PROPAGATION_XXX。
- PROPAGATION_REQUIRED
- PROPAGATION_REQUIRES_NEW
- PROPAGATION_SUPPORTS
- PROPAGATION_MANDATORY
- PROPAGATION_NESTED
- PROPAGATION_NEVER
- PROPAGATION_NOT_SUPPORTED
- PROPAGATION_REQUIRED
指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。
如该传播行为加在 doOther()方法上。若 doSome()方法在调用 doOther()方法时就是在事务内运行的,则 doOther()方法的执行也加入到该事务内执行。若 doSome()方法在调用doOther()方法时没有在事务内执行,则 doOther()方法会创建一个事务,并在其中执行。 - PROPAGATION_REQUIRES_NEW
总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
- PROPAGATION_SUPPORTS
指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
C、定义了默认事务超时时限
常量 TIMEOUT_DEFAULT
定义了事务底层默认的超时时限, sql 语句的执行时长。
注意:事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。
2. 使用 Spring 的事务注解@Transactional
(掌握)
spring框架自己处理事务,使用的是TransactionInterceptor
类,这个类TransactionInterceptor
实现了MethodInterceptor
接口,是AOP中的环绕通知
当业务方法没有异常时, 使用事务管理器对象的commit()提交事务,原码:
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
当业务方法有异常时, 使用事务管理器对象的rollback()提交事务,原码:
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
使用注解的步骤:
- 在spring的配置文件中声明事务管理器对象使用mybatis框架,访问数据库,事务管理是对象是
DataSourceTransactionManager
- 在spring的配置文件中声明注解驱动,告诉spring框架,使用注解管理事务
<!--声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="myDataSource" />
</bean>
<!--开启事务注解驱动:告诉spring使用注解处理事务
transaction-manager:指定事务管理器对象的id-->
<tx:annotation-driven transaction-manager="transactionManager" />
- 在service的实现类的public方法上面加入
@Transactional
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
timeout = 20,
rollbackFor = {
NullPointerException.class,
NotEnoughException.class
}
@Override
public void buy(Integer goodsId, Integer nums) {}
- @Transactional:
@Transactional
若用在方法上,只能用于 public 方法上。Spring 会忽略掉所有非 public 方法上的@Transaction 注解。若@Transaction 注解在类上,则表示该类上所有的 public 方法均将在执行时织入事务。- 属性:
- propagation:传播行为,该属性类型为 Propagation 枚举,默认值为Propagation.REQUIRED
- isolation:隔离级别该属性类型为 Isolation 枚举,默认值为Isolation.DEFAULT。
- timeout:超时时间,单位为秒,类型为 int,默认值为-1,即没有时限。
- rollbackFor:异常类的class文件,放在
{}
中,表示方法中抛出这些异常时,一定执行回滚行为。- 当不指定rollbackFor参数时,程序会按照如下方式执行:
当你的方法中抛出异常时,框架首先检查异常和rollbackFor中的值有没有一样的,
1)如果有一定回滚,
2)如果没有, 则看异常是不是RuntimeException,如果是RuntimeException则回滚。
- 当不指定rollbackFor参数时,程序会按照如下方式执行:
- readOnly: 用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值为 false。
- 事务使用的spring的aop的环绕通知
- 当 @Transactional中不指定任何参数时,全使用默认值
- 属性:
// 全使用默认值
@Transactional
public void buy(Integer goodsId, Integer nums) {}
3. 使用 AspectJ 的 AOP 配置管理事务(掌握)
使用 XML 配置事务代理的方式要为每个目标类都配置事务代理。当目标类较多,配置文件会变得非常臃肿。使用 AOP 配置事务的方式可以自动为每个符合切入点表达式的类生成事务代理。
1. pom.xml中加入aspectj的依赖坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
2. 在容器中添加事务管理器
同注解配置
3. 配置业务方法的事务属性
<!--声明业务方法的事务属性(隔离级别,传播行为,超时)
id:自定义的唯一名称,表示advice的配置起个名称
-->
<tx:advice id="serviceTrans" transaction-manager="transactionManager">
<!--给方法配置事务
tx:attributes:设置事务属性
-->
<tx:attributes>
<!--指定业务方法事务属性
1.指定方法名称
2.使用带有通配符的方法名称, 通配符是“*”,表示任意字符的意思。
name:方法名称或使用通配符的方法名称
propagation:指定传播行为
isolation:隔离级别
read-only:对数据库的操作是不是只读的。 默认是false,不是只读的
rollback-for:发生异常执行回滚, 异常类的全限定名称
timeout:超时时间
-->
<tx:method name="buy" propagation="REQUIRED"
isolation="DEFAULT" read-only="false"
timeout="20"
rollback-for="java.lang.NullPointerException,com.bjpowernode.exception.NotEnoughException" />
<!--add开头方法的事务属性-->
<tx:method name="add*" propagation="REQUIRED" isolation="DEFAULT"
rollback-for="java.lang.RuntimeException" />
<!--modify开头的方法-->
<tx:method name="modify*" propagation="REQUIRED" isolation="DEFAULT" />
<!--remove开头的方法-->
<tx:method name="remove*" />
<!--其他方法-->
<tx:method name="*" propagation="SUPPORTS" isolation="DEFAULT" />
</tx:attributes>
</tx:advice>
为了方便使用通配符去给每一类操作指定事务,有如下规定:
- add开头的都是添加操作
- modify开头的都是修改操作
- remove开头的都是删除
- find, query,search等等开头的都是查询
4. 配置aop(切入点表达式和切面的设置)
<!--配置aop-->
<aop:config >
<!--设置切入点表达式
expression:切入点表达式,指定哪些包中的类
id:表达式的唯一名称
-->
<aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))" />
<!--配置增强器(事务通知+切入点表达式)-->
<aop:advisor advice-ref="serviceTrans" pointcut-ref="servicePt" />
</aop:config>
4. 使用事务趟过的坑(谨记!)
-
假设现在有个Service层的接口
UserService
和它的实现类UserServiceImpl
,在Service对象上加上注解:@Service。Controller对象中有个属性需要接收这个UserService- 不使用事务时,
@Service
生成的对象就是UserServiceImpl
对象,Controller
对象中Service
属性可以有以下两种定义
@Autowired private UserService userService; @Autowired private UserServiceImpl userService;
- 当使用事务时,Controller对象中Service属性只能使用接口来定义,使用接口会报
UnsatisfiedDependencyException
异常- 原因:使用事务后,
@Service
生成的对象已经不是原来的那个UserServiceImpl
对象了,而是使用了动态代理生成的UserService
接口实现类,所以这里只能使用UserService
接口,为了方便,还是统一使用接口来定义好了
- 原因:使用事务后,
@Autowired private UserService userService;
- 不使用事务时,
-
当没有
UserService
接口时,只有类UserServiceImpl
,有事务和没事务都使用这个类,因为如果有事务的话,使用的动态代理是CGLIB,这时的UserServiceImpl
就相当于一个父类@Autowired private UserServiceImpl userService;
三、web项目中使用Spring的监听器ContextLoaderListener
(掌握)
Servlet原来的代码:
//创建容器对象
String config= "spring.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
我们会发现,每刷新一次页面,就要重新读取一次配置文件,并 new 出一个新的 Spring 容器,将配置文件中的对象再创建一遍。
解决办法:
使用监听器在ServletContext刚初始化完成时,把创建好的容器对象,放入到ServletContext作用域中。
1. 加入依赖
新加入的依赖:
servlet依赖、jsp依赖、spring-web依赖:
<!--spring-web依赖: 有监听器的类-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
<!--servlet依赖-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!--jsp依赖-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2.1-b03</version>
<scope>provided</scope>
</dependency>
2. 注册spring的监听器
监听器启动时,会创建spring的容器对象, 默认读取配置文件的位置:/WEB-INF/spring.xml
可以自定义配置文件的位置和名称,需要使用context-param的设置
<context-param>
<!--ContextLoader(监听器父类)的源码中可以看到通过contextConfigLocation参数获取配置文件位置,所以这里要给这个参数赋值-->
<param-name>contextConfigLocation</param-name>
<!--spring配置文件的位置-->
<param-value>classpath:spring.xml</param-value>
</context-param>
<!--注册spring提供的监听器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
3. 从全局作用域对象中获取容器
1) 直接从 ServletContext 中获取
//从ServletContext作用域中,获取容器对象
String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
WebApplicationContext ctx = (WebApplicationContext)getServletContext().getAttribute(key);
2) 通过 WebApplicationContextUtils 工具类获取
//spring提供了工具方法,获取容器对象
WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());