AOP-事务

AOP基础

  • AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。

AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)

AOP的优势:

  1. 减少重复代码
  2. 提高开发效率
  3. 维护方便

使用场景

  • 记录系统的操作日志
  • 权限控制
  • 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务

核心概念

1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)

2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用

4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)

特例:切面类:@Aspect切面所在的类,我们一般称为切面类**(被@Aspect注解标识的类)

5. 目标对象:Target,通知所应用的对象

通知Advice

通知类型:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
  1. @Around:环绕通知,此注解标注的通知方法在目标方法前,后都被执行

    • @Around环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行。

    • @Around环绕通知方法的返回值,必须是Object,来返回原始方法的返回值

    • 实例:

       @Around("execution(* com.alibaba.service.*.*(..))") 
          public Object recordTime(ProceedingJoinPoint pjp) throws Throwable{  //略}
      
  2. @PointCut

    • 实例:
     @Pointcut("execution(* com.alibaba.service.EmpService.login(com.itcast.pojo.Emp))")
        public void pt(){};
         
        @Around("com.alibaba.aop.TimeAspect.pt()")
        @Around("pt()")
        public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) {
    
            /*
            略
            */
            
            return proceed;
        }
    
    

注意事项:

  1. public:修饰外部切面类也可以引用表达式
  2. private:修饰只能在当前切面类使用
  3. @Pointcut注解:可以将切入点表达式抽去到一个方法中,如上方pt()
  4. 通知类型:在通知类型中@Around需要使用 ProceedingJoinPoint.proceed() 来调用原始方法,其他通知不需要考虑目标方法执行

通知顺序:(当使用多个切面时,可以通过@Order注解类来控制切面类的执行优先级

  • 不同切面类同时使用,默认按照切面类的类名称字母排序:

    • 目标方法前的通知方法:字母排名靠前的先执行
    • 目标方法后的通知方法:字母排名靠前的后执行
  • @Order(number) 加在切面类上来控制顺序()

    • 目标方法前的通知方法:数字小的先执行
    • 目标方法后的通知方法:数字小的后执行

切入点表达式

execution

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)

其中带?的表示可以省略的部分

  • 访问修饰符:可省略(比如: public、protected)

  • 包名.类名: 可省略

  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

示例:

@Before("execution(void com.alibaba.service.impl.DeptServiceImpl.delete(java.lang.Integer))")

可以使用通配符描述切入点

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

  • .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

切入点表达式的语法规则:

  1. 方法的访问修饰符可以省略
  2. 返回值可以使用*号代替(任意返回值类型)
  3. 包名可以使用*号代替,代表任意包(一层包使用一个*
  4. 使用..配置包名,标识此包以及此包下的所有子包
  5. 类名可以使用*号代替,标识任意类
  6. 方法名可以使用*号代替,表示任意方法
  7. 可以使用 * 配置参数,一个任意类型的参数
  8. 可以使用.. 配置参数,任意个任意类型的参数

注意事项

  • 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。

    execution(* com.alibaba.service.DeptService.list(..)) || execution(* com.alibaba.service.DeptService.delete(..))
    

annotation

  • @annotation 切入点表达式,用于匹配标识有特定注解的方法。

  • @annotation(com.itheima.anno.Log)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}

@Slf4j
@Component
@Aspect
public class MyAspect6 {
    //针对list方法、delete方法进行前置通知和后置通知
    //前置通知
    @Before("@annotation(com.alibaba.anno.MyLog)")
    public void before(){
        log.info("MyAspect6 -> before ...");
    }
}
  • execution切入点表达式
    • 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
    • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
  • annotation 切入点表达式
    • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

  • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型

  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型

ProceedingJoinPoint常用方法:

  • getTarget() //获取目标

  • getTarget().getClass().getName() //获取类名

  • getSignature() //获取目标方法签名

  • getSignature().getName() //获取方法名

  • getArgs(); //获取目标方法运行的参数

  • proceed() //执行原始方法,获取返回值(环绕通知使用)

JoinPoint常用方法:

  • getTarget() //获取目标
  • getTarget().getClass().getName() //获取目标类名
  • getSignature() //获取方法签名
  • getSignature().getName() //获取目标方法名
  • getArgs() //获取目标方法运行参数

AOP案例

需求:将案例中增、删、改相关接口的操作日志记录到数据库表中

  • 就是当访问部门管理和员工管理当中的增、删、改相关功能接口时,需要详细的操作日志,并保存在数据表中,便于后期数据追踪。

操作日志信息包含:

  • 操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
  1. 导入依赖:在pom.xml中导入AOP的依赖
  2. 编写AOP程序:针对于特定方法根据业务需要进行编程

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

实现:

