美文网首页spring
Spring AOP源码解析(一)

Spring AOP源码解析(一)

作者: anyoptional | 来源:发表于2021-04-20 09:28 被阅读0次

    引言

      spring-aop作为Spring生态中的基础模块,发挥着举足轻重的作用。spring-framework内部大量使用它来提供声明式的企业级服务,其中最为开发者熟知的莫过于spring-cachespring-tx了。

      学习spring-aop的关键在于掌握它的设计与实现。然而AOP相关概念甚多,抽象也颇为复杂,如果不建立一个全局视角就直接钻到源码里去的话,很容易被劝退。本文作为spring-aop源码解析系列的开篇(5.2.6.RELEASE),就和大家一起好好聊聊spring-aop的抽象体系。我们的目标是妈妈再也不用担心我不理解AOP了,话不多说,我们开始吧~~

    Why Spring AOP

      首先,让我们思考一下为什么要使用spring-aop?为了回答这个问题,假设我们在开发一个计算器程序:

    public interface Calculator {
        int calculate(int x, int y);    
    }
    
    public class AddCalculator implements Calculator {
        @Override
        public int calculate(int x, int y) {
            return x + y;
        }
    }
    
    public class SubCalculator implements Calculator {
        @Override
        public int calculate(int x, int y) {
            return x - y;
        }
    }
    

    现在,如果需要做性能统计,该怎么办呢?朴素的思路是为每个实现都单独添加一段统计耗时的逻辑,但这样不仅违背了开闭原则并且不可避免的会产生重复代码,更重要的是后续在引入MultipleCalculator等其它计算类型时,同样需要拷贝粘贴这一段逻辑。好一点的思路是使用装饰器模式

    public class TimingCalculator implements Calculator {
        
        private final Calculator calculator;
        
        public TimingCalculator(Calculator calculator) {
            this.calculator = calculator;
        }
        
        @Override
        public int calculate(int x, int y) {
            long start = System.currentTimeMillis();
            int result = calculator.calculate(x, y);
            long end = System.currentTimeMillis();
            System.out.println("time in millis: " + (end - start));
            return result;
        }
      
        public static void main(String[] args) {
            Calculator calculator = new TimingCalculator(new AddCalculator());
            // 控制台输出耗时
            calculator.calculate(3, 5);
        }
    }
    

    Better!但这种思路同样有一些缺陷:

    1. 需要进行包装,不那么直观,无形中增加了API使用者的心智负担
    2. 耦合度高,适用范围窄,只能用于Calculator,其它类型需要添加对应的装饰器

    可这种模式又是如此通用,以至于JDK为我们提供了一种优化思路——动态代理。

    public class TimingProxyFactory {
    
        public static Object getProxy(Object target, Class<?>[] ifcs) {
            ClassLoader cl = target.getClass().getClassLoader();
            return Proxy.newProxyInstance(cl, ifcs, new TimingInvocationHandler(target));
        }
    
        private static class TimingInvocationHandler implements InvocationHandler {
    
            private final Object target;
    
            public TimingInvocationHandler(Object target) {
                this.target = target;
            }
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // before
                long start = System.currentTimeMillis();
                try {
                    Object result = method.invoke(target, args);
                    // after returning
                    return result;
                } catch (Throwable throwable) {
                    // after throwing
                    throw throwable;
                } finally {
                    // after
                    long end = System.currentTimeMillis();
                    System.out.println("time in millis: " + (end - start));
                }
            }
        }
    
        public static void main(String[] args) {
            Calculator calculator = (Calculator) TimingProxyFactory.getProxy(new AddCalculator(),
                    new Class[]{Calculator.class});
            // 控制台输出耗时
            calculator.calculate(3, 5);
        }
    }
    

    Better!JDK动态代理的出现,解决了代码耦合的问题,即使Calculator中新增了方法,也可以在不附加任何修改的前提下就获得统计耗时的功能。更令人兴奋的是适用性得到了极大的增强,TimingProxyFactory不再只服务于Calculator,而可以是任意类型的接口。凡事都有两面性,JDK动态代理固然有诸多利好,却也不是没有缺点,它

    1. 没有解决调用时的包装问题,使用方不能无感地使用代理功能
    2. 只能针对接口进行代理,不能对类进行代理

    如果使用spring-aop呢?

    @Aspect
    @Component
    public class TimingAspect {
        // 对com.anyoptional.aop包下的所有类的所有方法进行增强,
        // 统一为它们增加性能统计功能,而不再局限于某些接口,
        // 适用性得到进一步提升
        @Around("execution(* com.anyoptional.aop.*.*(*, ..))")
        public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
            long start = System.currentTimeMillis();
            Object result = joinPoint.proceed();
            long end = System.currentTimeMillis();
            System.out.println("time in millis: " + (end - start));
            return result;
        }
    }
    
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class TimingAspectTest {
        @Autowired
        Calculator addCalculator;
    
        @Test
        public void contextLoad() {
            // 控制台输出耗时
            addCalculator.calculate(3, 5);
        }
    }
    

    Best!spring-aopSpring IoC的结合让业务逻辑和性能统计彻底解耦,使用者可以无感地使用增强后的Calculator实例,并且Calculator也不再局限为接口,spring-aop会根据Calculator的类型智能地选择生成JDK动态代理或使用CGLIB动态生成子类。同时,切面不仅仅只对某些类型生效,而可以是com.anyoptional.aop包下所有类的所有方法。通过使用AspectJ切面表达式,可以轻松横跨多种类型和多个对象,适用性得到进一步提升。

    AOP Concepts

      相信通过上面的小例子,我们能够感受到传统的OOP范式不能很好地封装一些贯穿全局的辅助逻辑,比如示例中的性能统计,又或者权限校验、事务管理等等。而使用spring-aop,能够在不侵入业务逻辑的基础上对原业务进行增强,并且可以很方便的在所有业务逻辑中生效。

      说了这么多,那什么是AOP呢?Spring官网给出了解释:

    Aspect-oriented Programming (AOP) complements Object-oriented Programming (OOP) by providing 
    another way of thinking about program structure. The key unit of modularity in OOP is the 
    class, whereas in AOP the unit of modularity is the aspect. Aspects enable the modularization
    of concerns (such as transaction management) that cut across multiple types and objects. 
    (Such concerns are often termed “crosscutting” concerns in AOP literature.)
    

    面向切面编程(Aspect-oriented Programming)通过提供对程序结构的另一种思维方式来补充面向对象编程(OOP)。在OOP编程范式中,模块化的核心单元是类,而在AOP编程范式中,模块化的核心单元是切面。切面,使得跨越多种类型和对象的问题得到模块化。

      一脸懵逼?没关系,让我们用人话来解读一下AOP相关术语吧。

    • 关注点(Concern):指系统中基于功能划分出的一部分,比如示例中的计算器和对它的性能统计

    • 横切关注点(Crosscutting concerns):部分关注点横切程序中的多个模块,即施加在多个需要的模块上,它们就被称作横切关注点,比如示例中的性能统计

    • 连接点(Join point):程序运行过程中的某个点,比如方法调用或处理异常。在spring-aop中,连接点始终代表方法调用,换句话说spring-aop是基于方法拦截的,比如示例中拦截了对位于com.anyoptional.aop包下所有类、所有方法的调用,这里,方法调用就是连接点

    • 增强(Advice):在特定连接点上由某个切面持有的动作,比如示例中的doAround(...),它为连接点添加了性能统计功能,我们就说它对连接点进行了增强。Advice的类型包括AroundBeforeAfterspring-aopAdvice建模为拦截器,并在连接点上维护了一个拦截器链

    • 引介(Introduction):动态地给类添加方法或字段。spring-aop允许我们动态地给对象引入新的接口及其实现。这个示例中就没有啦,它有点像Swift中的extension ,只不过extension发生在编译期,更确切地说像ObjectiveC runtime中的class_addMethod(...)objc_setAssociatedObject(...)

    • 切点(Pointcut):匹配连接点的谓词。Advice与切点表达式相关联,并对任何匹配的连接点进行增强,比如示例中的切点选取了位于com.anyoptional.aop包下的所有类的所有方法

    • 切面(Aspect):对跨越多个类型的关注点的模块化。比如示例中的性能统计切面, 它聚合了切点和增强,可以对匹配的任何连接点应用增强

    • 目标对象(Target object):被一个或多个切面增强的对象。由于spring-aop是基于运行时动态代理的,因此目标对象就是被代理对象,比如示例中的AddCalculator实例

    现在是不是稍微清晰一点了?

    Spring AOP Abstraction

      AOP的核心概念并不是Spring独有的,而是一个广泛使用的概念。在实现上,spring-aop确实也引入了一些特有的概念,我们继续说道说道。

    MethodInvocation

      spring-aop目前只支持方法调用(MethodInvocation)这一种Join pointMethodInvocation是对方法调用上下文的封装。

    public interface Joinpoint {
        /**
         * 执行拦截器链中的下一个拦截器,类似于Servlet中FilterChain的doFilter(...)
         */
        Object proceed() throws Throwable;
    
        /**
         * 持有static part的对象,比如对方法调用来说就是这个方法所属的实例,也就是被代理对象
         */
        Object getThis();
    
        /**
         * static part 是拦截器链作用的某个AccessibleObject,比如对方法调用来说就是那个方法
         */
        AccessibleObject getStaticPart();
    }
    
    public interface Invocation extends Joinpoint {
        /**
         * 获取参数信息,比如方法的参数
         * 对返回数组中元素的修改会反应到实际的参数上去,持有引用嘛,懂的都懂
         */
        Object[] getArguments();
    }
    
    public interface MethodInvocation extends Invocation {
        /**
         * 获取即将被调用的方法,类型更加具体的#getStaticPart()版本
         */
        Method getMethod();
    }
    
    Pointcut

      前文提到,Pointcut是用来匹配Join point的谓词,因此spring-aopPointcut的建模是围绕如何匹配方法来展开的。我们知道,Java中方法是不能脱离类而单独存在的,所以Pointcut也自然地分为了ClassFilterMethodMatcher两部分。

    public interface Pointcut {
        /**
         * 类型过滤器,首先过滤类型,因为Java中方法是附加在类上的,不能单独存在。
         */
        ClassFilter getClassFilter();
    
        /**
       * 方法匹配器,可以根据方法名、方法上的注解等信息来确定该方法
       * 是不是一个需要被增强的连接点
         */
        MethodMatcher getMethodMatcher();
    }
    
    public interface ClassFilter {
        /**
         * 判断给定的类或接口能否被切点所匹配
         */
        boolean matches(Class<?> clazz);
    }
    
    public interface MethodMatcher {
        /**
         * 判断给定的方法能否被切点所匹配
         * 这里的判定是静态判定,也就是不涉及方法参数,因为参数只有在触发调用的那一刻才能真正确定下来
         */
        boolean matches(Method method, Class<?> targetClass);
    
        /**
       * 返回true,表示需要进行进一步的运行时判定,具体来说,就是额外检查方法参数
         */
        boolean isRuntime();
    
        /**
       * 判断给定的方法能否被切点所匹配
       * 这里的判定是动态判定,只在静态判定成功和isRuntime()返回true的基础上才会触发
         */
        boolean matches(Method method, Class<?> targetClass, Object... args);
    }
    

    spring-aop提供了很多开箱即用的Pointcut实现,其中最常用的是AnnotationMatchingPointcutAspectJExpressionPointcut。前者根据类和方法上的注解信息执行匹配,比如@Async开启异步执行、@Transactional开启本地事务;后者通过AspectJ切点表达式来执行匹配,比如示例中对com.anyoptional.aop包下的所有类、所有方法的匹配。

    Hierarchy of Advice

      Advice表示在连接点上发生的动作。由于spring-aop只支持MethodInvocation,因此将Advice建模为方法拦截器(MethodInterceptor)就再合适不过了。

    // 标记接口,表示这是一个Advice
    public interface Advice {
    }
    // 同样是一个标记接口,表示这是一个拦截器
    public interface Interceptor extends Advice {
    }
    // 方法拦截器,拦截具体的方法
    public interface MethodInterceptor extends Interceptor {
        /**
         * 拦截方法调用,可以在方法调用前、后执行自定义逻辑,通过调用Joinpoint#proceed()转到拦截器链上的下一个实例
         */
        Object invoke(MethodInvocation invocation) throws Throwable;
    }
    

    MethodInterceptor的功能有点过于强大,它可以在方法执行前、执行后甚至是抛出异常时执行自定义逻辑,相当于AspectJ中的Around Advice(也就是所谓的环绕增强)。为了避免滥用,spring-aop设计了几种窄化的Advice,窄化后的Advice只能在方法执行的特定时机对它进行增强。

    // 目标方法执行前进行增强
    public interface BeforeAdvice extends Advice {
    }
    // 目标方法执行后进行增强
    // 方法执行完有两种可能的结果
    // 1. 正常结束
    // 2. 异常退出
    public interface AfterAdvice extends Advice {
    }
    // 对应方法正常结束
    public interface AfterReturningAdvice extends AfterAdvice {
      // 方法正常结束后进行增强,此时可以获取到方法返回值,如果有的话
        void afterReturning(@Nullable Object returnValue, Method method, 
                          Object[] args, @Nullable Object target) throws Throwable;
    }
    // 对应方法异常退出
    public interface ThrowsAdvice extends AfterAdvice {
            // ThrowsAdvice没有定义任何方法,它的方法是根据约定通过反射调用的,可能的形式是如下两种
        // 1. public void afterThrowing(Exception ex),ex可以是任意类型的异常
        // 2. public void afterThrowing(Method method, Object[] args, Object target, Exception ex),前三个参数分别是方法、方法参数和目标对象,最后一个参数是异常信息,同样可以是任意类型的异常
    }
    
    TargetSource

      spring-aop使用TargetSouece来获取目标对象(target object)。TargetSource作为工厂接口可以屏蔽目标对象的获取细节,真实的目标对象可能来自对象池也可能就是一个单例对象。

    public interface TargetClassAware {
        /**
         * 返回目标对象的真实类型
         */
        @Nullable
        Class<?> getTargetClass();
    }
    
    public interface TargetSource extends TargetClassAware {
        /**
         * 返回true,表示多次调用#getTarget()获取的是同一个对象,因此也就没必要调用#releaseTarget(...)了
         */
        boolean isStatic();
    
        /**
         * 返回目标对象,目标对象是持有连接点的对象,也是被代理的对象
         */
        @Nullable
        Object getTarget() throws Exception;
    
        /**
         * 释放通过#getTarget()获取的目标对象
         */
        void releaseTarget(Object target) throws Exception;
    }
    
    Advisor

      Advisor作为Advice的持有者,通常来说会额外携带一个过滤器来判定Advice的适用性,比如PointcutIntroduction使用的很少,我们就不讲它了)。换言之,Advisor表达的是可以对哪些类型的哪些方法进行增强。

    public interface Advisor {
        /**
         * 持有的Advice,可能是MethodInterceptor、BeforeAdvice,、ThrowsAdvice等等
         */
        Advice getAdvice();
    
        /**
         * 在spring-aop中未被使用,可忽略
         */
        boolean isPerInstance();
    }
    
    // PointcutAdvisor几乎涵盖了spring-aop中所有的Advisor类型
    public interface PointcutAdvisor extends Advisor {
        /**
         * 获取驱动此Advisor的切点
         */
        Pointcut getPointcut();
    }
    
    Advised

      Advised被设计来保存AOP配置信息,比如目标对象是谁?是使用JDK动态代理还是CGLIB?如果使用JDK动态代理,需要被代理的接口有哪些?围绕目标对象的Advisor有哪些?相信你也看出来了,这个接口是留给创建代理的工厂实现的,只有在掌握了这些信息,创建出的代理才可能是符合需求的。

    public interface Advised extends TargetClassAware {
        /**
         * AOP配置是否已冻结,如果是,后续就不能修改Advice了
         * isFrozen()的主要作用是给spring-aop一个hint,框架可以借此执行一些优化
         */
        boolean isFrozen();
    
        /**
         * 使用jdk动态代理还是cglib
         */
        boolean isProxyTargetClass();
    
        /**
         * 返回所有需要被代理的接口
         */
        Class<?>[] getProxiedInterfaces();
    
        /**
         * 检测某个接口是否被代理
         */
        boolean isInterfaceProxied(Class<?> intf);
    
        /**
         * 设置获取目标对象的工厂,也就是当前Advised作用的对象
         */
        void setTargetSource(TargetSource targetSource);
    
        /**
         * Return the {@code TargetSource} used by this {@code Advised} object.
         */
        TargetSource getTargetSource();
    
        /**
         * 设置是否暴露代理对象到ThreadLocal中,如果是,后续可通过AopContext获取
         */
        void setExposeProxy(boolean exposeProxy);
    
        /**
         * 是否暴露代理对象到ThreadLocal中
         */
        boolean isExposeProxy();
    
        /**
         * 设置当前AOP配置信息是否已被超前过滤过了,如果是的话,可以不执行Pointcut持有的ClassFilter
         */
        void setPreFiltered(boolean preFiltered);
    
        /**
       * 返回当前AOP配置信息是否已被超前过滤过
         */
        boolean isPreFiltered();
    
        /**
         * 返回所有配置给目标对象的Advisor,这些Advisor将组成拦截器链作用在连接点上
         */
        Advisor[] getAdvisors();
    
        /**
       * Advisor的增删改查
         */
        void addAdvisor(Advisor advisor) throws AopConfigException;
    
        void addAdvisor(int pos, Advisor advisor) throws AopConfigException;
    
        boolean removeAdvisor(Advisor advisor);
    
        void removeAdvisor(int index) throws AopConfigException;
    
        int indexOf(Advisor advisor);
    
        boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;
    
        void addAdvice(Advice advice) throws AopConfigException;
    
        void addAdvice(int pos, Advice advice) throws AopConfigException;
    
        boolean removeAdvice(Advice advice);
    
        int indexOf(Advice advice);
    
        /**
         * 因为toString()最终会作用在目标对象上,因此提供了toProxyConfigString()来返回配置信息的文本描述
         */
        String toProxyConfigString();
    }
    

    结语

      以上就是对spring-aop相关概念的解读了,相信耐心看完的你一定有所收货。下一篇的话我们一起研究研究Spring到底是如何执行织入的吧~~

      喔,对了,织入(Weaving)是把切面应用到目标对象并创建新的代理对象的过程。spring-aop的织入依赖于BeanPostProcessor,理解了spring-aop是如何执行织入的,也就从根上理解了spring-aop。好啦,我们下篇再见。

    相关文章

      网友评论

        本文标题:Spring AOP源码解析(一)

        本文链接:https://www.haomeiwen.com/subject/thrmlltx.html