zoukankan      html  css  js  c++  java
  • 如何通过自定义注解实现AOP切点定义

    面向切面编程(Aspect Oriented Programming, AOP)是面向对象编程(Object Oriented Programming,OOP)的强大补充,通过横切面注入的方式引入其他额外功能,比如日志记录,事务处理等,用户无需修改源代码就可以"优雅"的实现额外功能的补充,对于Programmer来说,AOP是个非常强大的工具。

    AOP中的切面处理逻辑会被应用到我们所定义的切点(Point Cut)上,切面逻辑定义可以使用 around, before,after等Aspect注解实现,切点可以使用Aspect注解中的参数指定或者通过xml配置文件声明。在编写代码的过程中,切点控制往往不够灵活,需要我们在xml或者Aspect注解参数中指定方法的path,当切点较多,需要颗粒度更加细致的切点控制时,通常我们需要添加大量的切点定义代码,这样比较麻烦。通常呢我们我们可以通过结合自定义注解来解决这个问题,实现更加灵活的切点控制。

    自定义注解实现AOP切点定义的背后原理说起来其实很简单,通过扫描项目所有的类,然后过滤出标注点的位置,将切面自定义逻辑应用到标注点上,就实现了我们的业务需求。但是,技术上如何去实现呢?本文就这一问题,结合Java Spring AOP框架给出解答。

    目标

    我们的自定义注解需要具备以下功能:

    1. 类中方法添加注解,则这个方法成为切点
    2. 类添加注解,则这个类中所有的方法成为切点
    3. 抽象类方法添加注解,则抽闲类中的这个方法成为切点
    4. 抽象类添加注解,则抽象类中的所有方法成为切点
    5. 接口添加注解,则接口中定义的所有方法成为切点
    6. 接口中方法添加注解,则接口中的这个方法成为切点

    JDK提供的关键类和方法

    Class

    Java最基本的元素称为"类",类中可以包涵方法和属性的定义。Class对象提供了很多有用的方法,可以帮助我们切点位置定位,比如:

    1. public native Class<? super T> getSuperclass();获取当前类的父类
    2. public Class<?>[] getInterfaces();获取当前类实现的接口
    3. public Method[] getDeclaredMethods() throws SecurityException 获取当前类中声明的方法
    4. public < A extends Annotation > A getAnnotation(Class < A > annotationClass)获取当前类指定标签的对象,若为空,表明当前类没有标签annotationClass。

    Method

    我们还使用到Method类的一些方法:

    1. public String getName(); 获取方法的名字
    2. public < T extends Annotation > T getAnnotation(Class annotationClass);取当前类指定标签的对象,若为空,表明当前类没有标签annotationClass

    ProceedingJoinPoint

    ProceedingJoinPoint接口提供了很多实用的函数,便于用户获取应用切面点函数具体的信息。下面四个接口是我们用的比较多的:

    1. Object proceed() throws Throwable; 调用要拦截的方法
    2. Object proceed(Object[] var1) throws Throwable;调用要拦截的方法,可以自定义传入参数
    3. Object[] getArgs();获取拦截方法的传入参数
    4. Signature getSignature();获取拦截方法的方法名

    实现

    XML配置

    配置xml文件,使能AOP和Spring Bean自动装配

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
    			http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
    			http://www.springframework.org/schema/aop
    			http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
        <!-- 使能AOP-->
        <aop:aspectj-autoproxy/>
        <!-- 自动装载bean使能-->
        <context:component-scan base-package="com.mj.spring.aop"/>
        <context:annotation-config/>
    
    </beans>
    

    定义自定义标签

    我们的自定义标签可以作用于类,方法上,运行时工作。这儿需要说一下两个标签@Target和Retention,@Target用来设置标签的作用范围:

    1. @Target(ElementType. FIELD)表示标签只能用来修饰字段、枚举的常量
    2. @Target(ElementType.METHOD)表示标签只能用来修饰方法
    3. @Target(ElementType.TYPE) 标签可用来修饰接口、类、枚举、注解
    4. ...

    @Retention用来修饰注解的生存范围

    1. @Retention(RetentionPolicy.SOURCE) 注解仅存在于源码中,在class字节码文件中不包含

    2. @Retention(RetentionPolicy.CLASS) 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得,

    3. @Retention(RetentionPolicy.RUNTIME) 注解会在class字节码文件中存在,在运行时可以通过反射获取到

       @Retention(RetentionPolicy.RUNTIME)
       public @interface AOPLog4jAnnotation {
       
       }
      

    注意到我们没有添加Target标签,不指定标签的作用范围,那么标签适用于所有范围。

    定义切面类

    完整的切面类代码如下所示,类中实现了切面逻辑的定义和切点判断的逻辑代码。

    @Component
    @Aspect
    public class APIProxy{
    
        private final static Log LOGGER = LogFactory.getLog(APIProxy.class);
    
        //切面应用范围是在com.mj.spring.aop包下面所有函数
        @Around("execution(* com.mj.spring.aop..*.*(..))")
        public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
            String signatureName = joinPoint.getSignature().getName();
            Class<? extends Object> invokeClass = joinPoint.getTarget().getClass();
            if (isTagged(invokeClass, signatureName)) {
                LOGGER.info(signatureName + " is tagged");
                joinPoint.proceed();
                return;
            }
            joinPoint.proceed();
    
        }
    
        //扫描父类是否被打上标签,或者父类中的这个方法是否被打伤标签
        private boolean isTagged(Class invokeClass, String signatureName) {
            if (isTaggedInInterfaceOf(invokeClass, signatureName)) {
                return true;
            }
            if (!invokeClass.equals(Object.class)) {
                return isTaggedInClassOf(invokeClass, signatureName) ? true :
                        isTagged(invokeClass.getSuperclass(), signatureName);
            }
            return false;
        }
    
        //扫描当前类的接口
        private boolean isTaggedInInterfaceOf(Class invokeClass, String signatureName) {
            Class[] interfaces = invokeClass.getInterfaces();
            for (Class cas : interfaces) {
                return isTaggedInClassOf(cas, signatureName) ? true :
                        isTaggedInInterfaceOf(cas, signatureName);
            }
            return false;
        }
    
        //方法名为signatureName的方法tagged有两种情况:方法本身被taged或者方法所在的类被taged
        private boolean isTaggedInClassOf(Class cas, String signatureName) {
            return Lists.newArrayList(cas.getDeclaredMethods())
                    .stream().anyMatch(method ->
                            isMethodWithName(method, signatureName) && isMethodTagged(method)
                                    || isMethodWithName(method, signatureName) && isClassTagged(cas));
        }
    
        private boolean isClassTagged(Class invokeClass) {
            return invokeClass.getAnnotation(AOPLog4jAnnotation.class) != null;
        }
    
        private boolean isMethodTagged(Method method) {
            return method.getAnnotation(AOPLog4jAnnotation.class) != null;
        }
    
        private boolean isMethodWithName(Method method, String name) {
            return method.getName().equals(name);
        }
    }
    

    下面代码实现了一个around切面advice定义,切面逻辑的应用范围是com.mj.spring.aop包下的所有的方法,判断当前执行方法是否被打上标签,如果打上标签,那么执行我们额外添加的业务逻辑代码,这里为简单起见在方法运行前打了一个log,然后执行方法,返回。否则直接调用方法,不做任何额外处理。

    //切面应用范围是在com.mj.spring.aop包下面所有函数
        @Around("execution(* com.mj.spring.aop..*.*(..))")
        public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
            String signatureName = joinPoint.getSignature().getName();
            Class<? extends Object> invokeClass = joinPoint.getTarget().getClass();
            if (isTagged(invokeClass, signatureName)) {
                LOGGER.info(signatureName + " is tagged");
                joinPoint.proceed();
                return;
            }
            joinPoint.proceed();
    
        }
    

    方法被打上自定义标签有以下几种可能:

    1. 该方法方法体被打上标签
    2. 该方法所在类被打上标签
    3. 该方法所在的API接口函数对应被打上标签
    4. 该方法所在的API接口被打上标签
    5. 该方法所在的抽象类被打上标签
    6. 该方法所在的抽象类函数定义被打上标签

    对于接口函数来说,接口之间可以多重嵌套,搜寻接口中指定函数的标签,需要采用递归的方式向上寻找,对于父类继承也同样如此。下面的代码实现涵盖了上面6种能性的所有判读。

        //扫描父类是否被打上标签,或者父类中的这个方法是否被打伤标签
        private boolean isTagged(Class invokeClass, String signatureName)    
        {
            if (isTaggedInInterfaceOf(invokeClass, signatureName)) {
                return true;
            }
            if (!invokeClass.equals(Object.class)) {
                return isTaggedInClassOf(invokeClass, signatureName) ? true :
                        isTagged(invokeClass.getSuperclass(), signatureName);
            }
            return false;
        }
    

    函数开始:

    1. 判断当前名为signatureName的方法是否在invokeClass类所实现的API接口中被Tag。(实现3和4的判断)
    2. 判断当前类是否为Object.class,若不是则执行第三步,否则执行第四步
    3. 判断当前名为signatureName的方法是否在类invokeClass中被tag(实现1和2的判断)
    4. 上面三项没有为真,则调用当前类的父类继续递归(实现5和6的判断)

    判断当前名为signatureName的方法是否在invokeClass类所实现的API接口中被Tag的代码如下所示,首先获取当前类所有接口,分别对每个接口类进行方法检查,若检查成功,则返回true,否则继续向上递归。

        //扫描当前类的接口
        private boolean isTaggedInInterfaceOf(Class invokeClass, String signatureName) {
            Class[] interfaces = invokeClass.getInterfaces();
            for (Class cas : interfaces) {
                return isTaggedInClassOf(cas, signatureName) ? true :
                        isTaggedInInterfaceOf(cas, signatureName);
            }
            return false;
        }
    

    判断一个名为signatureName的方法在类cas中是否被tag的代码如下所示:

        private boolean isTaggedInClassOf(Class cas, String signatureName) {
            return Lists.newArrayList(cas.getDeclaredMethods())
                    .stream().anyMatch(method ->
                            isMethodWithName(method, signatureName) && isMethodTagged(method)
                                    || isMethodWithName(method, signatureName)&& isClassTagged(cas));
    

    代码逻辑实现很简单,判断方法被tag条件为:该方法是在该类中同时(该方法体是被打上标签或者类被打上标签)

    Conclusion

    本文和大家分享了如何通过自定义注解实现AOP切点定义,希望能够对大家有所帮助。本文完整的源码,单元测试位于:< https://github.com/jma19/spring/tree/master/spring-aop >, 欢迎大家下载,批评指正。

  • 相关阅读:
    【BZOJ 4151 The Cave】
    【POJ 3080 Blue Jeans】
    【ZBH选讲·树变环】
    【ZBH选讲·拍照】
    【ZBH选讲·模数和】
    【CF Edu 28 C. Four Segments】
    【CF Edu 28 A. Curriculum Vitae】
    【CF Edu 28 B. Math Show】
    【CF Round 439 E. The Untended Antiquity】
    【CF Round 439 C. The Intriguing Obsession】
  • 原文地址:https://www.cnblogs.com/jun-ma/p/4844978.html
Copyright © 2011-2022 走看看