Springboot中AOP的应用

1. 文章内容

这篇文章主要记录工作中使用AOP的一次记录以及讲述了为什么会使用AOP,什么场景下适合使用AOP

1.1 什么是AOP?

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于增强现有的代码功能,提供横切关注点的模块化处理。看着介绍可能还是不知道AOP是用来干嘛的,说白了就是可以在方法执行之前,执行之后,或者抛出异常的时候执行一些相应的操作,比如可以用来做日志,性能监控(例如记录方法的执行时间、资源消耗等),异常处理(可以捕获方法抛出的异常)等

1.2 应用场景

在工作中碰到了这样一个需求,需要在现有代码的基础上记录用户的操作记录并且保存业务唯一标识,比如新增、删除、修改时需把对应的操作记录保存到数据库中,供后续使用。一开始碰到这样的需求,可能最开始想到的是在每个方法后面添加几行代码,把每个方法对应的操作记录到数据库不就ok了,大功告成。这样确实可以实现功能,不过也会带来相应的问题:

  1. 违反开闭原则(对拓展开放,对修改关闭)
  2. 如果原有方法多,则每个方法都要去修改,费时费力,不能统一管理,如果后续需求有变动,需要改的代码就会越来越多,导致维护难度加大

1.3 解决方案

既然上文已经列出了最基础的方案实现的缺点,那么应该也需要有相应的解决方案,没错,这个场景其实就是符合AOP的应用场景,其实我们需要做的其实只是在每个方法执行之后记录对应的操作,然后把数据保存到数据库即可。其实我们这个时候已经可以把这个功能的实现分为三步:

  1. 获取操作类型和业务唯一标识
  2. 获取用户信息,并且设置一些公共基本信息(ID、创建时间等)
  3. 保存到数据库

2. 基本实现

2.1 方案设计

我们如果要实现利用AOP来记录日志的话,想到的是使用注解,里面定义操作类型(新增、删除、修改)和业务唯一标识,当然了,如果有其他需求的话也可以在基础上进行拓展,比如需要记录具体的操作,例如新增了xxx,修改了xxx,删除了xxx等。可以多加一个字段来区分,其他的功能也类似。有了注解之后我们可以在方法上使用注解,然后在aspect(切面)中处理携带了注解的方法,解析方法上的参数(操作类型、业务唯一标识),这样就可以做到不侵入现有代码的情况下进行拓展,统一管理代码,方便维护。

2.2 代码实现

2.2.1 编写业务代码

实体类

用户信息类

JAVA 复制代码
@Data  
@Builder  
@NoArgsConstructor  
@AllArgsConstructor  
@Accessors(chain = true)  
public class Person {  
  
    /**  
    * 姓名  
    */  
    private String name;  

    /**  
    * 年龄  
    */  
    private Integer age;  
  
}

操作记录类

JAVA 复制代码
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class ActionRecord {

    /**
     * id
     */
    private Long id;

    /**
     * 模拟的业务唯一标识
     */
    private String name;

    /**
     * 具体的操作:添加、删除、修改
     */
    private String action;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 创建人
     */
    private String createBy;

}

Controller

JAVA 复制代码
@RestController
@RequiredArgsConstructor
public class PeopleController {

    private final PersonService personService;

    @PostMapping("/addPerson")
    public String addPerson(@RequestBody Person person) {
        return personService.addPerson(person);
    }

    @DeleteMapping("/deletePerson/{name}")
    public String deletePerson(@PathVariable String name) {
        return personService.deletePerson(name);
    }

}

service

JAVA 复制代码
public interface PersonService {  

    String addPerson(Person person);  

    String deletePerson(String name);  
}

serviceImpl

JAVA 复制代码
@Slf4j  
@Service  
public class PersonServiceImpl implements PersonService {  
  
    @Override  
    public String addPerson(Person person) {  
        log.info("添加的用户信息为:【{}】", person);  
        log.info("保存用户信息...");  
        return "保存成功";  
    }  

    @Override  
    public String deletePerson(String name) {  
        // ...  
        log.info("即将删除的用户为:【{}】", name);  
        log.info("删除用户中...");  
        return "删除成功";  
    }  
}

至此,基本环境就搭建好了,调用方法接口访问进行测试也是ok的。

2.2.2 环境测试

调用API,模拟添加用户,可以看到是可以正常调用到我们的添加和删除方法的

2.2.3 记录用户操作记录

方式1:直接修改现有代码

  1. 如果在现有基础上需要添加用户操作的记录,最开始想到的就是让前端在参数Person(因为懒,入参就不写DTO和校验了)里面添加两个字段,直接从前端传递操作人工号和姓名,然后再修改后端的方法,把用户信息手动保存到数据库,这种方式虽然麻烦,但是能跑,模拟实现如下,添加operatorCodeoperatorName字段
JAVA 复制代码
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Person {

    /**
     * 姓名
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 操作人工号
     */
    private String operatorCode;

    /**
     * 操作人姓名
     */
    private String operatorName;
}
  1. 在后端的方法里面添加保存操作记录的逻辑
JAVA 复制代码
    @Override
    public String addPerson(Person person) {
        log.info("添加的用户信息为:【{}】", person);
        log.info("保存用户信息...");
        this.addPersonAddOperateRecord(person);
        return "保存成功";
    }

    private void addPersonAddOperateRecord(Person person) {
        log.info("person:{}", person);
        log.info("保存:{}用户执行了添加用户的操作", person.getOperatorName());
    }

运行效果:可以看到这个方法是可行的

