AOP:Aspect Oriented Programming,面向切面编程。是通过预编译方式(aspectj)或者运行期动态代理(Spring)实现程序功能的统一维护的技术。
AOP是OOP(面向对象编程)的技术延续,是软件开发中的一个热点,也是Spring中的一个重要内容。利用AOP可以实现对业务逻辑各个部分之间的隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时提高了开发效率。
作用:不修改源码的情况下,进行功能增强,通过动态代理实现的
优势:减少重复代码,提高开发效率,方便维护
比如:给功能增加日志输出, 事务管理的功能
10个方法: 想给10个方法都增加一种打印日志的功能,但是又不想(不能)改源码,此时可以给它使用AOP增强。
实际上,Spring的AOP,底层是通过动态代理实现的。在运行期间,通过代理技术动态生成代理对象,代理对象方法执行时进行功能的增强介入,再去调用目标方法,从而完成功能增强。
常用的动态代理技术有:
JDK的动态代理:基于接口实现的
cglib的动态代理:基于子类实现的
Spring的AOP采用了哪种代理方式?
如果目标对象有接口,就采用JDK的动态代理技术
如果目标对象没有接口,就采用cglib技术
小结
AOP是:在不修改源码的情况下,进行功能增强
AOP的本质是:动态代理
目标对象(Target):要代理的/要增强的目标对象。
代理对象(Proxy):目标对象被AOP织入增强后,就得到一个代理对象
连接点(JoinPoint):能够被拦截到的点,在Spring里指的是方法
目标类里,所有能够进行增强的方法,都是连接点
切入点(PointCut):要对哪些连接点进行拦截的定义
已经增强的连接点,叫切入点
通知/增强(Advice):拦截到连接点之后要做的事情
对目标对象的方法,进行功能增强的代码
切面(Aspect):是切入点和通知的结合
织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程。Spring采用动态代理技术织入,而AspectJ采用编译期织入和装载期织入
AOP开发前要明确的事项
我们要做的事情:
编写核心业务代码(Target目标类的目标方法)
编写通知类,通知类中有通知方法(Advice增强功能方法)
在配置文件中,配置织入关系,即将哪些通知与哪些切入点 结合,形成切面
Spring的AOP做的事情:
生成动态代理的过程(把通知织入到切入点的过程),是由Spring来实现的
Spring会监控切入点方法的执行,一旦发现切入点方法执行,使用代理机制动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
小结
AOP相关的概念/术语
目标类Target:要对哪个类进行增强
代理对象Proxy:对目标类增强后的那个代理对象
连接点JoinPoint:目标类里可增强的方法
切入点PointCut:要增强的方法
通知Advice:要增强的功能方法
切面Aspect:切入点 + 通知
织入Weaving:把切入点 和 通知 进行结合,生成代理对象的过程
使用AOP,我们要做的事情:
编写目标类,自己的业务代码
编写通知类
配置切面
使用AOP,Spring做的事情
创建maven项目,导入坐标
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.4version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>5.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
dependencies>
创建目标类和通知类
目标类:com.execise.aop.UserServiceImpl
package com.execise.service;
public interface UserService {
void add();
void update();
}
package com.execise.service.impl;
import com.execise.service.UserService;
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("调用了UserServiceImpl的add方法~!");
//int a = 1 / 0 ;
}
public void update() {
System.out.println("调用了UserServiceImpl的update方法~!");
}
}
通知类|增强:com.execise.aop.MyAdvice
package com.execise.advice;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAdvice {
public void print(){
System.out.println("打印日志~!");
}
}
修改配置文件
把目标类和通知类都配置到Spring配置文件中
配置切入和通知方法(增强方法)的织入关系
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
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">
<bean id="us" class="com.execise.service.impl.UserServiceImpl"/>
<bean id="myAdvice" class="com.execise.advice.MyAdvice"/>
<aop:config>
<aop:aspect ref="myAdvice">
<aop:before method="print" pointcut="execution(* com.execise.service.impl.UserServiceImpl.add())"/>
aop:aspect>
aop:config>
beans>
注意:在xml中增加了aop的名称空间如下:
测试代码
package com.execise.test;
import com.execise.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestUserServiceImpl {
@Autowired
private UserService us;
@Test
public void testAdd(){
us.add();
}
@Test
public void testUpdate(){
us.update();
}
}
步骤小结
导入jar包:spring-context, aspectjweaver
编写目标类、编写通知类
配置切面
<aop:config>
<aop:aspect ref="通知对象">
<aop:before method="通知对象里的通知方法" pointcut="切入点表达式"/>
aop:aspect>
aop:config>
1) 切点表达式的写法
execution([权限修饰符] 返回值类型 包名.类名.方法名(参数列表))
修饰符:可以省略
返回值类型:
可以指定类型。比如String
(如果类型有歧义,就写全限定类名,比如:java.util.Date
)
*
,表示任意字符。比如Str*
,或者*
包名:
可以写.
:表示当前包下的类或者子包。比如com.execise.service
可以写..
:表示当前包里所有后代类、后代包。比如com..service
*
:表示任意字符。比如:com.it*
, com.*
类名:
可以指定类名。比如:UserServiceImpl
*
表示任意字符。比如:*ServiceImpl
,*
方法名:
可以指定方法名
*
表示任意字符。比如:save*
,*
参数列表:
可以指定类型。比如:String,Integer
表示第一个参数是String,第二个参数是Integer类型
*
表示任意字符。比如:
String, *
表示第一个参数是String,第二个参数是任意类型
Str*, Integer
表示第一个参数类型Str开头,第二个参数是Integer类型
可以使用..
表示任意个数、任意类型的参数
示例
execution(public void com.execise.dao.impl.UserDao.save())
execution(void com.execise.dao.impl.UserDao.*(..))
execution(* com.execise.dao.impl.*.*(..))
execution(* com.execise.dao..*.*(..))
execution(* *..*.*(..)) --不建议使用
<aop:config>
<aop:aspect ref="myAdvice">
<aop:before method="print" pointcut="execution(* com.execise..*.*(..))"/>
aop:aspect>
aop:config>
2) 通知的种类
通知的语法
<aop:通知类型 method="通知中的方法" pointcut="切点表达式">aop:通知类型>
通知的类型
名称 | 标签 | 说明 |
---|---|---|
前置通知 |
| 通知方法在切入点方法之前执行 |
后置通知 |
| 在切入点方法正常执行之后,执行通知方法 |
异常通知 |
| 在切入点方法抛出异常时,执行通知方法 |
最终通知 |
| 无论切入点方法是否有异常,最终都执行通知方法 |
环绕通知 |
| 通知方法在切入点方法之前、之后都执行 |
通知示例
注意:通知方法的名称随意,我们这里是为了方便理解,才起名称为:before, after等等
前置通知
通知方法定义MyAdvice
的before
方法:
public void before(){
System.out.println("前置通知");
}
xml配置
<aop:before method="before"
pointcut="execution(* com.execise.service..*.*())"/>
后置通知
通知方法定义
public void afterReturning(){
System.out.println("后置通知");
}
xml配置
<aop:after-returning method="afterReturning"
pointcut="execution(* com.execise.service..*.*())"/>
环绕通知
通知方法定义
/*
环绕增强比较特殊一些,它需要我们在增强的方法里面手动调用目标方法。
*/
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
//System.out.println("环绕:打印日志~!");
before();
//调用目标方法
//joinPoint.proceed(); //目标方法没有参数的调用
joinPoint.proceed(joinPoint.getArgs()); //目标方法有参的方式调用
afterReturning();
}
xml配置
<aop:around method="around"
pointcut="execution(* com.execise.service..*.*())"/>
异常抛出通知
通知方法定义
public void afterThrowing(){
System.out.println("抛出异常通知");
}
xml配置
<aop:after-throwing method="afterThrowing"
pointcut="execution(* com.execise.service..*.*())"/>
最终通知
通知方法定义
public void after(){
System.out.println("最终通知");
}
xml配置
3) 切点表达式的抽取
当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;在增强中使用pointcut-ref代替pointcut,来引入切入点表达式。
示例:
<aop:config>
<aop:aspect ref="myAdvice">
<aop:pointcut id="pointCut01" expression="execution(* com.execise..*.*(..))"/>
<aop:before method="before" pointcut-ref="pointCut01"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointCut01"/>
aop:aspect>
aop:config>
- 小结
需要我们编写的内容:
编写目标类,编写通知(增强)类
配置切面
<aop:config>
<aop:pointcut id="xxx" expression="切入点表达式"/>
<aop:aspect ref="通知对象">
<aop:before method="通知对象里的通知方法" pointcut-ref="xxx"/>
<aop:after-returning method="通知对象里的通知方法" pointcut-ref="xxx"/>
<aop:after-throwing method="通知对象里的通知方法" pointcut-ref="xxx"/>
<aop:after method="通知对象里的通知方法" pointcut-ref="xxx"/>
<aop:around method="通知对象里的通知方法" pointcut-ref="xxx"/>
aop:aspect>
aop:config>
注意环绕通知的方法
public Object aroundMethod(ProceedingJoinPoint pjp){
Object reuslt = null;
try{
//写前置通知代码
//调用目标对象的方法
result = pjp.proceed(pjp.getArgs());
//写后置通知代码
}catch(Throwable t){
//写异常通知代码
}finally{
//写最终通知代码
}
}
创建maven项目,导入坐标
注意:需要增加AOP的实现包:aspectjweaver
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.4version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>5.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
dependencies>
创建目标类,创建通知类
使用注解标注两个类,配置成为bean对象
@Repository
, @Service
, @Controller
注解,按照分层进行配置在通知类中,使用注解配置织入关系
com.execise.aop.Target
package com.execise.service;
public interface UserService {
void add();
void update();
}
package com.execise.service.impl;
import com.execise.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("调用了UserServiceImpl的add方法~!~");
//int a = 1 / 0 ;
}
public void update() {
System.out.println("调用了UserServiceImpl的update方法~!~");
}
}
通知类com.execise.aop.MyAdvice
package com.execise.advice;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/*
MyAdvice是增强类,它需要做:
1. 把自己交给spring管理 , 打上注解@Component
2. 表示这个类是切面增强类,专门用来做增强的,打注解 @Aspect
3. 把什么方法用来做什么增强(增强谁),就在这个方法上面打注解
前置增强 === @Before
后置增强 === @AfterReturning
*/
@Component
@Aspect
public class MyAdvice {
@Before("execution(* com.execise..*.*(..))")
public void print(){
System.out.println("打印日志~");
}
...
}
开启组件扫描和AOP自动代理
在applicationContext.xml
中
<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"
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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.execise"/>
<aop:aspectj-autoproxy/>
beans>
测试
package com.execise.test;
import com.execise.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestUserServiceImpl {
@Autowired
private UserService us;
@Test
public void testAdd(){
us.add();
}
}
4) 步骤小结
创建功能类UserServiceImpl
创建增强类MyAdvice
给他们都打上注解
UserServiceImpl : @Service
MyAdvice : @Component @Aspect
在applicationContext.xml中打开开关
<context:component-scan base-package="com.execise"/>
<aop:aspectj-autoproxy/>
1) 通知的种类
通知的语法
@通知注解("切入点表达式")
通知的类型
名称 | 注解 | 说明 |
---|---|---|
前置通知 | @Before | 通知方法在切入点方法之前执行 |
后置通知 | @AfterRuturning | 通知方法在切入点方法之后执行 |
异常通知 | @AfterThrowing | 通知方法在抛出异常时执行 |
最终通知 | @After | 通知方法无论是否有异常,最终都执行 |
环绕通知 | @Around | 通知方法在切入点方法之前、之后都执行 |
注意:
注解方式配置的通知,执行顺序是:前置->最终->后置/异常
如果想要指定执行的顺序,就使用环绕通知 , 因为环绕增强是由我们手动控制的。
2) 切点表达式的抽取
同xml的AOP一样,当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;
抽取方法是:
在增强类(切面类,即被`@Aspect`标的类)上增加一个额外的方法,在方法上使用`@Pointcut`注解定义切入点表达式,
在增强注解中引用切入点表达式所在的方法
示例:
package com.execise.advice;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/*
MyAdvice是增强类,它需要做:
1. 把自己交给spring管理 , 打上注解@Component
2. 表示这个类是切面增强类,专门用来做增强的,打注解 @Aspect
3. 把什么方法用来做什么增强(增强谁),就在这个方法上面打注解
前置增强 === @Before
后置增强 === @AfterReturning
*/
@Aspect
@Component
public class MyAdvice {
//@Before("execution(* com.execise..*.*(..))")
public void print(){
System.out.println("打印日志~!");
}
//===============================================================
//这个abc方法的作用就是为了抽取切点表达式! ,并且这个abc方法不会被调用!
@Pointcut("execution(* com.execise..*.*(..))")
public void abc(){
System.out.println("调用abc方法了~!");
}
//@Before("execution(* com.execise..*.*(..))")
@Before("abc()")
public void before(){
System.out.println("前置:打印日志~!");
}
//@AfterReturning("execution(* com.execise..*.*(..))")
@AfterReturning("abc()")
public void afterReturning(){
System.out.println("后置:打印日志~!");
}
//@AfterThrowing("execution(* com.execise..*.*(..))")
public void afterThrowing(){
System.out.println("异常:打印日志~!");
}
//@After("execution(* com.execise..*.*(..))")
public void after(){
System.out.println("最终:打印日志~!");
}
/*
环绕增强比较特殊一些,它需要我们在增强的方法里面手动调用目标方法。
*/
//@Around("execution(* com.execise..*.*(..))")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
//System.out.println("环绕:打印日志~!");
before();
//调用目标方法
//joinPoint.proceed(); //目标方法没有参数的调用
joinPoint.proceed(joinPoint.getArgs()); //目标方法有参的方式调用
afterReturning();
}
}
3) 小结
在通知类上加注解@Aspect
,声明成一个切面
在通知类里方法上加注解@Before/@AfterReturning/@AfterThrowing/@After/@Around
,配置切入点表达式
在xml里开启aop的自动代理:
主要是把XML的配置,放到核心配置类上
使用 @EnableAspectJAutoProxy 来允许AOP的自动配置
核心配置类
package com.execise.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.execise")
@EnableAspectJAutoProxy
public class AppConfig {
}
package com.execise.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Before("execution(* com.execise..*.*(..))")
public void print(){
System.out.println("打印日志~");
}
}
UserService接口
package com.execise.service;
public interface UserService {
void add();
void update();
}
UserServiceImpl实现类
package com.execise.service.impl;
import com.execise.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("调用了UserServiceImpl的add方法~!~");
}
public void update() {
System.out.println("调用了UserServiceImpl的update方法~!~");
}
}
单元测试
package com.execise.test;
import com.execise.config.AppConfig;
import com.execise.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class TestUserServiceImpl02 {
@Autowired
private UserService us;
@Test
public void testAdd(){
us.add();
}
}
【第一步】编写通知类
@Component
@Aspect
public class ProjectAdvice {
//匹配业务层的所有方法
@Pointcut("execution(* com.execise.service.*Service.*(..))")
private void servicePt(){}
//设置环绕通知,在原始操作的运行前后记录执行时间
@Around("ProjectAdvice.servicePt()") //本类类名可以省略不写
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
//获取执行的签名对象
Signature signature = pjp.getSignature();
//获取接口/类全限定名
String className = signature.getDeclaringTypeName();
//获取方法名
String methodName = signature.getName();
//记录开始时间
long start = System.currentTimeMillis();
//执行万次操作
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
//记录结束时间
long end = System.currentTimeMillis();
//打印执行结果
System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
}
}
【第二步】在SpringConfig配置类上开启AOP注解功能
@Configuration
@ComponentScan("com.execise")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
@EnableAspectJAutoProxy //开启AOP注解功能
public class SpringConfig {
}
【第三步】运行测试类,查看结果
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
@Autowired
private AccountService accountService;
@Test
public void testFindById(){
Account account = accountService.findById(2);
}
@Test
public void testFindAll(){
List<Account> list = accountService.findAll();
}
}
所谓事务管理,即:按照给定的事务规则,来执行提交或回滚操作。其中:
“给定的事务规则”:用TransactionDefinition
表示
“按照…来执行提交或回滚操作”:用PlatformTransactionManager
来完成
TransactionStatus
用于表示一个运行着的事务的状态
关于编程式事务的说明
编程式事务管理:通过编写代码的方式实现事务管理
编程式事务管理,因事务管理与业务功能耦合性太强,不方便维护,目前已经基本不用
spring 2.0 就已经提供了 xml配置的声明式事务管理的支持
如果想要了解Spring的编程式事务,可参考《资料/spring02_transaction_program》
以下API仅做介绍了解,用于了解Spring事务相关的API,并回顾事务相关的概念
PlatformTransactionManager
是Spring提供的事务管理器接口,它提供了我们常用的操作事务的方法:开启事务、提交事务等
注意:PlatformTransactionManager
是接口类型,不同的dao层技术有不同的实现,例如:
dao层是jdbcTemplate或Mybatis时,实现类是:DataSourceTransactionManager
dao层是Hibernate时,实现类是:HibernateTransactionManager
方法 | 返回值 | 说明 |
---|---|---|
getTransaction(TransactionDefinition td) | TransactionStatus | 开启事务,并得到事务状态 |
commit(TransactionStatus status) | 提交事务 | |
rollback(TransactionStatus status) | 回滚事务 |
TransactionDefinition
事务的定义信息对象,提供了以下常用方法:
方法 | 参数 | 返回值 | 说明 |
---|---|---|---|
getIsolationLevel() | int | 获取事务的隔离级别 | |
getPropogationBehavior() | int | 获取事务的传播行为 | |
getTimeout() | int | 获取超时时间 | |
isReadOnly() | boolean | 是否只读的事务 |
事务的隔离级别:
ISOLATION_DEFAULT
:默认事务隔离级别
MySql默认隔离级别:repeatable read
Oracle默认隔离级别:read committed
ISOLATION_READ_UNCOMMITTED
:读未提交–存在脏读、不可重复读、幻读
ISOLATION_READ_COMMITTED
:读已提交–存在不可重复读、幻读
ISOLATION_REPEATABLE_READ
:重复读–存在幻读
ISOLATION_SERIALIZABLE
:串行化–没有并发问题
事务的传播行为:
用于解决业务方法调用业务方法时,事务的统一性问题的
比如: A方法开启事务了之后,就调用了B方法,那么B方法是否也会被纳入事务管理的范畴呢?
以下三个,是要当前事务的
PROPAGATION_REQUIRED
:需要有事务。默认
如果有事务,就使用这个事务
如果没有事务,就创建事务。
PROPAGATION_SUPPORTS
:支持事务
如果有事务,就使用当前事务,
如果没有事务,就以非事务方式执行(没有事务)
PROPAGATION_MANDATORY
:强制的
如果有事务,就使用当前事务
如果没有事务,就抛异常
以下三个,是不要当前事务的
PROPAGATION_REQUIRES_NEW
:新建的
如果有事务,就把事务挂起,再新建事务
如果没有事务,新建事务
PROPAGATION_NOT_SUPPORTED
:不支持的
如果有事务,就把事务挂起,以非事务方式执行
如果没有事务,就以非事务方式执行
PROPAGATION_NEVER
:非事务的
如果有事务,就抛异常
如果没有事务,就以非事务方式执行
最后一个,是特殊的
PROPAGATION_NESTED
:嵌套的
如果有事务,就在事务里再嵌套一个事务执行
如果没有事务,就是类似REQUIRED
的操作
事务运行的超时时间:
超时后事务自动回滚
默认值-1,表示没有超时限制
如果有,可以以秒为单位进行设置
是否只读:
如果设置为只读,那么方法只能查询,不能增删改
通常是查询方法设置为只读
TransactionStatus
方法 | 返回值 | 说明 |
---|---|---|
hasSavePoint() | boolean | 事务是否有回滚点 |
isCompleted() | boolean | 事务是否已经完成 |
isNewTransaction() | boolean | 是否是新事务 |
isRollbackOnly() | boolean | 事务是否是 要回滚的状态 |
小结
PlatformTransactionManager接口:
如果dao层用的是Mybatis、JdbcTemplate:用DataSourceTransactionManager
如果dao层用的是Hibernate:用HibernateTransactionManager
事务定义信息:
事务的隔离级别:通常使用默认ISOLATION_DEFAULT
事务的传播行为:通常使用默认PROPAGATION_REQUIRED
事务的超时时间:如果事务执行超时,会回滚。单位是秒。值为-1表示永不超时
事务是否是只读:如果只读,事务里只能执行查询操作,不能增删改
转账功能的环境准备
zs给ls转账,不带事务的功能实现,为后边的事务控制做准备
1) 创建Maven项目,导入依赖坐标
dao层技术要使用MyBatis
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.23version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.6version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>2.0.6version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>1.7.20version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-coreartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>5.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.4version>
dependency>
dependencies>
2) 创建dao,Service:
AccountDao
接口:
package com.execise.dao;
import org.apache.ibatis.annotations.Param;
public interface AccountDao {
void kouqian(@Param("from") String from ,@Param("money") int money);
void jiaqian(@Param("to") String to ,@Param("money") int money);
}
AccountDao.xml
映射文件:
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.execise.dao.AccountDao">
<update id="kouqian">
update account set money = money - #{money} where name = #{from}
update>
<update id="jiaqian">
update account set money = money + #{money} where name = #{to}
update>
mapper>
AccountService
和AccountServiceImpl
package com.execise.service;
public interface AccountService {
void transfer(String from ,String to , int money);
}
package com.execise.service.impl;
import com.execise.dao.AccountDao;
import com.execise.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/*
1. 把这个类交给spring管理
2. 注入进来dao的对象!
*/
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao dao;
/**
* 转账
* @param from
* @param to
* @param money
*/
public void transfer(String from, String to, int money) {
//扣钱
dao.kouqian(from ,money);
//加钱
dao.jiaqian(to , money);
}
}
3) 配置bean和依赖注入
applicationContext.xml
<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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.execise"/>
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driverClass}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.execise.bean"/>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.execise.dao"/>
bean>
beans>
4) 功能测试
package com.execise.test;
import com.execise.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestAccountServiceImpl {
@Autowired
private AccountService as;
@Test
public void testTransfer(){
as.transfer("zs", "ls" , 100);
}
}
什么是声明式事务控制
介绍:
声明式事务控制,是采用声明的方式进行事务管理。所谓的声明,指的就是在配置文件中进行配置。
通过声明式(配置)的方式来处理事务,代替编码式事务控制
作用:
事务管理不入侵开发的组件,松耦合
业务逻辑代码中,没有事务的代码,甚至不会意识到正在事务当中。
事实上也应该如此,业务逻辑代码只处理业务功能,事务控制是属于系统层面的服务;如果想要更改事务,只需要在配置文件中重新配置即可
能以模板的方式使用
易维护。
注意:Spring的声明式事务,底层就是AOP
基于XML的声明式事务控制
1) 需要明确的事项
谁是目标类?(哪个类想用事务) AccountserviceImpl
谁是切入点?(哪个方法想用事务 ) transfer
谁是通知(增强)?(给上面的方法增强什么功能) 事务管理
dao层技术是JdbcTemplate,事务的管理员使用DataSourceTransactionManager
2) 快速入门
通过Spring的xml配置,对银行转账功能,进行事务控制
实现步骤
只需要修改applicationContext.xml
即可:
在配置文件中增加aop和tx的名称空间
配置事务的通知(增强)
配置切面,把事务通知织入到转账方法中
功能实现
<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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
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">
<context:component-scan base-package="com.execise"/>
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driverClass}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.execise.bean"/>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.execise.dao"/>
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" isolation="DEFAULT" propagation="REQUIRED" read-only="false" timeout="-1"/>
<tx:method name="save*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
<tx:method name="edit*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
<tx:method name="delete*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
<tx:method name="query*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
<tx:method name="find*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
tx:attributes>
tx:advice>
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.execise..*.*(..))"/>
aop:config>
beans>
3) 配置详解
aop:config
:切面配置
这个标签的配置,就是为了找到方法,然后给这些方法应用上事务。
<aop:config>
<aop:advisor advice-ref="txAdvice"
pointcut="execution(* com.execise.service.impl..*.*(..))"/>
aop:config>
aop:config
:aop提供的用于配置切面的标签
aop:advisor
:Spring提供的专门用于配置事务的,作用类似于aop:aspect
advice-ref
:要引入的通知配置,必须要引用
所配置的事务通知
pointcut
:切入点表达式
tx:advice
:事务通知配置
id
属性:唯一标识
transaction-manager
属性:配置一个事务管理器,即PlatformTransactionManager
的实现类对象
类似于我们的自己编写的事务管理器,里边提供了事务管理的方法,例如:提交、回滚事务的方法等等
tx:attributes
:在标签内部设置事务的属性信息(事务定义信息,TransactionDefinition
)
tx:method
:要进行事务控制的方法配置,表示 要对哪些方法,进行什么样的事务控制
name
属性:要进行事务控制方法名称,可以使用通配符*
isolation
属性:事务的隔离级别设置
propagation
属性:事务传播特性
read-only
属性:是否只读
timeout
属性:超时时间。默认-1表示不限制,如果设置的话,单位是秒
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
<tx:method name="save*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
<tx:method name="edit*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
<tx:method name="delete*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
<tx:method name="query*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
<tx:method name="find*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
tx:attributes>
tx:advice>
4) 小结
service里的方法,不需要有任何事务管理相关的代码
只需要在xml里配置即可
<bean id="txManager" class="DataSourceTransactionManager全限定类名">
<property name="dataSource" ref="连接池"/>
bean>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="*"/>
tx:attributes>
tx:advice>
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="切入点表达式"/>
aop:config>
基于注解的声明式事务控制
1) 快速入门
通过Spring的注解配置,对银行转账功能,进行事务控制
实现步骤
在需要事务控制的方法/类上增加注解@Transactional
@Transactional //类里面的所有方法都有事务
@Service
public class AccountServiceImpl implements AccountService {
}
在配置文件applicationContext.xml
中修改配置
配置事务管理器
开启事务的注解驱动
<tx:annotation-driven transaction-manager="tm"/>
功能实现
修改银行转账的Service接口:AccountService接口
package com.execise.service;
import com.execise.bean.Account;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/*
注解事务的配置:
0.在applicationContext.xml中配置事务管理员bean
1. 在类上或者方法上打注解 @Transactional
1.1 在类身上打,即表示该类中的所有方法都会应用上事务
1.2 在方法身上打,即表示只有这个方法会应用上事务。
2. 在xml里面打开注解的开关
注意:事务注解可以打在业务层接口或实现类上,一般建议打在接口上,这样程序的耦合性更低一些
*/
//1. 类上打注解
//@Transactional
public interface AccountService {
//2.方法上打注解
//@Transactional
@Transactional(isolation = Isolation.DEFAULT , propagation = Propagation.REQUIRED , readOnly = false , timeout = -1)
void transfer(String from ,String to , int money);
}
修改配置文件applicationContext.xml
<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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
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">
<context:component-scan base-package="com.execise"/>
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driverClass}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.execise.bean"/>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.execise.dao"/>
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
<tx:annotation-driven />
beans>
2) 配置详解
注解@Transactional
加在 需要进行事务控制的方法/类上,用于代替xml配置中的tx:advice
和事务切面的aop:config
isolation
属性:设置事务的隔离级别,从枚举Isolation
中取值
propagation
属性:设置事务的传播特性,从枚举Propagation
中取值
readOnly
属性:设置是否是只读的
timeout
属性:设置超时时间,单位秒。-1表示不限制
开启事务的注解驱动
XML方式
applicationContext.xml
中开启 事务的注解驱动,否则无效
<tx:annotation-driven transaction-manager="txManager"/>
<tx:annotation-driver/>
纯注解方式
@EnableTransactionManagement
配置示例
package com.execise.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@ComponentScan("com.execise")
@PropertySource("classpath:db.properties")
@MapperScan("com.execise.dao")
//4.核心配置类上使用@EnableTransactionManagement注解开启事务管理
@EnableTransactionManagement
public class AppConfig {
@Value("${db.driverClass}")
private String driver;
@Value("${db.url}")
private String url;
@Value("${db.username}")
private String username;
@Value("${db.password}")
private String password;
//1.创建数据源bean 交由Spring管理
@Bean
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
//2.创建SqlSessionFactory bean 交由Spring管理
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.execise.bean");
return sqlSessionFactoryBean;
}
//3.声明事务管理员bean 交由Spring管理
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource);
return tm;
}
}
单元测试
package com.execise.test;
import com.execise.config.AppConfig;
import com.execise.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class TestAccountServiceImpl02 {
@Autowired
private AccountService as;
@Test
public void testTransfer(){
as.transfer("zs", "ls" , 100);
}
}
3) 小结
在xml文件里
<bean id="txManager" class="DataSourceTransactionManager全限定类名">
<property name="dataSource" ref="连接池"/>
bean>
<tx:annotation-driven transaction-manager="txManager"/>
<context:component-scan base-package="com.execise"/>
哪个方法需要事务管理,就在哪个方法上加注解:@Transactional
什么样的异常,Spring事务默认是不进行回滚的?
事务配置
说明:对于RuntimeException类型异常或者Error错误,Spring事务能够进行回滚操作。但是对于编译器异常,Spring事务是不进行回滚的,所以需要使用rollbackFor来设置要回滚的异常。
案例:转账业务追加日志
需求和分析
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩:A账户减钱,B账户加钱,数据库记录日志
分析:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能
实现效果预期:
无论转账操作是否成功,均进行转账操作的日志留痕
存在的问题:
日志的记录与转账操作隶属同一个事务,同成功同失败
实现效果预期改进:
无论转账操作是否成功,日志必须保留
事务传播行为:事务协调员对事务管理员所携带事务的处理态度
【准备工作】环境整备
USE day32;
CREATE TABLE tbl_log(
id INT PRIMARY KEY AUTO_INCREMENT,
info VARCHAR(255),
createDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
public interface LogService {
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional
void log(String from, String to, Integer money);
}
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
@Override
public void log(String from,String to,Integer money ) {
logDao.log("转账操作由"+from+"到"+to+",金额:"+money);
}
}
public interface LogDao {
@Insert("insert into tbl_log (info) values(#{info})")
void log(String info);
}
【第一步】在AccountServiceImpl中调用logService中添加日志的方法
package com.execise.service.impl;
import com.execise.bean.Account;
import com.execise.dao.AccountDao;
import com.execise.service.AccountService;
import com.execise.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
@Override
public void transfer(String from, String to, int money) {
try {
//扣钱
accountDao.kouqian(from ,money);
//测试异常
int i=1/0;
//加钱
accountDao.jiaqian(to , money);
} finally {
logService.log(from,to,money);
}
}
}
【第二步】在LogService的log()方法上设置事务的传播行为
public interface LogService {
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String from, String to, Integer money);
}
【第三步】运行测试类,查看结果
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService as;
@Test
public void testTransfer() throws IOException {
as.transfer("zs","ls",200);
}
}
事务传播行为