从根上讲讲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() {
   	// 生成目标对象
    UserController target = new UserController();
    System.out.println("静态代理调用");
    // 生成代理对象
    IUserController userControllerStaticProxy = new UserControllerProxy(target);
    // 正常调用
    ReturnData<User> returnData = userControllerStaticProxy.getById(12);
    System.out.println("调用结果:" + returnData);
    System.out.println("=====================");
    // 负数,正常会抛出异常,但是在我们的代理对象中做了处理
    returnData = userControllerStaticProxy.getById(-12);
    System.out.println("调用结果:" + returnData);
}

结果:
静态代理调用结果
可以看到,调用的信息都打出来了,而且异常也被捕获了,并返回了统一的返回值。

通过静态代理,我们将日志和异常处理从业务逻辑代码中剥离出来,使方法职责划分更加清晰

然而,从上面的实现我们也看到了,之前 10 行代码可以完成的功能,通过静态代理却用了超过 40 行,还额外增加了一个类,而且静态代理不具有通用性,以后每次增加新的类都得手动写一个代理类。这肯定是无法让人接受的。

有没有一种方式可以只写一次日志和异常处理方法,然后可以作用于任意的类呢?(注意,这里也是一个“不得不”)

万能代理

想象一下,如果可以接管一个方法的执行过程,换句话说,假如现在有一个万能的代理类,这个代理类可以代理任意的类,当有方法被触发的时候,它将这个方法的相关信息回调给我们提供的方法调用处理器,那不就可以解决我们上面的问题了吗?
万能代理类
那么有没有这个万能代理类的存在呢?或者有没有办法实现这个机制呢?

JDK动态代理

JDK提供了动态代理的功能,我们只需要提供一个方法调用处理器 java.lang.reflect.InvocationHandler 的实现类,然后调用 java.lang.reflect.Proxy.newProxyInstance 就可以生成一个动态代理类了。

方法处理器:

public class MethodInvokeHandler implements InvocationHandler {
    private static final Logger log = LoggerFactory.getLogger(MethodInvokeHandler.class)
    /**
     * 被代理的目标对象
     */
    private Object target;

    public MethodInvokeHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 大哥,我现在准备要调用 method 这个方法了,参数是 args
        before(method, args);
        Object result = null;
        Throwable throwable = null;
        try {
            // 通过反射调用目标方法
            result = method.invoke(target, args);
            // 大哥,method 这个方法,我用参数 args 调用之后正常返回了,结果是 result
            afterReturning(method, args, result);
            return result;
        } catch (Throwable t) {
        	throwable = t;
            // 大哥,method 这个方法,我用参数 args 调用之后抛异常了,异常是 t
            return afterThrowing(method, args, t);
        } finally {
            // 大哥,method 这个方法执行完了,执行状态都在这,您看看还有什么吩咐
            after(method, args, result, throwable);
        }
    }

   @Override
    public void before(Method method, Object[] args) {
        String methodName = method.getDeclaringClass().getName() + "." + method.getName();
        log.info("方法调用开始,方法: {}, 参数: {}", methodName, Arrays.toString(args));
    }

    @Override
    public void afterReturning(Method method, Object[] args, Object result) {
        String methodName = method.getDeclaringClass().getName() + "." + method.getName();
        log.debug("方法调用成功, 方法: {}({}), 结果: {}", methodName, Arrays.toString(args), result);
    }

    @Override
    public Object afterThrowing(Method method, Object[] args, Throwable t) {
        String methodName = method.getDeclaringClass().getName() + "." + method.getName();
        log.error("方法调用发生异常, 方法: {}({})", methodName, Arrays.toString(args), t);
        // 统一异常返回值
        return new ReturnData<>(-1, "方法调用发生异常");
    }

    @Override
    public void after(Method method, Object[] args, Object result, Throwable t) {
        String methodName = method.getDeclaringClass().getName() + "." + method.getName();
        log.info("方法调用结束,方法: {}, 是否成功: {}", methodName, t == null);
    }
}

调用方法:

@Test
public void invokeDynamicProxy() {
    UserController target = new UserController();
    IUserController userControllerDynamicProxy = (IUserController) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{IUserController.class}, new MethodInvokeHandler(target));
    ReturnData<User> returnData = userControllerDynamicProxy.getById(12);
    System.out.println("调用结果:" + returnData);
    System.out.println("========================");
    returnData = userControllerDynamicProxy.getById(-12);
    System.out.println("调用结果:" + returnData)
}

输出结果:
动态代理调用结果
从输出日志中,我们看到,方法被调用的过程,都被记录下来了,而且异常信息也被记录之后改为统一的返回结果。

然而,使用 JDK 动态代理有一个致命的缺陷——它只能代理接口。也就是你的目标类必须实现接口,然后对这个接口进行代理,否则它就无能为力了。实践中,我们的Controller一般是没有实现接口的。

