事务的操作主要有三步:
通过@Transactional注解实现:一般用在业务层来控制事务,将多个数据访问操作控制在一个事务范围内,在执行方法之前开始事务,执行之后提交事务,发现异常回滚事务。
@Transactional注解书写位置:
说明:可以在application.yml配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息了
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。
@Transactional(rollbackFor=Exception.class)//指定出现何种异常都回滚事务。
属性值 | 含义 |
---|---|
REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… |
对于这些事务传播行为,我们只需要关注以下两个就可以了:
- REQUIRED(默认值)
- REQUIRES_NEW
例如:A方法里面有事务,B方法里面有事务,A方法调用B方法
AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。
面向指定的一个或多个方法进行编程,我们就称之为 面向切面编程。
实现步骤:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
AOP程序:TimeAspect
@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();
//执行原始方法
Object result = pjp.proceed();
//记录方法执行结束时间
long end = System.currentTimeMillis();
//计算方法执行耗时
log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);
return result;
}
}
1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
连接点指的是可以被aop控制的方法。例如:入门程序当中所有的业务方法都是可以被aop控制的方法。
2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
也就是这个共用方法。
3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用。
在AOP中通过切入点表达式来描述切入点, 即:通过切入点表达式设置给那些方法添加环绕。
假如:切入点表达式改为DeptServiceImpl.list(),此时就代表仅仅只有list这一个方法是切入点。只有list()方法在运行的时候才会应用通知。
4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
当通知和切入点结合在一起,就形成了一个切面。通过切面就能够描述当前aop程序需要针对于哪个原始方法,在什么时候执行什么样的操作。
切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
5. 目标对象:Target,通知所应用的对象
Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。
Spring中AOP的通知类型:
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
对于切入点表达式重复,我们可以使用抽取
Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。
新增一个方法,使用@PointCut注解,抽取公共的切入点表达式,需要的时候引用方法名就可以。
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){
}
//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法在执行时:发生异常
//后续代码不在执行
log.info("around after ...");
return result;
}
//后置通知
@After("pt()")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
1). 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
2). 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序
@Slf4j
@Component
@Aspect
@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
切入点表达式:
描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
首先我们先学习第一种最为常见的execution切入点表达式。
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?
的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
示例:
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
切入点表达式的语法规则:
*
号代替(任意返回值类型)*
号代替,代表任意包(一层包使用一个*
)..
配置包名,标识此包以及此包下的所有子包*
号代替,标识任意类*
号代替,表示任意方法*
配置参数,一个任意类型的参数..
配置参数,任意个任意类型的参数切入点表达式示例
省略方法的修饰符号
execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
使用*
代替返回值类型
execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
使用*
代替包名(一层包使用一个*
)
execution(* com.itheima.*.*.DeptServiceImpl.delete(java.lang.Integer))
使用..
省略包名
execution(* com..DeptServiceImpl.delete(java.lang.Integer))
使用*
代替类名
execution(* com..*.delete(java.lang.Integer))
使用*
代替方法名
execution(* com..*.*(java.lang.Integer))
使用 *
代替参数
execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))
使用..
省略参数
execution(* com..*.*(..))
注意事项:
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
切入点表达式的书写建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
//匹配DeptServiceImpl类中以find开头的方法
execution(* com.itheima.service.impl.DeptServiceImpl.find*(..))
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
execution(* com.itheima.service.DeptService.*(..))
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 …,使用 * 匹配单个包
execution(* com.itheima.*.*.DeptServiceImpl.find*(..))
要匹配多个无规则方法的时候,基于execution的切入点表达式就不是很方便。
实现步骤:
自定义注解:MyLog
@Target(ElementType.METHOD)//表示该注解只能作用在方法上
@Retention(RetentionPolicy.RUNTIME)//表示该注解将在运行时保留,在程序运行期间可以通过反射机制获取到这个注解的信息。
public @interface MyLog {
}
业务类:DeptServiceImpl
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
//模拟异常
//int num = 10/0;
return deptList;
}
}
切面类
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Before("@annotation(com.itheima.anno.MyLog)")
public void before(){
log.info("MyAspect6 -> before ...");
}
//后置通知
@After("@annotation(com.itheima.anno.MyLog)")
public void after(){
log.info("MyAspect6 -> after ...");
}
}
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
示例代码
@Slf4j
@Component
@Aspect
public class MyAspect7 {
@Pointcut("@annotation(com.itheima.anno.MyLog)")
private void pt(){}
//前置通知
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ...");
}
//后置通知
@Before("pt()")
public void after(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//获取目标类名
String name = pjp.getTarget().getClass().getName();
log.info("目标类名:{}",name);
//目标方法名
String methodName = pjp.getSignature().getName();
log.info("目标方法名:{}",methodName);
//获取方法执行时需要的参数
Object[] args = pjp.getArgs();
log.info("目标方法参数:{}", Arrays.toString(args));
//执行原始方法
Object returnValue = pjp.proceed();
return returnValue;
}
}