java(spring)手把手教你写个AOP框架

来源:互联网 发布:时差7小时 知乎 编辑:程序博客网 时间:2024/06/06 05:12
  • Why AOP?
  • AOP基本术语
  • AOP执行流程
  • 动手实现一个AOP
  • 源代码
  •  

    Why AOP?

     

    AOP(Aspect-Oriented Programming),意思是面向切面编程。传统的OOP面向对象相当于站在一个上帝模式从上往下看,里面的一块块都是一个对象,由我任意组合;而AOP不同之处在于,他是以一个旁观者的身法,从“侧面”看整个系统模块,看看哪里可以见缝插针,将自己想要处理的一段业务逻辑“编织”进去。


    Code duplication is the ultimate code smell. It’s a sign that something is very wrong with implementation or design.(重复的代码会让代码的质量很糟糕。如果出现这个状况,那么一定是实现或者设计环境出了问题)


    OOP本身是极力反对“重复发明轮子”的,但是有时却对重复的代码显得无可奈何,而AOP本身是一种很好的能解决这个问题的一种思想。

    抽象了半天,还是利用一个例子还更加形象的解释吧。如果你要做一个权限系统,那么肯定需要在很多业务逻辑之前都加上一个权限判断——只有符合条件的才能完成后面的操作。如果利用传统思想,很显然你会把做权限判断的业务逻辑做封装,然后在每个业务逻辑执行之前都执行以下那片处理权限判断的代码。如下图:

    看到没,每次一个判断每次一个判断,如果让这些权限判断的代码散落在系统的各个角落,那会是一个噩梦!就算采用OOP思想,将权限检查的业务放在一个类中,照样无济于事。因为每段业务代码开头总有这么一段抹不掉的身影(doSecurityCheck)。

    这时,AOP老兄终于按耐不住,要出场大展身手了!这位老兄马上说,放着那段业务逻辑代码,我来处理!

    他首先将权限处理的部分视作一个aspect(切面),然后想办法在运行时把切面weave(编织)进业务逻辑中合适的位置。比如就像这样做:

    这样,AOP就成功的帮我把权限验证部分插入到调用代码的前面执行。具体调用哪个方法其实AOP并不知道,只要你把切面织入了用户登录,那后调用用户登录,只要你织入了用户查询,那就调用用户查询。而且不单单是只掉某一个方法,它可以挨着排的调用。

    这只是其中一个强大的用处,还有像日志记录、性能分析、事务处理等更多都可以利用到AOP的地方。


    Think of AOP as complementing, not competing with, OOP. AOP can supplement OOP where it is weak. (AOP和OOP没有竞争关系,相反,AOP能够很好的补充OOP的不足)

    AOP基本术语

     

    l  Aspect(切面):就是你想给程序织入的代码片段、如权限处理、日志记录等。

    l  Weaving(编织):就是给指定的程序加上额外的业务逻辑的过程,比如将权限验证插入到用户登录的过程。

    l  Advice(通知):表示是在程序的哪里织入切面,比如前面织入,还是后面织入,或者是抛出异常的时候织入。

    l  Joinpoint(连接点):表示给那个程序织入切面,也就是被代理的目标对象的目标方法。

    l  Pointcut(切入点):表示给哪些程序织入切面,是连接点的集合,比如是用户登录和用户查询等都需要被织入。

     

    为了方便用户使用AOP,需要定义几种通知类型。

    l  Before:前置通知,在业务逻辑之前通知

    l  After:后置通知,在业务逻辑正常完结之后通知

    l  End:结束通知,不管业务逻辑是否正常完结,都会在后面执行的通知

    l  Error:错误通知,在业务逻辑抛出异常的时候通知

     

    AOP执行流程

     

    下图展示了AOP核心调用过程,通过调用AOP代理类,开始一个一个调用后面的(前置)通知/拦截器链条,完成之后在调用目标方法,最后回来的时候接着调用(后置、结束)通知/拦截器链条。

     

    如此一来就成功的完成了在AOP中给某个程序(目标方法)之前加上一段业务逻辑,之后加上一段业务逻辑的流程,并且杀伤力极大,可以将目标方法的范围进行任意控制。

     

    动手实现一个AOP

     

    前戏那么长,高潮不会短!这次写的AOP参考了很多Spring的代码,吸收了大师补充的养分。

    利用测试驱动开发的原则,我们先来考虑考虑我们会怎么用(写好测试代码),然后想想API怎么设计(将接口写好),最后考虑实现的问题。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
            @Test
        publicvoid testTranscation() {
            // 创建动态代理工厂,这是调用动态代理实现aop的初始点
            AopProxyFactory proxy = newAopProxyFactory();
     
            // 创建目标对象
            proxy.setTarget(newAopDemo());
     
            // 设置各个advice,以便在调用目标对象的指定方法时可以出现这些advice
            proxy.addAdvice(newTransactionAspect());
     
            // 获取代理对象
            IAopDemo p = (IAopDemo) proxy.getProxy();
     
            // 通过代理对象调用目标对象的指定方法
            p.doSomething();
        }


    参考springAPI设计,我们给出了上面这段测试代码。这样就能给AOP代理工厂配置目标对象,和各种各样的通知,目前只有事务处理的通知。然后通过代理工厂获取目标对象的代理对象,并完成类型转换的过程。最后调用指定方法,完成在这个方法的周围实现事务处理过程。

    设置目标对象的方法无需赘言,就是看中了那个对象,设置进去,这样就能搞个代理帮他做了。。。

    添加通知的实现,我想设计的好用一点。什么叫好用呢?也就是给你一些接口,BeforeAfterErrorEnd等,只要你定义的aspect类(切面)实现了任意一个接口,就能保证按照这个接口名字所显示的那样执行。比如我实现了Before接口和他的抽象方法,并在里面加了个“记录日志”的功能,这样,我以后就能在我的目标方法执行之前完成一次记录日志的过程了。这里我们使用的是事务处理的aspect切面:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Component
    @Match(methodMatch = "org.*.doSomething")
    publicclass TransactionAspect implementsBefore, End, Error {
     
        @Override
        publicvoid error(Method method, Object[] args, Exception e) {
            System.out.println("回滚事务");
        }
     
        @Override
        publicvoid before(Method method, Object[] args) {
            System.out.println("开启事务");
        }
     
        @Override
        publicvoid end(Method method, Object[] args, Object retVal) {
            System.out.println("关闭事务");
        }
     
    }


    在代理工厂中,我们使用Object  target存储这个目标对象,并使用集合来记录所有该类涉及到的通知。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
            privateObject target;
     
        privateList<Advice> adviceList = newArrayList<Advice>();
     
        /**
         *
         * @Title: setTarget
         * @Description: 设置目标
         * @param @param target 设定文件
         * @return void 返回类型
         * @throws
         */
        publicvoid setTarget(Object target) {
            this.target = target;
        }
     
        publicvoid addAdvice(Advice advice) {
            if(advice == null)
                thrownew NullPointerException("添加的通知不能为空");
            adviceList.add(advice);
        }

    而在完成配置代理工厂后,需要通过这个代理工厂来获取代理对象。在获取代理对象之前,我们把之前完成的配置(目标方法、通知集合)都初始化到AdvisedSupport对象中,将这个对象整体传给后面的代理实现(jdkcglib)完成代理类的初始化,以及通知和目标方法的调用。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
            publicObject getProxy() {
            if(target == null)
                thrownew NullPointerException("目标对象不能为空");
             
            AdvisedSupport config=newAdvisedSupport(target, adviceList);
     
            AopProxy proxy = null;
            // 若该目标对象实现了接口,就优先选择jdk动态代理;如果没有实现任何接口,就只能采用cglib动态代理;
            if(config.hasInterfaces()) {
                logger.info("采用jdk动态代理");
                proxy = newJDKAopProxy(config);
            }else{
                logger.info("采用cglib动态代理");
                proxy = newCglibAopProxy(config);
            }
            returnproxy.getProxy();
        }


    这里我们会根据不同的情况来判断他是选择jdk动态代理还是选择cglib动态代理。

    这里以jdk动态代理为例,cglib留给读者自行分析:


    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    publicclass JDKAopProxy implementsAopProxy, InvocationHandler {
     
        privateAdvisedSupport config;
     
        publicJDKAopProxy(AdvisedSupport config) {
            this.config = config;
        }
     
        @Override
        publicObject getProxy() {
            returnProxy.newProxyInstance(config.getClassLoader(),
                    config.getInterfaces(),this);
        }
     
        @Override
        publicObject invoke(Object proxy, Method method, Object[] args)
                throwsThrowable {
            returnnew ReflectiveMethodInvocation(config.getInterceptors(),config.getMatchers(), args,
                    method, config.getTarget()).proceed();
        }
     
    }



    这里我们首先在getProxy初始化了代理类,然后当代理类的方法被调用时,会完成目标方法调用,这个步骤都是ReflectiveMethodInvocation对象完成的。这个类实现了MethodInvocation,目的是为了完成之后的回调过程,这个后面可以看到。在这个ReflectiveMethodInvocation类里面,我们存储了足够多的信息

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
            /**
         * 通知advice
         */
        privateList<MethodInterceptor> chain;
     
        /**
         * 每个advice所配置的匹配信息
         */
        privateList<Matcher> matches;
     
        /**
         * 执行目标方法需要的参数
         */
        privateObject[] arguments;
     
        /**
         * 目标方法
         */
        privateMethod method;
     
        /**
         * 目标对象
         */
        privateObject target;
         
        /**
         * 记录当前advice链条(chain)所需要执行的方法的索引
         */
        privateint index;



    目的很简单:就是将多个通知链条和目标对象的方法本身的调用整合起来,形成逻辑完善的链条——前置通知在目标方法前面排着队完成,如果目标方法抛出了异常就执行错误通知,一旦正常执行完成目标方法就执行后置通知,而结束通知时不管是是正常执行完目标方法还是抛出了异常,最后都会执行的一个通知。

    下面来看看这个类中处理一连串方法调用的核心方法proceed()

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @Override
        publicObject proceed() throwsThrowable {
            //当链条走完的时候调用目标方法
            if(index == chain.size())
                returninvokeJoinpoint();
     
            Matcher matcher = matches.get(index);
     
            // 查看是否匹配,
            if(matcher.matches(this.method,this.target.getClass())) {
                returnchain.get(index++).invoke(this);
            }else{
                index++;
                returnproceed();
            }
     
        }
     
        /**
         *
         * @Title: invokeJoinpoint
         * @Description: 调用连接点方法
         * @param @return
         * @param @throws Throwable 设定文件
         * @return Object 返回类型
         * @throws
         */
        protectedObject invokeJoinpoint() throwsThrowable {
            returnmethod.invoke(target, arguments);
        }



    很简单,就是当链条走完的时候,调用目标方法。否则就继续指向链条上的方法。这里有一个检测是否匹配的过程,也就是我给我的切面类,也就是处理事务的切面TransactionAspect配置了一个注解Match,这个注解表示当目标类是org包下的某个类时,我就会对他的doSomething方法完成拦截,在这个方法周围加上事务处理。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Component
    @Match(methodMatch = "org.*.doSomething")
    publicclass TransactionAspect implementsBefore, End, Error {
     
        @Override
        publicvoid error(Method method, Object[] args, Exception e) {
            System.out.println("回滚事务");
        }
     
        @Override
        publicvoid before(Method method, Object[] args) {
            System.out.println("开启事务");
        }
     
        @Override
        publicvoid end(Method method, Object[] args, Object retVal) {
            System.out.println("关闭事务");
        }
     
    }



    具体判断是否匹配,我采用的是正则表达式,也就是当目标类的某个方法被调用时,一旦检测到他符合methodMatch配置的正则表达式,就给该方法前后加上指定的逻辑。如果发现不匹配,这继续寻找链条上下一个通知,直到走完整个通知链条。

    这里大家肯定会有一个问题:在链条上获取到一个通知,执行该通知的时候,如何确保前置通知是再前面执行,后置通知是再后面执行呢?并且在完成调用之后确保执行后面的通知调用流程?

    其实,spring在这里用到了一个很巧妙的编程技巧——通过多态原理和回调函数来处理。

    ?
    1
    chain.get(index++).invoke(this);



    这里获取到了第index个通知,拿到的是个接口类型,但是实现类出卖了他的本质,表示它到底是前置还是后置或者是其他等。当执行invoke时,将该MethodInvocation的实现类ReflectiveMethodInvocation的对象的引用传递进去。如此调用的方法,其实是这样的:

    可以发现,当实现类是后置通知的时候,我会选择AfterInterceptor来执行,当时前置通知的时候,会选择BeforeInterceptor来执行。也就是,碰到合适的通知,就采用合适的拦截器处理。

    以前置通知的方法拦截器为例:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    publicclass BeforeInterceptor implementsMethodInterceptor {
     
        privateBefore advice;
     
        publicBeforeInterceptor(Before advice) {
            this.advice =advice;
        }
     
        @Override
        publicObject invoke(MethodInvocation mi) throwsThrowable {
            advice.before(mi.getMethod(), mi.getArguments());
            returnmi.proceed();
        }
     
    }



    看到这里是不是有种似曾相识的感觉,这不就是开始那个权限验证的翻版么?对啊,it is。这里实现的很明确,就是在目标方法调用之前执行before中的业务逻辑,接着进行mi.proceed()又回调到了我们MethodInvocation的实现类ReflectiveMethodInvocation中的proceed方法中了。

    这样,也就保证了链条的次序执行。

    来看看我们测试用例的输出结果吧:

    ?
    1
    2
    3
    4
    5
    开启事务
     
    和哈哈哈哈哈...
     
    关闭事务



    目前还没能把ioc和aop整合起来使用,还有像ioc在web mvc框架中如何使用都还没提到,不过这些会在我们以后的博客中不断出现。

    源代码

     最后,放出源代码:https://github.com/mjaow/my_spring

    0 0