CGLIB(Code Generation Library)

CGLIB是一个功能强大,高性能而且高质量的代码生成库。它被用于在运行时继承类和实现接口。它可以通过继承为没有实现接口的类提供代理的能力,为JDK的动态代理提供了很好的补充。

用 CGLIB 来实现切面的功能,其实跟动态代理是一样的,区别在于动态代理只能代理接口,而 CGLIB 则即可以代理接口也可以通过继承的方式直接代理一个类。

我们先来看看,咱们手写一个通过继承实现的静态代理:

public class UserControllerProxy extends UserController {
    private UserController userController;

    public UserControllerProxy(UserController userController){
        this.userController = userController;
    }
    
    @Override
    public ReturnData<User> getById(int id) {
        before(id);
        ReturnData<User> result = null;
        Throwable throwable = null;
        try {
            result = userController.getById(id);
            afterReturning(id, result);
            return result;
        } catch (Throwable t) {
        	throwable = t;
            return afterThrowing(id, t);
        } finally {
            after(id, result, throwable);
        }
    }
    // 此处省略
    ...
}

跟前面的例子唯一的不同是,这里通过继承被代理的类而不是实现接口extends UserController

那么,我们如何像动态代理那样,生成一个通用的代理类呢?CGLIB 这时就要隆重登场了!

与JDK动态代理一样,CGLIB 为我们提供了通过继承实现的动态代理的功能,我们只需要提供一个方法调用处理器 net.sf.cglib.proxy.MethodInterceptor 的实现类,然后设置为 Enhancer 的 callback 对象,即可通过enhancer.create() 生成动态代理类。

定义方法拦截器:

public class MethodCallBack implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        before(method, args);
        Object result = null;
        Throwable throwable = null;
        try {
            result = proxy.invokeSuper(obj, args);
            afterReturning(method, args, result);
            return result;
        } catch (Throwable t) {
        	throwable = t;
            return afterThrowing(method, args, t);
        } finally {
            after(method, args, result, throwable);
        }
    }
    // 各个通知方法省略,跟前面的例子一样
}

使用 Enhancer 生成代理类:

@Test
public void invokeCglib() {
    Enhancer enhancer = new Enhancer();
    // 设置目标对象为父类
    enhancer.setSuperclass(UserController.class);
    // 设置回调方法
    enhancer.setCallback(new MethodCallBack());
    // 创建代理对象
    UserController userControllerCglibProxy = (UserController) enhancer.create();
    ReturnData<User> returnData = userControllerCglibProxy.getById(12);
    System.out.println("调用结果:" + returnData);
    System.out.println("========================");
    returnData = userControllerCglibProxy.getById(-12);
    System.out.println("调用结果:" + returnData);
}

输出结果:
CGLIB调用结果
可以看到,效果跟动态代理是一样的。

更加通用化

在这个基础上,如果我们将分发通知的方法和各个通知方法分离开来,就可以实现简单通用的 AOP 机制了

注意:这里我们为了便于理解 AOP 的本质因此采用较简单的实现方式,SpringAOP 的通知机制是将每一个类型的通知都封装为一个对象,形成一个通知链,采用链式调用的方式实现的,与本例并不相同,但是原理上是想通的都是一种实现通知的方式

首先,将通知方法抽离为一个接口:

/**
 * @author dadiyang
 * @since 2019-08-14
 */
public interface Advisor {
    /**
     * 方法执行前通知
     */
    void before(Method method, Object[] args);
    /**
     * 方法执行正常返回通知
     */
    void afterReturning(Method method, Object[] args, Object result);
    /**
     * 方法抛出异常时通知
     */
    Object afterThrowing(Method method, Object[] args, Throwable t);
    /**
     * 方法执行结束后(无论正常返回还是抛出异常)通知
     */
    void after(Method method, Object[] args, Object result, Throwable e);
}

然后是动态代理的 invoke 方法抽离为单独的类:

/**
 * 用于在方法执行前后调用各个通知方法
 * @author dadiyang
 * @since 2019-08-14
 */
public class MethodInvokeHandler implements InvocationHandler {
    protected Object target;
    private final Advisor advisor;

    public MethodInvokeHandler(Object target, Advisor advisor) {
        this.target = target;
        this.advisor = advisor;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 大哥,我现在准备要调用 method 这个方法了,参数是 args
        advisor.before(method, args);
        Object result = null;
        Throwable throwable = null;
        try {
            result = method.invoke(target, args);
            // 大哥,method 这个方法,我用参数 args 调用之后正常返回了,结果是 result
            advisor.afterReturning(method, args, result);
            return result;
        } catch (Throwable t) {
        	throwable = t;
            // 大哥,method 这个方法,我用参数 args 调用之后抛异常了,异常是 t
            return advisor.afterThrowing(method, args, t);
        } finally {
            // 大哥,method 这个方法执行完了,执行状态都在这,您看看还有什么吩咐
            advisor.after(method, args, result, throwable);
        }
    }
}

