2024-02-28-事务-AOP-黑马javaweb
事务管理
事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。这组操作要么同时成功,要么同时失败。
事务的操作主要有三步:
- 开启事务(一组操作开始前,开启事务):start transaction / begin ;
- 提交事务(这组操作全部成功后,提交事务):commit ;
- 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
spring事务管理
需求:当部门解散了不仅需要把部门信息删除了,还需要把该部门下的员工数据也删除了
直接在service层加上操作员工的mapper层方法
@Override
public void delete(Integer id) {
deptMapper.delete(id);
empMapper.deleteByDeptId(id);
}
实现mapper接口方法
@Delete("delete from emp where dept_id = #{id}")
void deleteByDeptId(Integer id);
问题:两次操作之间出现异常,即使程序运行抛出了异常,部门依然删除了,但是部门下的员工却没有删除,造成了数据的不一致
通过事务来实现,因为一个事务中的多个业务操作,要么全部成功,要么全部失败。
在方法运行之前,开启事务,如果方法成功执行,就提交事务,如果方法执行的过程当中出现异常了,就回滚事务。
@Transactional注解
@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。
@Transactional注解:一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
@Transactional注解书写位置:
- 方法
- 当前方法交给spring进行事务管理
- 类
- 当前类中所有的方法都交由spring进行事务管理
- 接口
- 接口下所有的实现类当中所有的方法都交给spring 进行事务管理
事务进阶
- 异常回滚的属性:rollbackFor 对哪些异常进行回滚
- 事务传播行为:propagation 事务之间相互调用时如何进行事务建立
rollbackFor
默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。
假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
@Transactional(rollbackFor = Exception.class) // 回滚所有的异常
propagation
当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制
两个事务方法,一个A方法,一个B方法。在这两个方法上都添加了@Transactional注解,就代表这两个方法都具有事务,而在A方法当中又去调用了B方法
所谓事务的传播行为,指的就是在A方法运行的时候,首先会开启一个事务,在A方法当中又调用了B方法, B方法自身也具有事务,那么B方法在运行的时候,到底是加入到A方法的事务当中来,还是B方法在运行的时候挂起方法A的事务,新建一个方法B事务?这个就涉及到了事务的传播行为。
- REQUIRED(默认值)需要事务,有则加入,无则创建新事务
- REQUIRES_NEW 总是创建新事务
**需求:**解散部门时需要记录操作日志
@Transactional(rollbackFor = Exception.class)
@Override
public void delete(Integer id) throws Exception {
try {
deptMapper.delete(id);
if (true) {
throw new Exception("异常出现");
}
empMapper.deleteByDeptId(id);
} finally {
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
deptLogService.insert(deptLog);
}
}
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional // 默认的,有事务就加入,没有就新建
@Override
public void insert(DeptLog log) {
deptLogMapper.insert(log);
}
}
日志没有记录成功,原因如下
-
在执行delete操作时开启了一个事务
-
当执行insert操作时,insert设置的事务传播行是默认值REQUIRED,表示有事务就加入,没有则新建事务
-
此时:delete和insert操作使用了同一个事务,同一个事务中的多个操作,要么同时成功,要么同时失败,所以当异常发生时进行事务回滚,就会回滚delete和insert操作
在DeptLogServiceImpl类中insert方法上,添加@Transactional(propagation = Propagation.REQUIRES_NEW)
Propagation.REQUIRES_NEW :不论是否有事务,都创建新事务 ,运行在一个独立的事务中。
AOP基础
AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),面向切面编程就是面向特定方法编程。
AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
需求:计算方法的运行时间
aop不会直接执行方法的逻辑,而是会执行我们所定义的 模板方法 , 然后再模板方法中:
- 记录方法运行开始时间
- 运行原始的业务方法(那此时原始的业务方法)
- 记录方法运行结束时间,计算方法执行耗时
AOP快速入门
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
我们通过AOP入门程序完成了业务方法执行耗时的统计,那其实AOP的功能远不止于此,常见的应用场景如下:
- 记录系统的操作日志
- 权限控制
- 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务
AOP核心概念
**连接点JoinPoint **可以被AOP控制的方法,所有的业务方法都是可以被aop控制的方法
通知:Advice 就是逻辑,表现为方法,方法前有@Around这样的注解
切入点:PointCut 表示应用到哪些方法上,切入点指的是匹配连接点的条件(切入点表达式会指定一些或者一个切入点)
切面:Aspect 通知和切入点构成一个切面,在什么时候执行什么样的操作
切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
目标对象:Target 通知应用的对象,也就是作用的方法
Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。
AOP进阶
切面类前要加上@Aspect注解
AOP的基础知识学习完之后,下面我们对AOP当中的各个细节进行详细的学习。主要分为4个部分:
- 通知类型
- 通知顺序
- 切入点表达式
- 连接点
通知类型
@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;
}
只要我们在通知方法上加上了@Around注解,就代表当前通知是一个环绕通知。
Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
程序发生异常的情况下:
@AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
@Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)
代码当中就存在了大量的重复性的切入点表达式,假如此时切入点表达式需要变动,就需要将所有的切入点表达式一个一个的来改动,就变得非常繁琐了
抽取
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){ // 外部需要使用这个切入点方法,就要改成public
}
@Around("pt()") //直接把原来的切入点表达式改成方法调用
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法在执行时:发生异常
//后续代码不在执行
log.info("around after ...");
return result;
}
多个切面类的执行顺序?
在不同切面类中,默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行(就像逐层返回)
如果我们想控制通知的执行顺序有两种方式:
- 修改切面类的类名(这种方式非常繁琐、而且不便管理)
- 使用Spring提供的@Order注解,在切面类前加上@Order注解标注类执行的顺序,越小越先执行
切入点表达式
切入点表达式:
-
描述切入点方法的一种表达式
-
作用:主要用来决定项目中的哪些方法需要加入通知
-
常见形式:
- execution(……):根据方法的签名来匹配
- @annotation(……) :根据注解匹配 设置号注解,标注了这个自定义注解的方法被加入通知
execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数类型) throws 异常?)
其中带?
的表示可以省略的部分
-
访问修饰符:可省略(比如: public、protected)
-
包名.类名: 可省略
-
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
使用
..
配置包名,标识此包以及此包下的所有子包execution(* com…DeptServiceImpl.delete(java.lang.Integer))
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 …,使用 * 匹配单个包
@annotation
如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。这个时候我们基于execution这种切入点表达式来描述就不是很方便了。而在之前我们是将两个切入点表达式组合在了一起完成的需求,这个是比较繁琐的。
实现步骤:
-
编写自定义注解
-
在业务类要做为连接点的方法上添加自定义注解
自定义注解:MyLog
@Target(ElementType.METHOD) //表示这个注解可以应用在方法上
@Retention(RetentionPolicy.RUNTIME) //这个注解会在运行时保留,允许通过反射机制来访问
public @interface MyLog {
}
切面类
@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;
}
}
AOP案例
需求:将案例中增、删、改相关接口的操作日志记录到数据库表中
操作日志信息包含:
- 操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
创建OperateLog数据库,相应的实体类,mapper接口insert方法
创建一个anno包下的Log 注解,由于涉及到多种类型的操作,可能对应多种类型名,采用@annotation注解方式利用自定义的注解把目标对象和通知绑定
@Retention(RetentionPolicy.RUNTIME) //注解在运行时保留
@Target(ElementType.METHOD) // 注解可以应用在方法上
public @interface Log {
}
创建aop包下的LogAspect切片类,需要注入HttpServletRequest来获取token中的登录信息
@Component
@Slf4j
@Aspect
public class LogAspect {
@Autowired
private HttpServletRequest request; // 用来获取token中的登录信息
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.example.anno.Log)") // 对于标注了Log的都进行Around
public Object recordLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 通过request获取jwt中的登录信息
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operateUser = (Integer) claims.get("id");
LocalDateTime operateTime = LocalDateTime.now();
String className = proceedingJoinPoint.getClass().getName();
String methodName = proceedingJoinPoint.getSignature().getName();
Object[] args = proceedingJoinPoint.getArgs();
String methodParams = Arrays.toString(args);
long begin = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
long end = System.currentTimeMillis();
String returnValue = JSONObject.toJSONString(result);
Long costTime = end - begin;
// 记录操作日志
OperateLog operateLog = new OperateLog(null, operateUser, operateTime, className,
methodName, methodParams, returnValue, costTime);
log.info("AOP操作记录日志:{}", operateLog);
operateLogMapper.insert(operateLog);
return result;
}
}