JAVA 复制代码
2023-09-06 22:41:48.811  INFO 9036 --- [nio-8080-exec-3] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-06 22:41:48.811  INFO 9036 --- [nio-8080-exec-3] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-06 22:41:48.811  INFO 9036 --- [nio-8080-exec-3] c.e.d.d.service.impl.PersonServiceImpl   : person:Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)
2023-09-06 22:41:48.811  INFO 9036 --- [nio-8080-exec-3] c.e.d.d.service.impl.PersonServiceImpl   : 保存:法外狂徒张三用户执行了添加用户的操作
  1. 优缺点分析

优点

  1. 操作简单

缺点

  1. 前端需要在每个需要保存用户操作记录的方法上都要额外加上操作人信息
  2. 后端也需要在每个需要保存用户操作记录的方法上进行拓展,在涉及的DTO里面都需要添加操作人字段,还需要根据影响的方法类型(添加、删除、修改)来执行不同的保存操作
  3. 代码不优雅,要往原有代码上继续堆积,可能会导致后续维护困难

方式2: AOP+后端获取用户信息

看到了上面方法的缺点,可能大家都接受不了,现在只是写个demo,模拟模拟,但是真实业务场景可能会有很多个需要修改的方法,因此方法1的成本太高,不推荐使用,转而推荐使用AOP+后端获取用户信息的方法。其实分析清楚需求的话就会发现,无非要记录的只是三种操作(新增、删除、修改),外加一个操作的用户工号和姓名

需要记录的方法共同点

  • 都是在现有方法上进行拓展
  • 不同方法需要记录为不同的操作类型
  • 都需要记录用户信息

梳理完了共同点之后,我们可以想到,可以通过AOP来统一做记录。通过定义注解,然后在现有方法上添加注解,标注操作类型和其他信息(例如具体的删除操作或者修改操作),让AOP在有该注解的方法执行完成之后进行记录用户操作记录

用户信息来源

说到这里,不是还没解决用户信息的问题吗?用户信息不是还得靠前端去给吗? 这肯定不是的,在一个完善的系统里面,前后端交互都会通过token,后端通过token进行鉴权,判断用户身份。因此获取用户信息可以从这一块进行入手,如果是单体系统可以直接通过现有的公共方法获取用户信息,如果是分布式的系统可以请求拥有用户信息的服务,去把用户信息拉过来,这样我们用户信息的问题就可以统一解决,既不破坏现有代码,也能实现功能

代码实现

  1. 定义注解 首先定义一个注解,其中name用来模拟系统的唯一标识,ActionType则是用户的操作类型
JAVA 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RecordUserAction {

    String name() default "";

    ActionType type();

}
  1. 定义操作类型的枚举
JAVA 复制代码
public enum ActionType {
    CREATE("创建"),
    UPDATE("更新"),
    DELETE("删除");

    private final String description;

    ActionType(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}
  1. 定义AOP的切面
JAVA 复制代码
@Slf4j
@Aspect
@Component
public class UserActionLoggingAspect {
    @AfterReturning(value = "@annotation(recordUserAction)", returning = "result")
    public void afterMethodExecution(JoinPoint joinPoint, RecordUserAction recordUserAction, Object result) {
        log.info("执行方法之后进行的一些操作...");
        ActionType type = recordUserAction.type();
        log.info("操作类型为:【{}】", type);
    }
}
  1. 把注解添加到原有代码的方法上
JAVA 复制代码
@Override
@RecordUserAction(type = ActionType.CREATE)
public String addPerson(Person person) {
    log.info("添加的用户信息为:【{}】", person);
    log.info("保存用户信息...");
    return "保存成功";
}
  1. 重新调用添加用户信息测试 可以看到已经可以在方法执行完之后走到AOP的代理类中,并且输出操作类型
JAVA 复制代码
2023-09-06 23:12:15.770  INFO 17760 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-09-06 23:12:15.770  INFO 17760 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-09-06 23:12:15.771  INFO 17760 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2023-09-06 23:12:15.811  INFO 17760 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-06 23:12:15.811  INFO 17760 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-06 23:12:15.812  INFO 17760 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-06 23:12:15.812  INFO 17760 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】
  1. 有了上述的内容,现在只缺根据业务唯一标识和操作人来保存操作记录信息

之前不是定义了业务唯一标识吗?现在就应该把它给用起来: 首先是在刚才的addPerson方法上添加name,用EL表达是动态传入我们不同DTO中的唯一标识,例如这里传入的是person对象中的name属性

JAVA 复制代码
@RecordUserAction(type = ActionType.CREATE, name = "#{#person.name}")
public String addPerson(Person person)

然后在我们的拦截方法中进行解析,把这个name解析出来,executeTemplate是解析EL表达式传入参数的方法

JAVA 复制代码
    @AfterReturning(value = "@annotation(recordUserAction)", returning = "result")
    public void afterMethodExecution(JoinPoint joinPoint, RecordUserAction recordUserAction, Object result) {
        log.info("执行方法之后进行的一些操作...");
        String name = recordUserAction.name();
        log.info("aop中获取到的name为:【{}】", executeTemplate(name, joinPoint));
        ActionType type = recordUserAction.type();
        log.info("操作类型为:【{}】", type);
    }
    
