zoukankan      html  css  js  c++  java
  • 基于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


    参考资料:

    作者: 唐浩荣
    本文版权归作者和博客园共有,欢迎转载,但是转载需在博客的合适位置给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    QFramework 使用指南 2020(二):下载与版本介绍
    QFramework 使用指南 2020 (一): 概述
    Unity 游戏框架搭建 2018 (二) 单例的模板与最佳实践
    Unity 游戏框架搭建 2018 (一) 架构、框架与 QFramework 简介
    Unity 游戏框架搭建 2017 (二十三) 重构小工具 Platform
    Unity 游戏框架搭建 2017 (二十二) 简易引用计数器
    Unity 游戏框架搭建 2017 (二十一) 使用对象池时的一些细节
    你确定你会写 Dockerfile 吗?
    小白学 Python 爬虫(8):网页基础
    老司机大型车祸现场
  • 原文地址:https://www.cnblogs.com/tanghaorong/p/14742436.html
Copyright © 2011-2022 走看看