• 基于AspectJ注解实现AOP


    AOP前奏:AOP的相关理论介绍

    1、Spring对AOP的支持

    Spring提供了3种类型的AOP支持:

    • 基于AspectJ注解驱动的切面(推荐):使用注解的方式,这是最简洁和最方便的!
    • 基于XML的AOP:使用XML配置,aop命名空间
    • 基于代理的经典SpringAOP:需要实现接口,手动创建代理

    2、AspectJ相关的注解

    AspectJ相关注解:

    • @Aspect:标记这个类是一个切面类。

    AspectJ增强相关注解:

    注解 描述
    @Before 表示将当前方法标记为前置通知
    @AfterReturning 表示将当前方法标记为返回通知
    @AfterThrowing 表示将当前方法标记为异常通知
    @After 表示将当前方法标记为后置通知
    @Around 表示将当前方法标记为环绕通知
    @Pointcut 表示定义重用切入点表达式,一次定义,处处使用,一处修改,处处生效
    @DeclareParents 表示将当前方法标记为引介通知(不要求掌握)

    PointCut Designators 切点指示器),是切点表达式的重要组成部分

    3、注解AOP的简单例子

    ①、编写代理对象接口

    /**
     * 代理对象接口
     */
    public interface IUserService {
    
        void addUser(String userName,Integer age);
    }
    

    ②、编写代理对象接口的实现类

    /**
     * 目标类,代理对象实现类,会被动态代理
     */
    @Service
    public class UserServiceImpl implements IUserService{
    
        @Override
        public void addUser(String userName, Integer age) {
            System.out.println(userName+":"+age);
        }
    }
    

    ③、编写切面类

    注意:AspectJ切入点表达式语法:execution(<访问修饰符>? <返回类型> <全限定名>? <方法名称>(<参数类型>) <异常>?),通过execution函数,可以定义切入的方法。

    代码块中带?符号的匹配式都是可选的,对于execution必不可少的只有三个:

    • 返回类型
    • 方法名
    • 参数
    /**
     * 创建日志切面类
     */
    @Aspect // @Aspect注解标记这个类是一个切面类
    @Component // @Component注解标记这个被扫描包扫描到时需要加入IOC容器
    public class LogAspect {  //定义一个日志切面类
    
        // @Before注解将当前方法标记为前置通知
        // value属性:配置当前通知的切入点表达式,通俗来说就是这个通知往谁身上套
        @Before(value = "execution(public void com.thr.aop.target.UserServiceImpl.addUser(String,Integer))")
        public void doBefore(JoinPoint joinPoint) { // 在通知方法中,声明JoinPoint类型的形参,就可以在Spring调用当前方法时把这个类型的对象传入
    
            // 1.通过JoinPoint对象获取目标方法的签名
            // 所谓方法的签名就是指方法声明时指定的相关信息,包括方法名、方法所在类等等
            Signature signature = joinPoint.getSignature();
    
            // 2.通过方法签名对象可以获取方法名
            String methodName = signature.getName();
    
            // 3.通过JoinPoint对象获取目标方法被调用时传入的参数
            Object[] args = joinPoint.getArgs();
    
            // 4.为了方便展示参数数据,把参数从数组类型转换为List集合
            List<Object> argList = Arrays.asList(args);
    
            System.out.println("[前置通知]"+ methodName +"方法开始执行,参数列表是:" + argList);
        }
    }
    

    ④、编写配置文件

    <?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:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!-- 配置自动扫描的包 -->
        <context:component-scan base-package="com.thr.aop"/>
        <!-- 开启基于AspectJ注解的AOP功能 -->
        <aop:aspectj-autoproxy/>
    
    </beans>
    

    ⑤、编写测试类

    public class AOPTest {
    
        //创建ApplicationContext对象
        private ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    
        @Test
        public void testAOP(){
            // 1.从IOC容器中获取接口类型的对象
            IUserService userService = ac.getBean(IUserService.class);
    
            // 2.调用方法查看是否应用了切面中的通知
            userService.addUser("张三",20);
        }
    }
    

    ⑥、运行结果

    image

    4、切入点表达式语法(重要)

    在上面的例子中,切入点表达式是写死的,如果有很多地方要切入的话,就要在切面类中编写大量重复性的代码,扩展性和实用性不高,所以下面来学习一下更加强大的切入点表达式。

    注意:AspectJ切入点表达式语法:execution(<访问修饰符>? <返回类型> <全限定名>? <方法名称>(<参数类型>) <异常>?),通过execution函数,可以定义切入的方法。代码块中带?符号的匹配式都是可选的,对于execution必不可少的只有三个:

    • 返回类型
    • 方法名
    • 参数

    完整的传统切入点表达式:execution(public void com.thr.aop.target.UserServiceImpl.addUser(String,Integer))

    上面最大可以简写为:execution(* *..*.*(..)) 表示匹配任意修饰符,返回值,包,类,方法,参数。

    • *号代替“权限修饰符”和“返回值”部分,表示“权限修饰符”和“返回值”不限,即任意类型,注意:这里一个*代表两部分,下面有介绍
    • 在包名的部分,使用*表示包名任意
    • 在包名的部分,使用*..表示包名任意、包的层次深度任意
    • 在类名的部分,使用*号表示类名任意,也可以可以使用*号代替类名的一部分,例如:
    *Service
    

    上面例子*Service表示匹配所有类名、接口名以Service结尾的类或接口(*号位置不限)

    • 在方法名部分,使用*号表示方法名任意,也可以使用*号代替方法名的一部分,例如:
    *Operation
    

    上面例子*Operation表示匹配所有方法名以Operation结尾的方法(*号位置不限)

    • 在方法参数列表部分,使用(..)表示参数列表任意
    • 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头,后面的任意
    • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
    execution(public int *..*Service.*(.., int))
    

    上面例子是对的,而下面例子是错的:

    execution(* int *..*Service.*(.., int))
    
    • 对于execution()表达式整体可以使用三个逻辑运算符号(了解,几乎不用)
      • execution() || execution()表示满足两个execution()中的任何一个即可
      • execution() && execution()表示两个execution()表达式必须都满足
      • !execution()表示不满足表达式的其他方法

    AOP切入点表达式补充:

    image

    上面相关函数的详细使用可以参考:spring aop中pointcut表达式完整版

    4、重用切入点表达式

    这里需要用到@Pointcut注解。在一处声明切入点表达式之后,在其它有需要的地方引用这个切入点表达式就好。易于维护,一处修改,处处生效。声明方式如下:

    // 切入点表达式重用
    @Pointcut("execution(* *..*.add*(..))")
    public void doPointCut() {}
    

    在同一个类内部引用时:

    @Before(value = "doPointCut()")
    public void doBefore(JoinPoint joinPoint) {
    

    在不同类中引用:

    @Before(value = "com.thr.aop.aspect.LogAspect.doPointCut")
    public void doBefore(JoinPoint joinPoint) {
    

    5、注解AOP的完整例子

    基于前面简单的例子,除了切面类LogAspect代码需要改变之外,其它的类中代码都不变。

    /**
     * 创建日志切面类
     */
    
    @Aspect // @Aspect注解标记这个类是一个切面类
    @Component // @Component注解标记这个被扫描包扫描到时需要加入IOC容器
    public class LogAspect {  //定义一个日志切面类
    
        // 使用@Pointcut注解重用切入点表达式
        // 当前类引用时:doPointCut()
        // 其他类引用时:com.thr.aop.aspect.LogAspect.doPointCut()
        @Pointcut(value = "execution(* *..*.add*(..))")
        public void doPointCut() {
        }
    
        // @Before注解将当前方法标记为前置通知
        // value属性:配置当前通知的切入点表达式,通俗来说就是这个通知往谁身上套
        @Before(value = "doPointCut()")
        public void doBefore(JoinPoint joinPoint) { // 在通知方法中,声明JoinPoint类型的形参,就可以在Spring调用当前方法时把这个类型的对象传入
    
            // 1.通过JoinPoint对象获取目标方法的签名
            // 所谓方法的签名就是指方法声明时指定的相关信息,包括方法名、方法所在类等等
            Signature signature = joinPoint.getSignature();
    
            // 2.通过方法签名对象可以获取方法名
            String methodName = signature.getName();
    
            // 3.通过JoinPoint对象获取目标方法被调用时传入的参数
            Object[] args = joinPoint.getArgs();
    
            // 4.为了方便展示参数数据,把参数从数组类型转换为List集合
            List<Object> argList = Arrays.asList(args);
    
            System.out.println("[前置通知]" + methodName + "方法开始执行,参数列表是:" + argList);
        }
    
        // @AfterReturning注解将当前方法标记为返回通知
        // 使用returning指定一个形参名,Spring会在调用当前方法时,把目标方法的返回值从这个位置传入
        @AfterReturning(value = "doPointCut()", returning = "returnValue")
        public void doAfterReturning(JoinPoint joinPoint, Object returnValue) {
    
            String methodName = joinPoint.getSignature().getName();
    
            System.out.println("[返回通知]" + methodName + "方法成功结束,返回值是:" + returnValue);
        }
    
        // @AfterThrowing注解将当前方法标记为异常通知
        // 使用throwing属性指定一个形参名称,Spring调用当前方法时,会把目标方法抛出的异常对象从这里传入
        @AfterThrowing(value = "doPointCut()", throwing = "throwable")
        public void doAfterThrowing(JoinPoint joinPoint, Throwable throwable) {
    
            String methodName = joinPoint.getSignature().getName();
    
            System.out.println("[异常通知]" + methodName + "方法异常结束,异常信息是:" + throwable.getMessage());
        }
    
        // @After注解将当前方法标记为后置通知
        @After(value = "doPointCut()")
        public void doAfter(JoinPoint joinPoint) {
    
            String methodName = joinPoint.getSignature().getName();
    
            System.out.println("[后置通知]" + methodName + "方法最终结束");
        }
    }
    

    运行结果:

    image

    小细节,通知执行的顺序

    • Spring版本5.3.x以前:
      • 前置通知
      • 目标操作
      • 后置通知
      • 返回通知或异常通知
    • Spring版本5.3.x以后:
      • 前置通知
      • 目标操作
      • 返回通知或异常通知
      • 后置通知

    6、环绕通知的举例

    环绕通知就是前面四个通知的结合,但Spring官方建议选用“能实现所需行为的功能最小的通知类型”: 提供最简单的编程模式,减少了出错的可能性。,本例在环绕通知中触发异常通知。

    ①、修改代理对象接口的实现类

    /**
     * 目标类,会被动态代理
     */
    @Service
    public class UserServiceImpl implements IUserService {
    
        @Override
        public void addUser(String userName, Integer age) {
            //出现异常
            int i = 1;
            int j = 0;
            int x = i / j;
            System.out.println(userName + ":" + age);
        }
    }
    

    ②、编写环绕通知切面类

    /**
     * 创建日志环绕通知切面类
     */
    @Aspect // @Aspect注解标记这个类是一个切面类
    @Component // @Component注解标记这个被扫描包扫描到时需要加入IOC容器
    public class Log1Aspect {  //定义一个日志切面类
    
        // 使用@Pointcut注解重用切入点表达式
        // 当前类引用时:doPointCut()
        // 其他类引用时:com.thr.aop.aspect.LogAspect.doPointCut()
        @Pointcut(value = "execution(* *..*.add*(..))")
        public void doPointCut() {
        }
    
        // 使用表示当前方法是环绕通知
        @Around(value = "doPointCut()")
        public Object doAround(ProceedingJoinPoint joinPoint) {
    
            // 获取目标方法名
            String methodName = joinPoint.getSignature().getName();
    
            // 声明一个变量,用来接收目标方法的返回值
            Object targetMethodReturnValue = null;
    
            // 获取外界调用目标方法时传入的实参
            Object[] args = joinPoint.getArgs();
    
            try {
                // 调用目标方法之前的位置相当于前置通知
                System.out.println("[环绕通知]" + methodName + "方法开始执行,参数列表:" + Arrays.asList(args));
    
                // 通过ProceedingJoinPoint对象的proceed(Object[] var1)调用目标方法
                targetMethodReturnValue = joinPoint.proceed();
    
                // 调用目标方法成功返回之后的位置相当于返回通知
                System.out.println("[环绕通知]" + methodName + "方法成功返回,返回值是:" + targetMethodReturnValue);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                // 调用目标方法抛出异常之后的位置相当于异常通知
                System.out.println("[环绕通知]" + methodName + "方法抛出异常,异常信息:" + throwable.getMessage());
            } finally {
                // 调用目标方法最终结束之后的位置相当于后置通知
                System.out.println("[环绕通知]" + methodName + "方法最终结束");
            }
    
            // 将目标方法的返回值返回
            // 这里如果环绕通知没有把目标方法的返回值返回,外界将无法获取这个返回值数据
            return targetMethodReturnValue;
        }
    }
    

    ③、运行结果

    image

    7、切面的优先级

    [1]概念:相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

    • 优先级高的切面:外面
    • 优先级低的切面:里面

    使用@Order注解可以控制切面的优先级:

    • @Order(较小的数):优先级高
    • @Order(较大的数):优先级低

    image

    [2]实际意义:实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。

    image

    此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。

    image


    参考资料:

    作者: 唐浩荣
    本文版权归作者和博客园共有,欢迎转载,但是转载需在博客的合适位置给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    开源高性能网络库Libevent的简介
    网络IO之阻塞、非阻塞、同步、异步总结【转】
    C语言20150620
    EF那点事
    SSO单点登录的实现原理是怎样的
    SQL索引器
    基础数学知识
    hibernate优化笔记(随时更新)
    JAVA中保留小数的多种方法
    Hibernate的session缓存和对象的四种状态
  • 原文地址:https://www.cnblogs.com/tanghaorong/p/14742436.html
Copyright © 2020-2023  润新知