    private String executeTemplate(String message, JoinPoint joinPoint) {
        ExpressionParser parser = new SpelExpressionParser();
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        String[] params = discoverer.getParameterNames(method);
        Object[] args = joinPoint.getArgs();
        EvaluationContext context = new StandardEvaluationContext();
        for (int len = 0; len < params.length; len++) {
            context.setVariable(params[len], args[len]);
        }
        return parser.parseExpression(message, new TemplateParserContext()).getValue(context, String.class);
    }

再次执行,可以看到我们的name已经被解析出来了

JAVA 复制代码
-09-06 23:20:14.871  INFO 9712 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-06 23:20:14.871  INFO 9712 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-06 23:20:14.871  INFO 9712 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-06 23:20:14.877  INFO 9712 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【李华】
2023-09-06 23:20:14.877  INFO 9712 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】

到了这里,参数中的唯一标识我们可以动态传入,操作类型也可以通过注解进行指定,就差用户信息了,这里就模拟一个方法来获取用户的信息

JAVA 复制代码
    @AfterReturning(value = "@annotation(recordUserAction)", returning = "result")
    public void afterMethodExecution(JoinPoint joinPoint, RecordUserAction recordUserAction, Object result) {
        log.info("执行方法之后进行的一些操作...");
        String name = recordUserAction.name();
        log.info("aop中获取到的name为:【{}】", executeTemplate(name, joinPoint));
        ActionType type = recordUserAction.type();
        log.info("操作类型为:【{}】", type);
        String operatorCode = this.getOperatorCode();
        log.info("获取到的操作人工号为:{}", operatorCode);
    }

    /**
     * 获取用户工号
     *
     * @return 用户工号
     */
    private String getOperatorCode() {
        return "C20230906";
    }

此时再调用addPerson方法,可以看到我们的参数都齐全了

JAVA 复制代码
2023-09-06 23:24:43.017  INFO 16992 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-06 23:24:43.017  INFO 16992 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-06 23:24:43.017  INFO 16992 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-06 23:24:43.022  INFO 16992 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【李华】
2023-09-06 23:24:43.022  INFO 16992 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】
2023-09-06 23:24:43.022  INFO 16992 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 获取到的操作人工号为:C20230906

这个时候我们再来一个统一保存的方法就ok了,把ID、创建人、创建时间这些基本信息都填充好再直接落库,就大功告成

JAVA 复制代码
    @AfterReturning(value = "@annotation(recordUserAction)", returning = "result")
    public void afterMethodExecution(JoinPoint joinPoint, RecordUserAction recordUserAction, Object result) {
        log.info("执行方法之后进行的一些操作...");
        String name = executeTemplate(recordUserAction.name(), joinPoint);
        log.info("aop中获取到的name为:【{}】", executeTemplate(name, joinPoint));
        ActionType type = recordUserAction.type();
        log.info("操作类型为:【{}】", type);
        this.setActionRecordBasicInfo(name, type.getDescription());
    }

    /**
     * 设置操作记录的基本信息,包括id、创建人、创建时间
     *
     * @param name       唯一标识
     * @param actionName 操作名称
     */
    private void setActionRecordBasicInfo(String name, String actionName) {
        ActionRecord build = ActionRecord.builder().id(123456789L).createBy(getOperatorCode())
                .name(name).action(actionName).createTime(new Date()).build();
        log.info("添加的操作记录:{}成功", build);
    }

调用接口进行测试:

JAVA 复制代码
2023-09-10 22:14:39.927  INFO 23328 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-10 22:14:39.928  INFO 23328 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-10 22:14:39.928  INFO 23328 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-10 22:14:39.933  INFO 23328 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【李华】
2023-09-10 22:14:39.933  INFO 23328 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】
2023-09-10 22:14:39.933  INFO 23328 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 添加的操作记录:ActionRecord(id=123456789, name=李华, action=创建, createTime=Sun Sep 10 22:14:39 CST 2023, createBy=C20230906)成功

删除数据的接口同理,调整如下:

修改service中的代码,添加注解

JAVA 复制代码
    @Override
    @RecordUserAction(type = ActionType.DELETE, name = "#{#name}")
    public String deletePerson(String name) {
        log.info("即将删除的用户为:【{}】", name);
        log.info("删除用户中...");
        return "删除成功";
    }

其他地方就不用修改了,AOP已经帮我们都做好了,直接运行测试

JAVA 复制代码
2023-09-10 22:15:52.108  INFO 23328 --- [nio-8080-exec-8] c.e.d.d.service.impl.PersonServiceImpl   : 即将删除的用户为:【张三】
2023-09-10 22:15:52.108  INFO 23328 --- [nio-8080-exec-8] c.e.d.d.service.impl.PersonServiceImpl   : 删除用户中...
2023-09-10 22:15:52.108  INFO 23328 --- [nio-8080-exec-8] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-10 22:15:52.109  INFO 23328 --- [nio-8080-exec-8] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【张三】
2023-09-10 22:15:52.109  INFO 23328 --- [nio-8080-exec-8] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【DELETE】
2023-09-10 22:15:52.109  INFO 23328 --- [nio-8080-exec-8] c.e.d.d.aspect.UserActionLoggingAspect   : 添加的操作记录:ActionRecord(id=123456789, name=张三, action=删除, createTime=Sun Sep 10 22:15:52 CST 2023, createBy=C20230906)成功

到此为止,已基本实现了我们预期的功能,可以使用AOP进行统一记录用户的操作记录

3.优化提升

3.1 优化分析

经过第二章的实现,我们已经可以实现基本的记录用户操作日志的功能,但是还是存在一些其他问题,比如:

