2024-02-28-事务-AOP-黑马javaweb

本文介绍了如何在Spring框架中使用AOP实现事务管理,解决多操作间的事务一致性问题,以及如何通过AOP记录操作日志。重点讨论了@Transactional注解的使用、事务传播行为、AOP通知类型和切入点表达式的应用。
摘要由CSDN通过智能技术生成

2024-02-28-事务-AOP-黑马javaweb

事务管理

事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。这组操作要么同时成功,要么同时失败。

事务的操作主要有三步:

  1. 开启事务(一组操作开始前,开启事务):start transaction / begin ;
  2. 提交事务(这组操作全部成功后,提交事务):commit ;
  3. 回滚事务(中间任何一个操作出现异常,回滚事务):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 进行事务管理

事务进阶

  1. 异常回滚的属性:rollbackFor 对哪些异常进行回滚
  2. 事务传播行为: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事务?这个就涉及到了事务的传播行为。

  1. REQUIRED(默认值)需要事务,有则加入,无则创建新事务
  2. 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个部分:

  1. 通知类型
  2. 通知顺序
  3. 切入点表达式
  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;
}

多个切面类的执行顺序?

在不同切面类中,默认按照切面类的类名字母排序:

  • 目标方法前的通知方法:字母排名靠前的先执行
  • 目标方法后的通知方法:字母排名靠前的后执行(就像逐层返回)

如果我们想控制通知的执行顺序有两种方式:

  1. 修改切面类的类名(这种方式非常繁琐、而且不便管理)
  2. 使用Spring提供的@Order注解,在切面类前加上@Order注解标注类执行的顺序,越小越先执行
切入点表达式

切入点表达式:

  • 描述切入点方法的一种表达式

  • 作用:主要用来决定项目中的哪些方法需要加入通知

  • 常见形式:

    1. execution(……):根据方法的签名来匹配
    2. @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这种切入点表达式来描述就不是很方便了。而在之前我们是将两个切入点表达式组合在了一起完成的需求,这个是比较繁琐的。

实现步骤:

  1. 编写自定义注解

  2. 在业务类要做为连接点的方法上添加自定义注解

自定义注解: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;
    }
}
  • 23
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值