而 CGLIB 的方法处理器也单独抽离出来:

public class MethodCallBack implements MethodInterceptor {
    private Advisor advisor;

    public MethodCallBack(Advisor advisor) {
        this.advisor = advisor;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 大哥,我现在准备要调用 method 这个方法了,参数是 args
        advisor.before(method, args);
        Object result = null;
        Throwable throwable = null;
        try {
            result = proxy.invokeSuper(obj, args);
            // 大哥,method 这个方法,我用参数 args 调用之后正常返回了,结果是 result
            advisor.afterReturning(method, args, result);
            return result;
        } catch (Throwable t) {
        	throwable = t;
            // 大哥,method 这个方法,我用参数 args 调用之后抛异常了,异常是 t
            return advisor.afterThrowing(method, args, t);
        } finally {
            // 大哥,method 这个方法执行完了,执行状态都在这,您看看还有什么吩咐
            advisor.after(method, args, result, e);
        }
    }
}

这样,当我们需要在任何的方法执行前后做任何的操作,就可以通过实现 Advisor 接口来写我们的横切关注点的逻辑了(我们其实并不总是需要所有的通知,可以将接口方法设置为 default,并提供空实现,或者为每个通知类型定义一个独立的接口,这样可以按需实现)。例如,我们写一个通知器来实现上面的 MethodInvokeHandler 的功能:

/**
 * 实现记录方法调用追踪信息
 *
 * @author dadiyang
 * @since 2019-08-15
 */
public class MethodTraceAdvisor implements Advisor {
    private static final Logger log = LoggerFactory.getLogger(MethodInvokeHandler.class);

    @Override
    public void before(Method method, Object[] args) {
        String methodName = method.getDeclaringClass().getName() + "." + method.getName();
        log.info("方法调用开始,方法: {}, 参数: {}", methodName, Arrays.toString(args));
    }

    @Override
    public void afterReturning(Method method, Object[] args, Object result) {
        String methodName = method.getDeclaringClass().getName() + "." + method.getName();
        log.debug("方法调用成功, 方法: {}, 参数: {}, 结果: {}", methodName, Arrays.toString(args), result);
    }

    @Override
    public Object afterThrowing(Method method, Object[] args, Throwable t) throws Throwable {
        String methodName = method.getDeclaringClass().getName() + "." + method.getName();
        log.error("方法调用发生异常, 方法: {}, 参数 {}", methodName, Arrays.toString(args), t);
        // 统一异常返回值
        throw t;
    }

    @Override
    public void after(Method method, Object[] args, Object result, Throwable t) {
        String methodName = method.getDeclaringClass().getName() + "." + method.getName();
        log.error("方法调用结束, 方法: {}, 参数 {}", methodName, Arrays.toString(args));
    }
}

然后传给 Proxy.newInstance:

UserController target = new UserController();
IUserController userController = (IUserController) Proxy.newProxyInstance(
			Main.class.getClassLoader(), 
			new Class[]{IUserController.class}, 
			new MethodInvokeHandler(target, advisor));
System.out.println(userController.getById(12));

或者使用 CGLIB:

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserController.class);
enhancer.setCallback(new MethodCallBack(advisor));
IUserController userController = (IUserController) enhancer.create();
System.out.println(userController.getById(12));

实现的效果是一样的,有了这两种机制,我们可以代理所有我们想代理的类(有些特殊情况除外)。这样,我们就有了一个通用的切面方法调用处理器了。

总结

至此,我们就知道了 AOP 的两种底层实现技术:JDK 动态代理和 CGLIB,这也是 SpringAOP 的底层实现原理。SpringAOP 默认在目标类有实现接口时使用标准的动态代理,在没有实现接口时,使用 CGLIB 代理。而且提供了配置方式强制使用 CGLIB 代理,即 @EnableAspectJAutoProxy(proxyTargetClass = true)

具体的通知实现方式上,SpringAOP 的通知方式并不是我们上面演示的,一个接口实现所有的通知,而是将所有的通知都包装成一个通知对象,然后组成一条调用链,在合适的时机调用这条链的每个通知。具体的源码解析建议可以看一下这个视频:https://www.bilibili.com/video/av32102436/?p=27 以后有机会,我们再来探讨。

有了上面这些知识了解,建议再看一下Spring官方文档 https://docs.spring.io/spring/docs/5.1.2.RELEASE/spring-framework-reference/core.html#aop 然后自己写一个简单的程序来验证一下。

参数文献

  • https://stackoverflow.com/questions/23700540/cross-cutting-concern-example
  • https://www.bilibili.com/video/av32102436/?p=27
  • https://blog.csdn.net/dadiyang/article/details/82920139
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值