  1. 目前的注解都是直接作用于方法上,如果想使用在方法里面调用的方法呢?
  2. 目前保存的逻辑代码都是在aspect类中进行编写,这个是专门为注解进行服务的,但是如果一些场景无法直接通过注解去完成保存,需要手动进行保存操作记录的,上述代码是否兼容?
  3. 如果小伙伴们观察得细致的话,可以发现目前切面的代码与service中调用的方法使用的是同一个线程。正常来说,记录操作记录不应该影响现有业务,是否应该考虑使用其他线程来执行保存操作记录的相关操作?

3.2 优化实现

以下是目前的设计,代码的执行都是同步串行的,并且逻辑是在aspect里面进行编写,不易于复用

为了解决上述3个问题,可以优化成下面的方式,来满足不同场景下的操作(虚线代表不同线程),并把保存用户操作记录的逻辑提取到service

通过注解保存操作记录

手动调用保存操作记录

3.2.1 手动记录方法里的方法

看到这里,有的小伙伴就奇怪了,既然添加注解就能够实现功能,为什么还需要手动调用保存操作记录呢? 正如下面的这个场景: 现实场景中,可能会存在比较复杂的代码,例如想要在被代理的删除方法里面做点其他事情,并且做的这个事情也是需要保存操作记录,例如删除用户的其他信息,代码如下(这里用张三2号只是为了验证是否被AOP拦截到):

JAVA 复制代码
@Override
@RecordUserAction(type = ActionType.DELETE, name = "#{#name}")
public String deletePerson(String name) {
    log.info("即将删除的用户为:【{}】", name);
    log.info("删除用户中...");
    deletePersonMoney("张三2号");
    return "删除成功";
}

/**
 * 模拟删除用户的其他信息
 * 并且需要保存这个删除记录
 *
 * @param name
 */
private void deletePersonMoney(String name) {
    log.info("doSomethingOther拿到的name:{}", name);
    log.info("模拟删除用户的其他相关信息...");
    log.info("测试public方法是否可以被代理");
}

现在我们预期的效果是deletePersonMoney()方法也会被AOP拦截到,然后进行保存操作记录,直接用现在的代码跑起来试试:

JAVA 复制代码
2023-09-12 23:19:33.398  INFO 324 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 即将删除的用户为:【张三】
2023-09-12 23:19:33.399  INFO 324 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 删除用户中...
2023-09-12 23:19:33.399  INFO 324 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : doSomethingOther拿到的name:张三2号
2023-09-12 23:19:33.399  INFO 324 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 模拟删除用户的其他相关信息...
2023-09-12 23:19:33.399  INFO 324 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 测试public方法是否可以被代理
2023-09-12 23:19:33.399  INFO 324 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-12 23:19:33.403  INFO 324 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【张三】
2023-09-12 23:19:33.403  INFO 324 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【DELETE】
2023-09-12 23:19:33.405  INFO 324 --- [         task-1] c.e.d.d.s.impl.ActionRecordService       : 添加的操作记录:ActionRecord(id=123456789, name=张三, action=删除, createTime=Tue Sep 12 23:19:33 CST 2023, createBy=C20230906)成功

通过日志,可以发现只有张三进入了AOP的拦截,这时候我发现deletePersonMoney()方法上并没有添加注解,现在把注解也安排上,再次尝试,如果顺利的话,张三2号应该也可以被AOP拦截到,然后进行保存:

JAVA 复制代码
@RecordUserAction(type = ActionType.DELETE, name = "#{#name}")
private void deletePersonMoney(String name) {
    log.info("doSomethingOther拿到的name:{}", name);
    log.info("模拟删除用户的其他相关信息...");
    log.info("测试public方法是否可以被代理");
}

运行结果如下,发现好像并没有什么作用:

JAVA 复制代码
2023-09-12 23:22:51.866  INFO 10808 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 即将删除的用户为:【张三】
2023-09-12 23:22:51.866  INFO 10808 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 删除用户中...
2023-09-12 23:22:51.866  INFO 10808 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : doSomethingOther拿到的name:张三2号
2023-09-12 23:22:51.866  INFO 10808 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 模拟删除用户的其他相关信息...
2023-09-12 23:22:51.866  INFO 10808 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 测试public方法是否可以被代理
2023-09-12 23:22:51.867  INFO 10808 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-12 23:22:51.870  INFO 10808 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【张三】
2023-09-12 23:22:51.870  INFO 10808 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【DELETE】
2023-09-12 23:22:51.871  INFO 10808 --- [         task-1] c.e.d.d.s.impl.ActionRecordService       : 添加的操作记录:ActionRecord(id=123456789, name=张三, action=删除, createTime=Tue Sep 12 23:22:51 CST 2023, createBy=C20230906)成功

这个时候我又有了新发现,难不成是因为我的方法是private的?!导致了没法被代理?话不多说,改:

JAVA 复制代码
@RecordUserAction(type = ActionType.DELETE, name = "#{#name}")
public void deletePersonMoney(String name) {
    log.info("doSomethingOther拿到的name:{}", name);
    log.info("模拟删除用户的其他相关信息...");
    log.info("测试public方法是否可以被代理");
}

再次测试,这下总该行了吧?不过现实是好像改成public也没什么用

JAVA 复制代码
2023-09-12 23:24:41.085  INFO 9088 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 即将删除的用户为:【张三】
2023-09-12 23:24:41.086  INFO 9088 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 删除用户中...
2023-09-12 23:24:41.086  INFO 9088 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : doSomethingOther拿到的name:张三2号
2023-09-12 23:24:41.086  INFO 9088 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 模拟删除用户的其他相关信息...
2023-09-12 23:24:41.086  INFO 9088 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 测试public方法是否可以被代理
2023-09-12 23:24:41.086  INFO 9088 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-12 23:24:41.089  INFO 9088 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【张三】
2023-09-12 23:24:41.089  INFO 9088 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【DELETE】
2023-09-12 23:24:41.090  INFO 9088 --- [         task-1] c.e.d.d.s.impl.ActionRecordService       : 添加的操作记录:ActionRecord(id=123456789, name=张三, action=删除, createTime=Tue Sep 12 23:24:41 CST 2023, createBy=C20230906)成功

到这里就给我整不会了,能试的方法都试了,可能是姿势不对,所以我觉得这个时候可能得研究一下Spring中的AOP是个啥情况,导致无法成功代理。

经过网上的搜索,发现了Springboot中的AOP默认使用的是JDK动态代理,只有当目标类没有实现接口时才会使用 Cglib 代理

所以是不是因为我这里是接口,然后使用了JDK动态代理?但是是什么原因导致没法代理成功,却没有明确的答案,因此我打算自己尝试一下,所以就有了动态代理学习这个文章,里面经过测试,发现了基于JDK动态代理的方式,类里面直接调用类里面的方法是无法再次被代理的,就算方法是public的也不行,只有Cglib可以。恰好这里又是一个实现了接口的类,因此可能是使用的JDK动态代理,导致方法内部调用同一个类的其他方法无法被再次代理。

解决方案

按照上面的分析,这里应该起码有2种解决方案:

  1. deletePersonMoney()方法提取到另外一个类,这样就相当于会再次被代理
  2. 手动调用保存用户操作记录的方法

这里我选择的是方法2,毕竟方法1如果涉及到的代码比较多的话,就需要弄很多个类。可以,但没必要。所以才有了手动调用方法保存操作记录,下文就介绍如何实现手动保存。

3.2.2 把aspect的方法提取为service方法

目前我们的代码都是写在aspect中,并不能满足需要手动记录操作记录的情况,所以现在需要把保存操作记录的逻辑提取到service中,作为一个公共的方法,实现aspect调用和手动调用

修改aspect之后的代码:

JAVA 复制代码
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class UserActionLoggingAspect {

    private final ActionRecordService actionRecordService;

    @AfterReturning(value = "@annotation(recordUserAction)", returning = "result")
    public void afterMethodExecution(JoinPoint joinPoint, RecordUserAction recordUserAction, Object result) {
        log.info("执行方法之后进行的一些操作...");
        String name = executeTemplate(recordUserAction.name(), joinPoint);
        log.info("aop中获取到的name为:【{}】", executeTemplate(name, joinPoint));
        ActionType type = recordUserAction.type();
        log.info("操作类型为:【{}】", type);
        actionRecordService.saveActionRecordBasicInfo(name, type.getDescription());
    }
    
    /**
     * 解析SPEL
     *
     * @param message
     * @param joinPoint
     * @return
     */
    private String executeTemplate(String message, JoinPoint joinPoint) {
        ExpressionParser parser = new SpelExpressionParser();
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        String[] params = discoverer.getParameterNames(method);
        Object[] args = joinPoint.getArgs();
        EvaluationContext context = new StandardEvaluationContext();
        for (int len = 0; len < params.length; len++) {
            context.setVariable(params[len], args[len]);
        }
        return parser.parseExpression(message, new TemplateParserContext()).getValue(context, String.class);
    }

}

新增的ActionRecordService

JAVA 复制代码
@Slf4j
@Service
public class ActionRecordService {

    /**
     * 设置操作记录的基本信息,包括id、创建人、创建时间
     *
     * @param name       唯一标识
     * @param actionName 操作名称
     */
    public void saveActionRecordBasicInfo(String name, String actionName) {
        ActionRecord build = ActionRecord.builder().id(123456789L).createBy(getOperatorCode())
                .name(name).action(actionName).createTime(new Date()).build();
        log.info("添加的操作记录:{}成功", build);
    }

    /**
     * 获取用户工号
     *
     * @return 用户工号
     */
    public String getOperatorCode() {
        return "C20230906";
    }

}

经过这么修改之后,保存操作记录的方法就变成了一个公共的方法,无论是aspect还是用户手动调用,都可以满足了,进行测试:

新增接口

JAVA 复制代码
2023-09-10 23:16:44.318  INFO 6564 --- [nio-8080-exec-3] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-10 23:16:44.318  INFO 6564 --- [nio-8080-exec-3] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-10 23:16:44.318  INFO 6564 --- [nio-8080-exec-3] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-10 23:16:44.319  INFO 6564 --- [nio-8080-exec-3] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【李华】
2023-09-10 23:16:44.319  INFO 6564 --- [nio-8080-exec-3] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】
2023-09-10 23:16:44.319  INFO 6564 --- [nio-8080-exec-3] c.e.d.d.s.impl.ActionRecordService       : 添加的操作记录:ActionRecord(id=123456789, name=李华, action=创建, createTime=Sun Sep 10 23:16:44 CST 2023, createBy=C20230906)成功

删除接口

JAVA 复制代码
2023-09-10 23:17:25.060  INFO 6564 --- [nio-8080-exec-5] c.e.d.d.service.impl.PersonServiceImpl   : 即将删除的用户为:【张三】
2023-09-10 23:17:25.060  INFO 6564 --- [nio-8080-exec-5] c.e.d.d.service.impl.PersonServiceImpl   : 删除用户中...
2023-09-10 23:17:25.060  INFO 6564 --- [nio-8080-exec-5] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-10 23:17:25.061  INFO 6564 --- [nio-8080-exec-5] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【张三】
2023-09-10 23:17:25.061  INFO 6564 --- [nio-8080-exec-5] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【DELETE】
2023-09-10 23:17:25.061  INFO 6564 --- [nio-8080-exec-5] c.e.d.d.s.impl.ActionRecordService       : 添加的操作记录:ActionRecord(id=123456789, name=张三, action=删除, createTime=Sun Sep 10 23:17:25 CST 2023, createBy=C20230906)成功

测试接口:上述接口都是与之前一样,可以正常进行调用的

3.3.3 使用新线程分离主次逻辑

为了实现我们主业务与保存操作记录分离(假设极端情况下,如果保存操作记录耗时久,会影响原有接口响应的速度,所以才把主业务和次业务进行分开),我们可以考虑使用线程池,我这里就使用springboot自带的线程池进行实现该功能,修改service,添加线程池。

JAVA 复制代码
@Slf4j
@Service
@RequiredArgsConstructor
public class ActionRecordService {

    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;

