从根上讲讲AOP——手敲一个简单的实现

前段时间学习 AOP(面向切面编程) 的知识,翻译了一篇文章:Spring AOP 最热门面试题及答案,没想到发布之后阅读量非常高(看来,标题真的很重要),但其实那篇译文讲得并不是很好,在文章最前面我也说了,只是因为概念介绍比较全面,所以翻译过来加深印象,意想不到的是这篇文章居然在百度“SpringAOP面试”关键词排名第一位

在比较深入的学习了 AOP 的知识并应用于实际开发中,解决了很多问题之后,我就一直打算写几篇关于 AOP 的文章。前几天接到读者来信,说自己很疑惑,不知道AOP究竟是怎么回事,实践工作中也用不上。因此,我觉得更加有必要从根上来讲讲 AOP 究竟是怎么回事,为什么需要 AOP,什么场景下会用到 AOP

一项技术肯定是为了解决特定的问题而生的,可以从“为什么不得不用”,这个角度入手,然后再去探究“有哪些实现”和“实现的原理”。

典型场景举例

有过开发经验的同学,想必对这样的 Controller 方法非常熟悉:
常见的切面应用场景

  1. 收到请求的时候打印请求信息和参数
  2. 记录开始时间
  3. 调用 Service 方法
  4. 调用结束后,打印成功信息和耗时,若发生异常则记录异常信息
  5. 统一的返回结果

我们仔细思考一下,这样的方法中,实际上有用的代码就只有一行

return ResultUtil.successResult(userService.getById(id));

但是我们却用了超过 10 行来写,想像一下,如果你维护的项目有 1000 个接口,那么你写的 10000 行代码中有 9000 行是重复的,多么可怕!DRY!!!

怎么办?(注意体会这里的不得不,我们不是为了用而用,而是有需求无法满足,而不得不使用新的模式或技术)

如果可以这样就好了

仔细观察,我们会发现,像日志、耗时分析、异常处理这样的需求很像夹心饼干:
夹心饼干

这个“夹心饼干”的每一层都是我们的关注点(concern),我们称业务逻辑为核心关注点(core concern),而围绕着它要完成的切面功能,则为横切关注点(cross-cutting concern),如日志、安全、事务等。

如果在代码执行的时候,可以给我们一个通知(Advice)

  1. “大哥,我现在准备要调用UserController.getById这个方法了,参数是 xxx” --前置通知Before
  2. “大哥,UserController.getById这个方法,我用参数 xxx 调用之后正常返回了,结果是xxxx” –返回后通知AfterReturning
  3. “大哥,UserController.getById这个方法,我用参数 xxx 调用之后抛异常了,异常给你,你来决定要继续抛出还是把异常记录下来然后正常返回” –抛出异常后通知AfterThrowing
  4. “大哥,UserController.getById这个方法执行完了,执行状态都在这,您看看还有什么吩咐” –后置通知After

还可以有第五种:“嘿,老兄,UserController.getById这个方法被触发了,参数在这,要不要执行,怎么执行都交给你了” –环绕绕通知Around,简单起见,我们先不做介绍。

如果有了这些通知,我们就可以不用为了记录方法执行信息或者处理异常而重复地写那么多代码了!

可是,Java 没有直接提供这样的通知机制,我们要怎么做才能得到这些通知呢

如何得到通知(Advice)

有一个相对可行的方法,就是代理。

静态代理

目标类

改写上面提到的Controller方法,我们只关心实际的业务代码(简单起见,我们省略注解,先不统计耗时;还有,为后面讲解方便,我们让这个 Controller 实现一个接口):

public interface IUserController {
   
    public ReturnData<User> getById(int id);
}
public class UserController implements IUserController {
   
    @Override
    public ReturnData<User> getById(int id) {
   
     	if (id < 0) {
   
            throw new IllegalArgumentException("id不能为负数");
        }
        return ResultUtil.successResult(userService.getById(id));
    }
}

静态代理类

public class UserControllerProxy implements IUserController {
   
    private UserController userController;

    public UserControllerProxy(UserController userController){
   
        this.userController = userController;
    }
    
