• Spring-AOP


    AOP面向切面编程

    AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性

    Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:

    当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

    使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。

    Spring AOP 和 AspectJ AOP 有什么区别?

    Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

    Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

    如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。

    第一节 情景设定

    1、声明接口

    public interface Calculator {
        
        int add(int i, int j);
        
        int sub(int i, int j);
        
        int mul(int i, int j);
        
        int div(int i, int j);
        
    }
    

    2、给接口声明一个纯净版实现

    没有额外功能

    package com.atguigu.proxy.imp;
        
    import com.atguigu.proxy.api.Calculator;
        
    public class CalculatorPureImpl implements Calculator {
        
        @Override
        public int add(int i, int j) {
        
            int result = i + j;
        
            System.out.println("方法内部 result = " + result);
        
            return result;
        }
        
        @Override
        public int sub(int i, int j) {
        
            int result = i - j;
        
            System.out.println("方法内部 result = " + result);
        
            return result;
        }
        
        @Override
        public int mul(int i, int j) {
        
            int result = i * j;
        
            System.out.println("方法内部 result = " + result);
        
            return result;
        }
        
        @Override
        public int div(int i, int j) {
        
            int result = i / j;
        
            System.out.println("方法内部 result = " + result);
        
            return result;
        }
    }
    

    3、再声明一个带日志功能的实现

    package com.atguigu.proxy.imp;
    
    import com.atguigu.proxy.api.Calculator;
    
    public class CalculatorLogImpl implements Calculator {
        
        @Override
        public int add(int i, int j) {
        
            System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
        
            int result = i + j;
        
            System.out.println("方法内部 result = " + result);
        
            System.out.println("[日志] add 方法结束了,结果是:" + result);
        
            return result;
        }
        
        @Override
        public int sub(int i, int j) {
        
            System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
        
            int result = i - j;
        
            System.out.println("方法内部 result = " + result);
        
            System.out.println("[日志] sub 方法结束了,结果是:" + result);
        
            return result;
        }
        
        @Override
        public int mul(int i, int j) {
        
            System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
        
            int result = i * j;
        
            System.out.println("方法内部 result = " + result);
        
            System.out.println("[日志] mul 方法结束了,结果是:" + result);
        
            return result;
        }
        
        @Override
        public int div(int i, int j) {
        
            System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
        
            int result = i / j;
        
            System.out.println("方法内部 result = " + result);
        
            System.out.println("[日志] div 方法结束了,结果是:" + result);
        
            return result;
        }
    }
    

    4、提出问题

    ①现有代码缺陷

    针对带日志功能的实现类,我们发现有如下缺陷:

    • 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
    • 附加功能分散在各个业务功能方法中,不利于统一维护

    ②解决思路

    解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

    ③困难

    解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。

    第二节 代理模式

    1、概念

    ①介绍

    二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

    使用代理前:

    使用代理后:

    ②生活中的代理

    • 广告商找大明星拍广告需要经过经纪人
    • 合作伙伴找大老板谈合作要约见面时间需要经过秘书
    • 房产中介是买卖双方的代理

    ③相关术语

    • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
    • 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。

    理解代理模式、AOP的核心关键词就一个字:

    2、静态代理

    创建静态代理类:

    
    public class CalculatorStaticProxy implements Calculator {
        
        // 将被代理的目标对象声明为成员变量
        private Calculator target;
        
        public CalculatorStaticProxy(Calculator target) {
            this.target = target;
        }
        
        @Override
        public int add(int i, int j) {
        
            // 附加功能由代理类中的代理方法来实现
            System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
        
            // 通过目标对象来实现核心业务逻辑
            int addResult = target.add(i, j);
        
            System.out.println("[日志] add 方法结束了,结果是:" + addResult);
        
            return addResult;
        }
        ……
    

    静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。

    提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

    3、动态代理

    ①生产代理对象的工厂类

    JDK本身就支持动态代理,这是反射技术的一部分。下面我们还是创建一个代理类(生产代理对象的工厂类):

    // 泛型T要求是目标对象实现的接口类型,本代理类根据这个接口来进行代理
    public class LogDynamicProxyFactory<T> {
        
        // 将被代理的目标对象声明为成员变量
        private T target;
        
        public LogDynamicProxyFactory(T target) {
            this.target = target;
        }
        
        public T getProxy() {
        
            // 创建代理对象所需参数一:加载目标对象的类的类加载器
            ClassLoader classLoader = target.getClass().getClassLoader();
        
            // 创建代理对象所需参数二:目标对象的类所实现的所有接口组成的数组
            Class<?>[] interfaces = target.getClass().getInterfaces();
        
            // 创建代理对象所需参数三:InvocationHandler对象
            // Lambda表达式口诀:
            // 1、复制小括号
            // 2、写死右箭头
            // 3、落地大括号
            InvocationHandler handler = (
                                        // 代理对象,当前方法用不上这个对象
                                        Object proxy,
        
                                         // method就是代表目标方法的Method对象
                                         Method method,
        
                                         // 外部调用目标方法时传入的实际参数
                                         Object[] args)->{
        
                // 我们对InvocationHandler接口中invoke()方法的实现就是在调用目标方法
                // 围绕目标方法的调用,就可以添加我们的附加功能
        
                // 声明一个局部变量,用来存储目标方法的返回值
                Object targetMethodReturnValue = null;
        
                // 通过method对象获取方法名
                String methodName = method.getName();
        
                // 为了便于在打印时看到数组中的数据,把参数数组转换为List
                List<Object> argumentList = Arrays.asList(args);
        
                try {
        
                    // 在目标方法执行前:打印方法开始的日志
                    System.out.println("[动态代理][日志] " + methodName + " 方法开始了,参数是:" + argumentList);
        
                    // 调用目标方法:需要传入两个参数
                    // 参数1:调用目标方法的目标对象
                    // 参数2:外部调用目标方法时传入的实际参数
                    // 调用后会返回目标方法的返回值
                    targetMethodReturnValue = method.invoke(target, args);
        
                    // 在目标方法成功后:打印方法成功结束的日志【寿终正寝】
                    System.out.println("[动态代理][日志] " + methodName + " 方法成功结束了,返回值是:" + targetMethodReturnValue);
        
                }catch (Exception e){
        
                    // 通过e对象获取异常类型的全类名
                    String exceptionName = e.getClass().getName();
        
                    // 通过e对象获取异常消息
                    String message = e.getMessage();
        
                    // 在目标方法失败后:打印方法抛出异常的日志【死于非命】
                    System.out.println("[动态代理][日志] " + methodName + " 方法抛异常了,异常信息是:" + exceptionName + "," + message);
        
                }finally {
        
                    // 在目标方法最终结束后:打印方法最终结束的日志【盖棺定论】
                    System.out.println("[动态代理][日志] " + methodName + " 方法最终结束了");
        
                }
        
                // 这里必须将目标方法的返回值返回给外界,如果没有返回,外界将无法拿到目标方法的返回值
                return targetMethodReturnValue;
            };
        
            // 创建代理对象
            T proxy = (T) Proxy.newProxyInstance(classLoader, interfaces, handler);
        
            // 返回代理对象
            return proxy;
        }
    }
    

    ②测试

    @Test
    public void testDynamicProxy() {
        
        // 1.创建被代理的目标对象
        Calculator target = new CalculatorPureImpl();
        
        // 2.创建能够生产代理对象的工厂对象
        LogDynamicProxyFactory<Calculator> factory = new LogDynamicProxyFactory<>(target);
        
        // 3.通过工厂对象生产目标对象的代理对象
        Calculator proxy = factory.getProxy();
        
        // 4.通过代理对象间接调用目标对象
        int addResult = proxy.add(10, 2);
        System.out.println("方法外部 addResult = " + addResult + "
    ");
        
        int subResult = proxy.sub(10, 2);
        System.out.println("方法外部 subResult = " + subResult + "
    ");
        
        int mulResult = proxy.mul(10, 2);
        System.out.println("方法外部 mulResult = " + mulResult + "
    ");
        
        int divResult = proxy.div(10, 2);
        System.out.println("方法外部 divResult = " + divResult + "
    ");
    }
    

    ③练习

    动态代理的实现过程不重要,重要的是使用现成的动态代理类去用到其他目标对象上。

    声明另外一个接口:

    public interface SoldierService {
        
        int saveSoldier(String soldierName);
        
        int removeSoldier(Integer soldierId);
        
        int updateSoldier(Integer soldierId, String soldierName);
        
        String getSoldierNameById(Integer soldierId);
        
    }
    

    给接口一个实现类:

    public class SoldierServiceImpl implements SoldierService {
        
        @Override
        public int saveSoldier(String soldierName) {
        
            System.out.println("核心业务逻辑:保存到数据库……");
        
            return 1;
        }
        
        @Override
        public int removeSoldier(Integer soldierId) {
        
            System.out.println("核心业务逻辑:从数据库删除……");
        
            return 1;
        }
        
        @Override
        public int updateSoldier(Integer soldierId, String soldierName) {
        
            System.out.println("核心业务逻辑:更新……");
        
            return 1;
        }
    
        @Override
        public String getSoldierNameById(Integer soldierId) {
        
            System.out.println("核心业务逻辑:查询数据库……");
        
            return "good";
        }
    }
    

    测试:

    @Test
    public void testSoldierServiceDynamicProxy() {
        
        // 1.创建被代理的目标对象
        SoldierService soldierService = new SoldierServiceImpl();
        
        // 2.创建生产代理对象的工厂对象
        LogDynamicProxyFactory<SoldierService> factory = new LogDynamicProxyFactory<>(soldierService);
        
        // 3.生产代理对象
        SoldierService proxy = factory.getProxy();
        
        // 4.通过代理对象调用目标方法
        String soldierName = proxy.getSoldierNameById(1);
        System.out.println("soldierName = " + soldierName);
        
    }
    

    第三节 AOP的核心套路

    第四节 AOP术语

    1、横切关注点

    从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

    这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。

    2、通知[记住]

    每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

    • 前置通知:在被代理的目标方法执行
    • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝
    • 异常通知:在被代理的目标方法异常结束后执行(死于非命
    • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论
    • 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

    3、切面[记住]

    封装通知方法的类。

    4、目标

    被代理的目标对象。

    5、代理

    向目标对象应用通知之后创建的代理对象。

    6、连接点

    这也是一个纯逻辑概念,不是语法定义的。

    把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。

    7、切入点[记住]

    定位连接点的方式。

    每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。

    如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

    Spring 的 AOP 技术可以通过切入点定位到特定的连接点。

    切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

    第五节 基于注解的AOP

    1、AOP概念介绍

    ①名词解释

    AOP:Aspect Oriented Programming面向切面编程

    ②AOP的作用

    下面两点是同一件事的两面,一枚硬币的两面:

    • 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
    • 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。

    2、基于注解的AOP用到的技术

    • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
    • cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
    • AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。

    3、初步实现

    ①加入依赖

    在IOC所需依赖基础上再加入下面依赖即可:

      <!-- spring-aspects会帮我们传递过来aspectjweaver -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
                <version>5.3.1</version>
            </dependency>
    

    ②准备被代理的目标资源

    [1]接口

    public interface Calculator {
        
        int add(int i, int j);
        
        int sub(int i, int j);
        
        int mul(int i, int j);
        
        int div(int i, int j);
        
    }
    

    [2]纯净的实现类

    在Spring环境下工作,所有的一切都必须放在IOC容器中。现在接口的实现类是AOP要代理的目标类,所以它也必须放入IOC容器。

    
    package com.atguigu.aop.imp;
        
    import com.atguigu.aop.api.Calculator;
    import org.springframework.stereotype.Component;
        
    @Component
    public class CalculatorPureImpl implements Calculator {
        
        @Override
        public int add(int i, int j) {
        
            int result = i + j;
        
            System.out.println("方法内部 result = " + result);
        
            return result;
        }
        
        @Override
        public int sub(int i, int j) {
        
            int result = i - j;
        
            System.out.println("方法内部 result = " + result);
        
            return result;
        }
        
        @Override
        public int mul(int i, int j) {
        
            int result = i * j;
        
            System.out.println("方法内部 result = " + result);
        
            return result;
        }
        
        @Override
        public int div(int i, int j) {
        
            int result = i / j;
        
            System.out.println("方法内部 result = " + result);
        
            return result;
        }
    }
    

    ③创建切面类

    // @Aspect表示这个类是一个切面类
    @Aspect
    // @Component注解保证这个切面类能够放入IOC容器
    @Component
    public class LogAspect {
            
        // @Before注解:声明当前方法是前置通知方法
        // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
        @Before(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
        public void printLogBeforeCore() {
            System.out.println("[AOP前置通知] 方法开始了");
        }
        
        @AfterReturning(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
        public void printLogAfterSuccess() {
            System.out.println("[AOP返回通知] 方法成功返回了");
        }
        
        @AfterThrowing(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
        public void printLogAfterException() {
            System.out.println("[AOP异常通知] 方法抛异常了");
        }
        
        @After(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
        public void printLogFinallyEnd() {
            System.out.println("[AOP后置通知] 方法最终结束了");
        }
        
    }
    

    ④创建Spring的配置文件

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

    ⑤测试

    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(value = {"classpath:applicationContext.xml"})
    public class AOPTest {
        
        @Autowired
        private Calculator calculator;
        
        @Test
        public void testAnnotationAOP() {
        
            int add = calculator.add(10, 2);
            System.out.println("方法外部 add = " + add);
        
        }
        
    }
    

    打印效果如下:

    [AOP前置通知] 方法开始了

    方法内部 result = 12

    [AOP返回通知] 方法成功返回了

    [AOP后置通知] 方法最终结束了

    方法外部 add = 12

    ⑥通知执行顺序

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

    4、各个通知获取细节信息

    ①JoinPoint接口

    org.aspectj.lang.JoinPoint

    • 要点1:JoinPoint接口通过getSignature()方法获取目标方法的签名
    • 要点2:通过目标方法签名对象获取方法名
    • 要点3:通过JoinPoint对象获取外界调用目标方法时传入的实参列表组成的数组
    
    // @Before注解标记前置通知方法
    // value属性:切入点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
    // 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
    // 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
    @Before(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
    public void printLogBeforeCore(JoinPoint joinPoint) {
        
        // 1.通过JoinPoint对象获取目标方法签名对象
        // 方法的签名:一个方法的全部声明信息
        Signature signature = joinPoint.getSignature();
        
        // 2.通过方法的签名对象获取目标方法的详细信息
        String methodName = signature.getName();
        System.out.println("methodName = " + methodName);
        
        int modifiers = signature.getModifiers();
        System.out.println("modifiers = " + modifiers);
        
        String declaringTypeName = signature.getDeclaringTypeName();
        System.out.println("declaringTypeName = " + declaringTypeName);
        
        // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();
        
        // 4.由于数组直接打印看不到具体数据,所以转换为List集合
        List<Object> argList = Arrays.asList(args);
        
        System.out.println("[AOP前置通知] " + methodName + "方法开始了,参数列表:" + argList);
    }
    

    需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。

    ②方法返回值

    在返回通知中,通过@AfterReturning注解的returning属性获取目标方法的返回值

    // @AfterReturning注解标记返回通知方法
    // 在返回通知中获取目标方法返回值分两步:
    // 第一步:在@AfterReturning注解中通过returning属性设置一个名称
    // 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
    @AfterReturning(
            value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
            returning = "targetMethodReturnValue"
    )
    public void printLogAfterCoreSuccess(JoinPoint joinPoint, Object targetMethodReturnValue) {
        
        String methodName = joinPoint.getSignature().getName();
        
        System.out.println("[AOP返回通知] "+methodName+"方法成功结束了,返回值是:" + targetMethodReturnValue);
    }
    

    ③目标方法抛出的异常

    在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象

    // @AfterThrowing注解标记异常通知方法
    // 在异常通知中获取目标方法抛出的异常分两步:
    // 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
    // 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
    @AfterThrowing(
            value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
            throwing = "targetMethodException"
    )
    public void printLogAfterCoreException(JoinPoint joinPoint, Throwable targetMethodException) {
        
        String methodName = joinPoint.getSignature().getName();
        
        System.out.println("[AOP异常通知] "+methodName+"方法抛异常了,异常类型是:" + targetMethodException.getClass().getName());
    }
    

    打印效果局部如下:

    [AOP异常通知] div方法抛异常了,异常类型是:java.lang.ArithmeticException

    java.lang.ArithmeticException: / by zero

    at com.atguigu.aop.imp.CalculatorPureImpl.div(CalculatorPureImpl.java:42) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)

    5、重用切入点表达式

    在一处声明切入点表达式之后,其他有需要的地方引用这个切入点表达式。易于维护,一处修改,处处生效。声明方式如下:

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

    同一个类内部引用时:

      @Before(value = "declarPointCut()")
        public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
    

    在不同类中引用:

    
    @Around(value = "com.atguigu.spring.aop.aspect.LogAspect.declarPointCut()")
    public Object roundAdvice(ProceedingJoinPoint joinPoint) {
    

    而作为存放切入点表达式的类,可以把整个项目中所有切入点表达式全部集中过来,便于统一管理:

    @Component
    public class AtguiguPointCut {
        
        @Pointcut(value = "execution(public int *..Calculator.sub(int,int))")
        public void atguiguGlobalPointCut(){}
        
        @Pointcut(value = "execution(public int *..Calculator.add(int,int))")
        public void atguiguSecondPointCut(){}
        
        @Pointcut(value = "execution(* *..*Service.*(..))")
        public void transactionPointCut(){}
    }
    

    6、切入点表达式语法

    ①切入点表达式的作用

    ②语法细节

    • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限

    • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。

      • 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
    • 在包名的部分,使用“*..”表示包名任意、包的层次深度任意

    • 在类名的部分,类名部分整体用*号代替,表示类名任意

    • 在类名的部分,可以使用*号代替类名的一部分

      
      *Service
      

    上面例子表示匹配所有名称以Service结尾的类或接口

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

    上面例子表示匹配所有方法名以Operation结尾的方法

    • 在方法参数列表部分,使用(..)表示参数列表任意

    • 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头

    • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的

    • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的

    • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符

      
      execution(public int *..*Service.*(.., int))
      

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

    execution(* int *..*Service.*(.., int))
    

    但是public *表示权限修饰符明确,返回值任意是可以的。

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

    7、环绕通知

    环绕通知对应整个try...catch...finally结构,包括前面四种通知的所有功能。

    // 使用@Around注解标明环绕通知方法
    @Around(value = "com.atguigu.aop.aspect.AtguiguPointCut.transactionPointCut()")
    public Object manageTransaction(
        
            // 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,
            // Spring会将这个类型的对象传给我们
            ProceedingJoinPoint joinPoint) {
        
        // 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
        Object[] args = joinPoint.getArgs();
        
        // 通过ProceedingJoinPoint对象获取目标方法的签名对象
        Signature signature = joinPoint.getSignature();
        
        // 通过签名对象获取目标方法的方法名
        String methodName = signature.getName();
        
        // 声明变量用来存储目标方法的返回值
        Object targetMethodReturnValue = null;
        
        try {
        
            // 在目标方法执行前:开启事务(模拟)
            System.out.println("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));
        
            // 过ProceedingJoinPoint对象调用目标方法
            // 目标方法的返回值一定要返回给外界调用者
            targetMethodReturnValue = joinPoint.proceed(args);
        
            // 在目标方法成功返回后:提交事务(模拟)
            System.out.println("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetMethodReturnValue);
        
        }catch (Throwable e){
        
            // 在目标方法抛异常后:回滚事务(模拟)
            System.out.println("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());
        
        }finally {
        
            // 在目标方法最终结束后:释放数据库连接
            System.out.println("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);
        
        }
        
        return targetMethodReturnValue;
    }
    

    8、切面的优先级

    ①概念

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

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

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

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

    ②实际意义

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

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

    9、没有接口的情况

    在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。为了证明这一点,我们做下面的测试:

    ①创建目标类

    请确保这个类在自动扫描的包下,同时确保切面的切入点表达式能够覆盖到类中的方法。

    
    @Service
    public class EmployeeService {
        
        public void getEmpList() {
            System.out.println("方法内部 com.atguigu.aop.imp.EmployeeService.getEmpList");
        }
        
    }
    

    ②测试

     @Autowired
        private EmployeeService employeeService;
        
        @Test
        public void testNoInterfaceProxy() {
            employeeService.getEmpList();
            System.out.println();
        }
    

    ③Debug查看

    [1]没有实现接口情况

    [2]有实现接口的情况

    同时我们发现:Mybatis调用的Mapper接口类型的对象其实也是动态代理机制

    10、小结

    第六节 基于XML的AOP[了解]

    1、准备工作

    ①加入依赖

    和基于注解的AOP时一样。

    ②准备代码

    把测试基于注解功能时的Java类复制到新module中,去除所有注解。

    2、配置Spring配置文件

    <!-- 配置目标类的bean -->
    <bean id="calculatorPure" class="com.atguigu.aop.imp.CalculatorPureImpl"/>
        
    <!-- 配置切面类的bean -->
    <bean id="logAspect" class="com.atguigu.aop.aspect.LogAspect"/>
        
    <!-- 配置AOP -->
    <aop:config>
        
        <!-- 配置切入点表达式 -->
        <aop:pointcut id="logPointCut" expression="execution(* *..*.*(..))"/>
        
        <!-- aop:aspect标签:配置切面 -->
        <!-- ref属性:关联切面类的bean -->
        <aop:aspect ref="logAspect">
            <!-- aop:before标签:配置前置通知 -->
            <!-- method属性:指定前置通知的方法名 -->
            <!-- pointcut-ref属性:引用切入点表达式 -->
            <aop:before method="printLogBeforeCore" pointcut-ref="logPointCut"/>
        
            <!-- aop:after-returning标签:配置返回通知 -->
            <!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
            <aop:after-returning
                    method="printLogAfterCoreSuccess"
                    pointcut-ref="logPointCut"
                    returning="targetMethodReturnValue"/>
        
            <!-- aop:after-throwing标签:配置异常通知 -->
            <!-- throwing属性:指定通知方法中用来接收目标方法抛出异常的异常对象的参数名 -->
            <aop:after-throwing
                    method="printLogAfterCoreException"
                    pointcut-ref="logPointCut"
                    throwing="targetMethodException"/>
        
            <!-- aop:after标签:配置后置通知 -->
            <aop:after method="printLogCoreFinallyEnd" pointcut-ref="logPointCut"/>
        
            <!-- aop:around标签:配置环绕通知 -->
            <!--<aop:around method="……" pointcut-ref="logPointCut"/>-->
        </aop:aspect>
        
    </aop:config>
    

    3、测试

    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(value = {"classpath:spring-context.xml"})
    public class AOPTest {
        
        @Autowired
        private Calculator calculator;
        
        @Test
        public void testLogAspect() {
            int add = calculator.add(10, 2);
            System.out.println("add = " + add);
        }
    }
    

    第七节 AOP对获取bean的影响

    一、根据类型获取bean

    1、情景一

    • bean对应的类没有实现任何接口

    • 根据bean本身的类型获取bean

      • 测试:IOC容器中同类型的bean只有一个

        正常获取到IOC容器中的那个bean对象

      • 测试:IOC容器中同类型的bean有多个

        会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

    2、情景二

    • bean对应的类实现了接口,这个接口也只有这一个实现类
      • 测试:根据接口类型获取bean
      • 测试:根据类获取bean
      • 结论:上面两种情况其实都能够正常获取到bean,而且是同一个对象

    3、情景三

    • 声明一个接口

    • 接口有多个实现类

    • 接口所有实现类都放入IOC容器

      • 测试:根据接口类型获取bean

        会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

      • 测试:根据类获取bean

        正常

    4、情景四

    • 声明一个接口
    • 接口有一个实现类
    • 创建一个切面类,对上面接口的实现类应用通知
      • 测试:根据接口类型获取bean:正常获取
      • 测试:根据类获取bean:抛出异常:NoSuchBeanDefinitionException

    原因分析:

    • 应用了切面后,真正放在IOC容器中的是代理类的对象
    • 目标类并没有被放到IOC容器中,所以根据目标类的类型从IOC容器中是找不到的

    从内存分析的角度来说,IOC容器中引用的是代理对象,代理对象引用的是目标对象。IOC容器并没有直接引用目标对象,所以根据目标类本身在IOC容器范围内查找不到。

    debug查看代理类的类型:

    5、情景五

    • 声明一个类
    • 创建一个切面类,对上面的类应用通知
      • 测试:根据类获取bean,能获取到

    debug查看实际类型:

    二、自动装配

    自动装配需先从IOC容器中获取到唯一的一个bean才能够执行装配。所以装配能否成功和装配底层的原理,和前面测试的获取bean的机制是一致的。

    1、情景一

    • 目标bean对应的类没有实现任何接口

    • 根据bean本身的类型装配这个bean

      • 测试:IOC容器中同类型的bean只有一个

        正常装配

      • 测试:IOC容器中同类型的bean有多个

        会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

    2、情景二

    • 目标bean对应的类实现了接口,这个接口也只有这一个实现类

      • 测试:根据接口类型装配bean

        正常

      • 测试:根据类装配bean

        正常

    3、情景三

    • 声明一个接口

    • 接口有多个实现类

    • 接口所有实现类都放入IOC容器

      • 测试:根据接口类型装配bean

        @Autowired注解会先根据类型查找,此时会找到多个符合的bean,然后根据成员变量名作为bean的id进一步筛选,如果没有id匹配的,则会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

      • 测试:根据类装配bean

        正常

    4、情景四

    • 声明一个接口

    • 接口有一个实现类

    • 创建一个切面类,对上面接口的实现类应用通知

      • 测试:根据接口类型装配bean

        正常

      • 测试:根据类装配bean

        此时获取不到对应的bean,所以无法装配,抛出下面的异常:

    Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'fruitApple' is expected to be of type 'com.atguigu.bean.impl.FruitAppleImpl' but was actually of type 'com.sun.proxy.$Proxy15'

    5、情景五

    • 声明一个类

    • 创建一个切面类,对上面的类应用通知

      • 测试:根据类装配bean

        正常

    三、总结

    1、对实现了接口的类应用切面

    2、对没实现接口的类应用切面

  • 相关阅读:
    在某个点上弹出层
    根据表名、过程名、视图名查找对应的数据库
    js时间转换nan问题 兼容浏览器
    过滤html标记 以及 返回顶部
    自定义控件 加入include 报错 问题
    在有索引视图的表上新增、修改、删除 报错 set ARITHABORT 选项不对
    访微博代码
    兼容问题 链接不跳转
    js上下滚动代码
    onload问题
  • 原文地址:https://www.cnblogs.com/tianwenxin/p/14960309.html
Copyright © 2020-2023  润新知