    /**
     * 设置操作记录的基本信息,包括id、创建人、创建时间
     *
     * @param name       唯一标识
     * @param actionName 操作名称
     */
    public void saveActionRecordBasicInfo(String name, String actionName) {
        threadPoolTaskExecutor.execute(() -> {
            ActionRecord build = ActionRecord.builder().id(123456789L).createBy(getOperatorCode())
                    .name(name).action(actionName).createTime(new Date()).build();
            log.info("添加的操作记录:{}成功", build);
        });
    }

    /**
     * 获取用户工号
     *
     * @return 用户工号
     */
    public String getOperatorCode() {
        return "C20230906";
    }

}

再次调用添加接口进行测试,通过日志可以看到保存操作记录的线程使用了task-1,并不是主线程中的nio-8080-exec-1

JAVA 复制代码
2023-09-10 23:29:09.998  INFO 11552 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-10 23:29:09.998  INFO 11552 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-10 23:29:09.999  INFO 11552 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-10 23:29:10.004  INFO 11552 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【李华】
2023-09-10 23:29:10.004  INFO 11552 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】
2023-09-10 23:29:10.005  INFO 11552 --- [         task-1] c.e.d.d.s.impl.ActionRecordService       : 添加的操作记录:ActionRecord(id=123456789, name=李华, action=创建, createTime=Sun Sep 10 23:29:10 CST 2023, createBy=C20230906)成功

新的发现

在使用threadPoolTaskExecutor的时候,问了一下ChatGPT,发现了这个线程池的新用法,不仅限于使用 threadPoolTaskExecutor.execute,还可以通过在方法上使用@Async注解来实现,即:

主启动类加上@EnableAsync,开启异步:

JAVA 复制代码
@EnableAsync
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}
JAVA 复制代码
    @Async
    public void saveActionRecordBasicInfo(String name, String actionName) {
//        threadPoolTaskExecutor.execute(() -> {
        ActionRecord build = ActionRecord.builder().id(123456789L).createBy(getOperatorCode())
                .name(name).action(actionName).createTime(new Date()).build();
        log.info("添加的操作记录:{}成功", build);
//        });
    }

而且!这两个东西是一样的,@Async如果不指定线程池的话,使用的就是ThreadPoolTaskExecutor 以下是ChatGPT给的答案:

如果在使用 @Async 注解时没有指定线程池的名称,Spring Boot 将使用默认的线程池来执行异步方法。 默认情况下,Spring Boot 使用一个名为 "taskExecutor" 的线程池作为默认线程池。这个默认的线程池是基于 ThreadPoolTaskExecutor 实现的,并由 Spring 自动配置。 如果没有显式定义自己的线程池 bean,并且没有指定 @Async 注解的 valueexecutor 属性来指定线程池名称,那么 @Async 注解将使用默认的线程池。

所以,针对目前的场景,直接在方法上加个@Async注解就完事了,简单粗暴

那么,问题又来了,它两的区别是啥呢?报着这个问题,我又问了GhatPT,得到的答案如下:

threadPoolTaskExecutor.execute() 方法和在方法上添加 @Async 注解有以下区别:

  1. 使用方式不同threadPoolTaskExecutor.execute() 是手动调用线程池的执行方法,需要在代码中显式调用;而在方法上添加 @Async 注解则是通过注解实现异步执行,由 Spring AOP 自动代理生成异步调用的代理对象。
  2. 控制粒度不同 :使用 threadPoolTaskExecutor.execute() 可以直接控制方法的异步调用,可以灵活地在任何需要的地方手动调用线程池;而使用 @Async 注解将方法标记为异步,由 Spring 在方法被调用时自动进行异步处理,无需手动调用线程池。
  3. 异常处理方式不同 :使用 threadPoolTaskExecutor.execute() 可以在任务执行过程中手动处理异常,通过 try-catch 块捕获异常并进行适当的处理;而使用 @Async 注解的方法在抛出异常时,可以通过定义全局异常处理器或在异步方法中捕获异常来进行处理。
  4. 代码侵入性不同 :使用 threadPoolTaskExecutor.execute() 需要在每个需要异步执行的地方显式调用,会对代码进行较大的侵入;而通过在方法上添加 @Async 注解,可以将异步调用的逻辑从业务代码中分离出来,减少了代码的侵入性,提高了可读性和可维护性。

总的来说,threadPoolTaskExecutor.execute() 方法适用于需要手动控制线程池调用的灵活场景,而使用 @Async 注解可以实现代码简洁、与业务逻辑解耦的异步操作。具体选择哪种方式取决于具体的使用场景和需求。

那么,就来验证一下异常的处理方式这一块,看看它说的是否属实:

  1. 定义一个全局异常处理器
JAVA 复制代码
/**
 * 全局异常处理器,目前用来处理ArithmeticException
 */
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ArithmeticException.class)
    public ResponseEntity<String> handleArithmeticException(ArithmeticException ex) {
        // 处理 ArithmeticException 异常
        log.info("全局异常处理器捕捉到了ArithmeticException异常...");
        String errorMessage = "发生算术异常:" + ex.getMessage();
        // 可以进行日志记录、返回特定的错误信息等操作

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorMessage);
    }

    // 下文可以定义其他的 @ExceptionHandler 方法来处理其他异常类型
}
  1. 手动制造一个异常 int i = 1 / 0;