    @Override
    public ReturnData<User> getById(int id) {
   
        // 大哥,我现在准备要调用 UserController.getById 这个方法了,参数给你
        before(id);
        ReturnData<User> result = null;
        Throwable throwable = null;
        try {
   
            result = userController.getById(id);
            // 大哥,UserController.getById这个方法,我用参数 xxx 调用之后正常返回了,结果是xxxx
            afterReturning(id, result);
            return result;
        } catch (Throwable t) {
   
        	throwable = t;
            // 大哥,UserController.getById这个方法,我用参数 xxx 调用之后抛异常了,异常给你
            return afterThrowing(id, t);
        } finally {
   
            // 大哥,UserController.getById这个方法执行完了,执行状态都在这,您看看还有什么吩咐
            after(id, result, throwable);
        }
    }
    // 各个通知方法
    ...
}

这样我们就获取到上面提到的各种通知了

方法实现示例

这时我们就可以在各个相应的的方法中写我们的代码了:

public void before(int id) {
   
     log.info("根据主键id获取用户信息开始, id: {}", id);
 }

 public void afterReturning(int id, ReturnData<User> result) {
   
     log.info("根据主键id获取用户信息成功, id: {}, 结果: {}", id, result);
 }

 public ReturnData<User> afterThrowing(int id, Throwable t) {
   
     log.error("据主键id获取用户信息发生异常, id: {}", id, t);
     // 统一异常返回值
     return new ReturnData<>(0, "据主键id获取用户发生异常, id: " + id);
 }

 public void after(int id, ReturnData<User> result, Throwable t) {
   
     log.info("据主键id获取用户信息方法调用结束 id: {},是否成功: {}", id, t == null);
 }

写个测试方法:

@Test
public void invokeStaticProxy
Spring AOP是Spring框架中的一个重要模块,它提供了面向切面编程(AOP)的支持。AOP是一种编程思想,它可以在不改变原有代码的情况下,通过在程序运行时动态地将代码“织入”到现有代码中,从而实现对原有代码的增强。 Spring AOP提供了基于注解的AOP实现,使得开发者可以通过注解的方式来定义切面、切点和通知等相关内容,从而简化了AOP的使用。 下面是一个基于注解的AOP实现的例子: 1. 定义切面类 ```java @Aspect @Component public class LogAspect { @Pointcut("@annotation(Log)") public void logPointcut() {} @Before("logPointcut()") public void beforeLog(JoinPoint joinPoint) { // 前置通知 System.out.println("执行方法:" + joinPoint.getSignature().getName()); } @AfterReturning("logPointcut()") public void afterLog(JoinPoint joinPoint) { // 后置通知 System.out.println("方法执行完成:" + joinPoint.getSignature().getName()); } @AfterThrowing(pointcut = "logPointcut()", throwing = "ex") public void afterThrowingLog(JoinPoint joinPoint, Exception ex) { // 异常通知 System.out.println("方法执行异常:" + joinPoint.getSignature().getName() + ",异常信息:" + ex.getMessage()); } } ``` 2. 定义业务逻辑类 ```java @Service public class UserService { @Log public void addUser(User user) { // 添加用户 System.out.println("添加用户:" + user.getName()); } @Log public void deleteUser(String userId) { // 删除用户 System.out.println("删除用户:" + userId); throw new RuntimeException("删除用户异常"); } } ``` 3. 在配置文件中开启AOP ```xml <aop:aspectj-autoproxy/> <context:component-scan base-package="com.example"/> ``` 在这个例子中,我们定义了一个切面类LogAspect,其中通过@Aspect注解定义了一个切面,通过@Pointcut注解定义了一个切点,通过@Before、@AfterReturning和@AfterThrowing注解分别定义了前置通知、后置通知和异常通知。 在业务逻辑类中,我们通过@Log注解标注了需要增强的方法。 最后,在配置文件中,我们通过<aop:aspectj-autoproxy/>开启了AOP功能,并通过<context:component-scan>扫描了指定包下的所有组件。 这样,当我们调用UserService中的方法时,就会触发LogAspect中定义的通知,从而实现对原有代码的增强。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值