@Component // 声明该类为Spring组件
@Aspect // 声明该类为切面
@Slf4j // 使用Lombok注解,简化日志记录
public  class StoreLogs {
    @Autowired // 自动注入CreateLogMapper对象
    CreateLogMapper createLogMapper;
    @Autowired // 自动注入HttpServletRequest对象,用于获取HTTP请求的信息
    HttpServletRequest httpServletRequest;
    // 定义切点,匹配所有以Insert、Update、Delete结尾的方法
    @Pointcut("execution(* com.alibaba.controller.*.*Insert*(..)) || execution(* com.alibaba.controller.*.*Update*(..)) || execution(* com.alibaba.controller.*.*Delete*(..)) ")
    public void logs(){}
    // 定义环绕通知,记录日志并执行被切入的方法
//    @Around("logs()")
    public Object aopLogs(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 获取HTTP请求头中的token
        String token = httpServletRequest.getHeader("token");
        log.info("token:{}",token);
        // 解析token,获取用户信息
        Claims claims = JwtUtils.parsJwt(token);
        // 获取方法参数
        Object[] args = proceedingJoinPoint.getArgs();
        // 记录方法执行开始时间
        long start = System.currentTimeMillis();
        // 执行被切入的方法
        Object proceed = proceedingJoinPoint.proceed();
        // 记录方法执行结束时间
        long end = System.currentTimeMillis();
        // 获取方法参数
        Object[] args1 = proceedingJoinPoint.getArgs();
        // 计算方法执行时间
        String sumTime = end-start+"ms";
        // 创建日志对象
        CreateLog createLog = new CreateLog(claims.get("id").toString()+":"+claims.get("username").toString(),proceedingJoinPoint.getTarget().getClass().getName(),proceedingJoinPoint.getSignature().getName(),Arrays.toString(args),Arrays.toString(args1),sumTime);
        // 将日志对象插入到数据库中
        createLogMapper.insertLog(createLog);
        // 返回方法执行结果
        return proceed;
    }
}
@Mapper
public interface CreateLogMapper {
    @Insert("insert into createlog (name, class_name, method_name, request, response, run_time) value " +
            "(#{name},#{className},#{methodName},#{request},#{response},#{runTime})")
    void insertLog(CreateLog createLog);
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateLog {
    private String name;
    private String className;
    private String methodName;
    private String request;
    private String response;
    private String runTime;
}

create table createlog
(
    id          int auto_increment comment '主键'
        primary key,
    name        varchar(50)                        not null comment '操作人',
    class_name  varchar(100)                       not null comment '执行方法的全类名',
    method_name varchar(100)                       not null comment '执行方法名',
    request     varchar(500)                       not null comment '方法运行时参数',
    response    varchar(500)                       not null comment '返回值',
    run_time    varchar(50)                        not null comment '方法执行时长',
    create_time datetime default CURRENT_TIMESTAMP not null comment '操作时间'
)
    collate = utf8mb4_general_ci;

事务

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

事务的操作主要有三步:

  1. 开启事务(一组操作开始前,开启事务):start transaction / begin ;
  2. 提交事务(这组操作全部成功后,提交事务):commit ;
  3. 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;

Transactional注解

@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。

@Transactional注解:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。

@Transactional注解书写位置:

  • 方法
    • 当前方法交给spring进行事务管理
    • 当前类中所有的方法都交由spring进行事务管理
  • 接口
    • 接口下所有的实现类当中所有的方法都交给spring 进行事务管理

说明:可以在application.yml配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息了

#spring事务管理日志
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug

@Transactional注解当中的两个常见的属性:

  1. 异常回滚的属性:rollbackFor

    • 结论:
    • 在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚。
    • 如果还需要回滚指定类型的异常,可以通过rollbackFor属性来指定。
    // 所有异常全部回滚
    @Transactional(rollbackFor = Exception.class)
    
  2. 事务传播行为:propagation

    什么是事务的传播行为呢?

    • 就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
属性值含义
REQUIRED【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW需要新事务,无论有无,总是创建新事务
SUPPORTS支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY必须有事务,否则抛异常
NEVER必须没事务,否则抛异常

对于这些事务传播行为,我们只需要关注以下两个就可以了:

  1. REQUIRED(默认值)
  2. REQUIRES_NEW

案例

接下来我们就通过一个案例来演示下事务传播行为propagation属性的使用。

**需求:**解散部门时需要记录操作日志

​ 由于解散部门是一个非常重要而且非常危险的操作,所以在业务当中要求每一次执行解散部门的操作都需要留下痕迹,就是要记录操作日志。而且还要求无论是执行成功了还是执行失败了,都需要留下痕迹。

步骤:

  1. 执行解散部门的业务:先删除部门,再删除部门下的员工(前面已实现)
  2. 记录解散部门的日志,到日志表(未实现)

准备工作:

  1. 创建数据库表 dept_log 日志表:
create table dept_log(
   	id int auto_increment comment '主键ID' primary key,
    create_time datetime null comment '操作时间',
    description varchar(300) null comment '操作描述'
)comment '部门操作日志表';
  1. 引入资料中提供的实体类:DeptLog
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
    private Integer id;
    private LocalDateTime createTime;
    private String description;
}
  1. 引入资料中提供的Mapper接口:DeptLogMapper
@Mapper
public interface DeptLogMapper {

    @Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
    void insert(DeptLog log);

}
  1. 引入资料中提供的业务接口:DeptLogService
public interface DeptLogService {
    void insert(DeptLog deptLog);
}
  1. 引入资料中提供的业务实现类:DeptLogServiceImpl
@Service
public class DeptLogServiceImpl implements DeptLogService {

    @Autowired
    private DeptLogMapper deptLogMapper;

    @Transactional //事务传播行为:有事务就加入、没有事务就新建事务
    @Override
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }
}

代码实现:

业务实现类:DeptServiceImpl

@Slf4j
@Service
//@Transactional //当前业务实现类中的所有的方法,都添加了spring事务管理机制
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;
    
    @Autowired
    private EmpMapper empMapper;

    @Autowired
    private DeptLogService deptLogService;


    //根据部门id,删除部门信息及部门下的所有员工
    @Override
    @Log
    @Transactional(rollbackFor = Exception.class) 
    public void delete(Integer id) throws Exception {
        try {
            //根据部门id删除部门信息
            deptMapper.deleteById(id);
            //模拟:异常
            if(true){
                throw new Exception("出现异常了~~~");
            }
            //删除部门下的所有员工信息
            empMapper.deleteByDeptId(id);
        }finally {
            //不论是否有异常,最终都要执行的代码:记录日志
            DeptLog deptLog = new DeptLog();
            deptLog.setCreateTime(LocalDateTime.now());
            deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
            //调用其他业务类中的方法
            deptLogService.insert(deptLog);
        }
    }
    
    //省略其他代码...
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值