• 如何通过自定义注解实现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 >, 欢迎大家下载,批评指正。

  • 相关阅读:
    基本数据类型转换
    java8新增的日期时间包
    算法之冒泡排序
    基本数据类型
    spring入门
    JiuDuOj——1020
    JiuDuOj——1051
    2015.11.26——Accelerated C++
    POJ2681——求字符串长度
    POJ1017——装箱问题
  • 原文地址:https://www.cnblogs.com/jun-ma/p/4844978.html
Copyright © 2020-2023  润新知