JAVA 复制代码
//    @Async
    public void saveActionRecordBasicInfo(String name, String actionName) {
        threadPoolTaskExecutor.execute(() -> {
            ActionRecord build = ActionRecord.builder().id(123456789L).createBy(getOperatorCode())
                    .name(name).action(actionName).createTime(new Date()).build();
            int i = 1 / 0;
            log.info("添加的操作记录:{}成功", build);
        });
    }
  1. 执行代码验证
JAVA 复制代码
2023-09-11 00:11:18.421  INFO 17200 --- [nio-8080-exec-3] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-11 00:11:18.422  INFO 17200 --- [nio-8080-exec-3] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-11 00:11:18.422  INFO 17200 --- [nio-8080-exec-3] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-11 00:11:18.422  INFO 17200 --- [nio-8080-exec-3] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【李华】
2023-09-11 00:11:18.423  INFO 17200 --- [nio-8080-exec-3] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】
Exception in thread "task-3" java.lang.ArithmeticException: / by zero
	at com.example.demo.demos.service.impl.ActionRecordService.lambda$saveActionRecordBasicInfo$0(ActionRecordService.java:30)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

现在是没使用@Async注解,使用的是execute方法来执行,按照上述的描述,没被全局异常处理器捕捉到,属于正常,可以理解。 现在改成使用@Async注解,再进行测试

JAVA 复制代码
    @Async
    public void saveActionRecordBasicInfo(String name, String actionName) {
//        threadPoolTaskExecutor.execute(() -> {
        ActionRecord build = ActionRecord.builder().id(123456789L).createBy(getOperatorCode())
                .name(name).action(actionName).createTime(new Date()).build();
        int i = 1 / 0;
        log.info("添加的操作记录:{}成功", build);
//        });
    }

再次执行,WTF???怎么还没被捕获到?这就给我整不会了

JAVA 复制代码
2023-09-11 00:15:28.498  INFO 19424 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-11 00:15:28.499  INFO 19424 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-11 00:15:28.499  INFO 19424 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-11 00:15:28.507  INFO 19424 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【李华】
2023-09-11 00:15:28.507  INFO 19424 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】
2023-09-11 00:15:28.513 ERROR 19424 --- [         task-1] .a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected exception occurred invoking async method: public void com.example.demo.demos.service.impl.ActionRecordService.saveActionRecordBasicInfo(java.lang.String,java.lang.String)

java.lang.ArithmeticException: / by zero
	at com.example.demo.demos.service.impl.ActionRecordService.saveActionRecordBasicInfo(ActionRecordService.java:30) ~[classes/:na]
	at com.example.demo.demos.service.impl.ActionRecordService$$FastClassBySpringCGLIB$$8317d9f1.invoke(<generated>) ~[classes/:na]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.23.jar:5.3.23]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793) ~[spring-aop-5.3.23.jar:5.3.23]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.23.jar:5.3.23]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.23.jar:5.3.23]
	at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115) ~[spring-aop-5.3.23.jar:5.3.23]
	at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_121]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) ~[na:1.8.0_121]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) ~[na:1.8.0_121]
	at java.lang.Thread.run(Thread.java:745) ~[na:1.8.0_121]
  1. 解决问题

怀着忐忑的心情,我再次问了ChatGTP,得到的意思大概就是,你那段代码是异步执行的,是其他线程抛的异常我全局异常处理器GlobalExceptionHandler可不管。你如果要处理这个异常,你得自己去整个异步线程单独的处理器去管管。所以,没办法,只能使用异步线程的异常处理器来处理:

先定义个自定义的异步异常处理器

JAVA 复制代码
@Slf4j
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
        // 异常处理逻辑,例如返回错误信息
        log.error("在自定义的异步异常处理中,捕获到了异步方法发生异常:{}", throwable.getMessage());
    }
}

再把我们的自定义异常处理器注册到Spring的容器中(这里的线程池参数需要根据实际情况进行调整)

JAVA 复制代码
@Configuration
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(20);
        // 队列容量
        executor.setQueueCapacity(100);
        // 线程池中线程的名称前缀
        executor.setThreadNamePrefix("MyAsyncThread-");
        // 允许的空闲时间
        executor.setKeepAliveSeconds(60);
        // 线程池关闭时等待任务完成的时间
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        // 拒绝策略(默认使用 AbortPolicy,可以根据需要自行调整)
//        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

再次执行代码,进行验证,发现果然生效了,并且线程的名字前缀是我们自定义的MyAsyncThread-

JAVA 复制代码
2023-09-11 00:25:11.641  INFO 20136 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-11 00:25:11.642  INFO 20136 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-11 00:25:11.642  INFO 20136 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-11 00:25:11.647  INFO 20136 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【李华】
2023-09-11 00:25:11.647  INFO 20136 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】
2023-09-11 00:25:11.651 ERROR 20136 --- [MyAsyncThread-1] c.e.d.d.h.CustomAsyncExceptionHandler    : 在自定义的异步异常处理中,捕获到了异步方法发生异常:/ by zero

现在service中是使用了@Async注解的,可以被我们自定义的异常处理器捕获到,如果没错的话,改成execute就不能被异常处理器捕获到,需要手动处理,进行验证一下:

JAVA 复制代码
//    @Async
    public void saveActionRecordBasicInfo(String name, String actionName) {
        threadPoolTaskExecutor.execute(() -> {
            ActionRecord build = ActionRecord.builder().id(123456789L).createBy(getOperatorCode())
                    .name(name).action(actionName).createTime(new Date()).build();
            int i = 1 / 0;
            log.info("添加的操作记录:{}成功", build);
        });
    }

运行结果如下,果然没错,用execute执行的异步代码需要手动捕获,无论是正常的全局异常处理器还是自定义的异步异常处理器都无法捕获这个异常

JAVA 复制代码
2023-09-11 00:27:34.136  INFO 22672 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 添加的用户信息为:【Person(name=李华, age=20, operatorCode=法外狂徒, operatorName=法外狂徒张三)】
2023-09-11 00:27:34.136  INFO 22672 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 保存用户信息...
2023-09-11 00:27:34.137  INFO 22672 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-11 00:27:34.142  INFO 22672 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【李华】
2023-09-11 00:27:34.142  INFO 22672 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【CREATE】
Exception in thread "task-1" java.lang.ArithmeticException: / by zero
	at com.example.demo.demos.service.impl.ActionRecordService.lambda$saveActionRecordBasicInfo$0(ActionRecordService.java:30)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

到这里的时候,就完成了这两种方式处理异常的验证,其中还是有点小坑的,针对于目前的情况,我们可以手动捕获异常,或者在异步异常处理器里面写我们处理异常的代码即可。

如果需要在保存完操作记录之后通知前端的话,可以考虑引入消息相关的技术来实现,这里暂不过多介绍

4. 额外拓展

4.1 AOP可拦截的其他方法

上文实现的是在方法执行成功之后进行拦截,AOP也给我们提供了其他时机,可以让我们在不同的时机做不同的事情,满足我们的需求,还有其他几个时机如下:

JAVA 复制代码
     /**
     * 在目标方法执行之前执行通知。可以用于执行一些前置操作,如权限校验、参数验证等。
     *
     * @param joinPoint
     * @param recordUserAction
     */
    @Before("@annotation(recordUserAction)")
    public void beforeMethodExecution(JoinPoint joinPoint, RecordUserAction recordUserAction) {
        // 前置操作逻辑
        log.info("前置操作逻辑...");
    }

    /**
     * 在目标方法执行之后(包括正常返回和异常返回)执行通知。可以用于执行一些最终操作,如资源释放、日志记录等。
     *
     * @param joinPoint
     * @param recordUserAction
     */
    @After("@annotation(recordUserAction)")
    public void afterMethodExecution(JoinPoint joinPoint, RecordUserAction recordUserAction) {
        // 后置操作逻辑
        log.info("后置操作逻辑...");
    }

    /**
     * 在目标方法抛出异常后执行通知。可以用于捕获并处理异常、记录日志等。
     *
     * @param joinPoint
     * @param recordUserAction
     * @param ex
     */
    @AfterThrowing(value = "@annotation(recordUserAction)", throwing = "ex")
    public void afterThrowingMethodExecution(JoinPoint joinPoint, RecordUserAction recordUserAction, Exception ex) {
        // 异常处理逻辑
        log.info("异常处理逻辑...");
    }

    /**
     * 在目标方法执行前后都执行通知。可以用于包裹目标方法,并可控制是否执行目标方法、修改目标方法的返回值等
     *
     * @param proceedingJoinPoint
     * @param recordUserAction
     * @return
     * @throws Throwable
     */
    @Around("@annotation(recordUserAction)")
    public Object aroundMethodExecution(ProceedingJoinPoint proceedingJoinPoint, RecordUserAction recordUserAction) throws Throwable {
        // 前置操作逻辑
        log.info("前置操作...");
        Object result = proceedingJoinPoint.proceed(); // 执行目标方法
        // 后置操作逻辑
        log.info("后置操作...");
        return result;
    }

运行效果:

JAVA 复制代码
2023-09-12 23:43:22.679  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 前置操作...
2023-09-12 23:43:22.679  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 前置操作逻辑...
2023-09-12 23:43:22.682  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 即将删除的用户为:【张三】
2023-09-12 23:43:22.683  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 删除用户中...
2023-09-12 23:43:22.683  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : doSomethingOther拿到的name:张三2号
2023-09-12 23:43:22.683  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 模拟删除用户的其他相关信息...
2023-09-12 23:43:22.683  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.service.impl.PersonServiceImpl   : 测试public方法是否可以被代理
2023-09-12 23:43:22.683  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 执行方法之后进行的一些操作...
2023-09-12 23:43:22.686  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : aop中获取到的name为:【张三】
2023-09-12 23:43:22.686  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 操作类型为:【DELETE】
2023-09-12 23:43:22.687  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 后置操作逻辑...
2023-09-12 23:43:22.687  INFO 20548 --- [nio-8080-exec-1] c.e.d.d.aspect.UserActionLoggingAspect   : 后置操作...
2023-09-12 23:43:22.687  INFO 20548 --- [         task-1] c.e.d.d.s.impl.ActionRecordService       : 添加的操作记录:ActionRecord(id=123456789, name=张三, action=删除, createTime=Tue Sep 12 23:43:22 CST 2023, createBy=C20230906)成功

5. 个人总结

以前时不时就刷到AOP,虽然看得很多,但是实际上手操作还是第一次,所以把这个过程进行记录。当然了,写完这篇文章的时候也不仅仅只掌握了AOP这个技能,更重要的是代码一步步优化的过程,从最开始的实现功能,到进一步的优化代码,再到代码的复用、模块职责的划分都有了不少收获,这也让自己能够在下次思考问题的时候站在不同的角度进行分析。

源码链接:gitee.com/szq2021/aop...

相关推荐
hai405873 分钟前
Spring Boot中的响应与分层解耦架构
spring boot·后端·架构
Adolf_19931 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼2 小时前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺2 小时前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
新知图书2 小时前
Rust编程的作用域与所有权
开发语言·后端·rust
wn5313 小时前
【Go - 类型断言】
服务器·开发语言·后端·golang
希冀1234 小时前
【操作系统】1.2操作系统的发展与分类
后端
GoppViper4 小时前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文5 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring