09.AOP-尚硅谷Spring零基础入门到进阶,一套搞定spring6全套视频教程(源码级讲解)

发布于:2024-07-11 ⋅ 阅读:(21) ⋅ 点赞:(0)

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

  • 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
  • 附加功能分散在各个业务功能方法中,不利于统一维护
    解决思路
    解决核心:解耦。把附加功能从业务功能代码中抽取出来。
    困难
    解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引
    入新的技术。

代理模式

概念

介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时
候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中
剥离出来一一解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够
集中在一起也有利于统一维护。
相关术语
,代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
·目标:被代理“套用”了非核心逻辑代码的类、对象、方法。

场景模拟

  1. 声明计算器接口Calculator,包含加减乘除的抽象方法

    package org.example;  
    
    public interface Calculator {  
        public int add(int i, int j);  
    
        public int sub(int i, int j);  
    
        public int mul(int i, int j);  
    
        public int div(int i, int j);  
    }
    
  2. 写一个实现Calculator业务的实现类

    package org.example;
    public class CalculatorImpl 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. 写一个实现Calculator业务的带有日志功能的实现类

    package org.example;  
    public class CalculatorLogImpl implements Calculator {
        @Override
        public int add(int i, int j) {
            System.out.println("计算开始,i=" + i + "j=" + j);
            int result = i + j;
            System.out.println("计算结束,i=" + i + "j=" + j + "result=" + result);
            System.out.println("result=" + result);
            return result;
        }
        @Override
        public int sub(int i, int j) {
            System.out.println("计算开始,i=" + i + "j=" + j);
            int result = i - j;
            System.out.println("计算结束,i=" + i + "j=" + j + "result=" + result);
            System.out.println("result=" + result);
            return result;
        }
        @Override
        public int mul(int i, int j) {
            System.out.println("计算开始,i=" + i + "j=" + j);
            int result = i * j;
            System.out.println("计算结束,i=" + i + "j=" + j + "result=" + result);
            System.out.println("result=" + result);
            return result;
        }
        @Override
        public int div(int i, int j) {
            System.out.println("计算开始,i=" + i + "j=" + j);
            int result = i / j;
            System.out.println("计算结束,i=" + i + "j=" + j + "result=" + result);
            System.out.println("result=" + result);
            return result;
        }
    }
    

静态代理

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

动态代理

使用java.lang.reflect.Proxy类实现动态代理
官方示例代码

InvocationHandler handler = new MyInvocationHandler(...);  
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),  
        new class<?>[]{Foo.class},  
        handler);

创建一个代理工厂类

package org.example;  
  
import lombok.val;  
  
import javax.print.attribute.standard.JobKOctets;  
import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
import java.lang.reflect.Proxy;  
  
public class ProxyFactory {  
    Object target;  
  
    public ProxyFactory(Object target) {  
        this.target = target;  
    }  
  
    public Object getProxy() {  
/*      有三个参数  
        第一个参数:CLassLoader:加载动态生成代理类的来加载器  
        第二个参数:CLass[]interfaces:目录对象实现的所有接口cLass类型数组  
        第三个参数:InvocationHandler:设置代理对象实现目标对象方法的过程*/  
        ClassLoader cLassLoader = target.getClass().getClassLoader();  
        Class[] classes = target.getClass().getInterfaces();  
        InvocationHandler invocationHandler = new InvocationHandler() {  
            @Override  
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
                //调用方法前日志  
                System.out.println("[动态代理][调用前日志]" + method.getName() + "参数:" + args);  
                //调用目标方法  
                Object result = method.invoke(target, args);  
                //调用方法后日志  
                System.out.println("[动态代理][调用后日志]" + method.getName() + "参数:" + args);  
                return result;  
            }  
        };  
        return Proxy.newProxyInstance(cLassLoader, classes, invocationHandler);  
    }  
}

编写测试类

@Test  
public void calculatorTest(){  
    ProxyFactory proxyFactory=new ProxyFactory(new CalculatorImpl());  
    Calculator proxy=(Calculator) proxyFactory.getProxy();  
    proxy.add(1,1);  
}

输出结果

[动态代理][调用前日志]add参数:[Ljava.lang.Object;@7d0587f1
result=2
[动态代理][调用后日志]add参数:[Ljava.lang.Object;@7d0587f1

基于注解的AOP

动态代理分类:JDK动态代理和cglib动态代理
JDK动态代理生成接口实现类代理对象
cglib动态代理继承被代理的目标类,生成子类代理对象,不需要目标类实现接口

  • 有接口可以使用JDK动态代理和cblib动态代理
  • 没有接口只能使用cblib动态代理
    Aspect:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入"被代理的目标类编译得到的字节码
    文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了Aspect)中的注解。

使用AOP步骤

  1. 引入aop相关依赖

    <!--spring aop依赖-->  
    <dependency>  
        <groupId>org.springframework</groupId>  
        <artifactId>spring-aop</artifactId>  
        <version>6.0.2</version>  
    </dependency>  
    <!--spring aspects依赖-->  
    <dependency>  
        <groupId>org.springframework</groupId>  
        <artifactId>spring-aspects</artifactId>  
        <version>6.0.2</version>  
    </dependency>
    
  2. 创建目标资源

    1. 接口

      package com.example.annoAOP;  
      
      public interface Calculator {  
          public int add(int i, int j);  
      
          public int sub(int i, int j);  
      
          public int mul(int i, int j);  
      
          public int div(int i, int j);  
      }
      
    2. 实现类

      package com.example.annoAOP;  
      
      import org.springframework.stereotype.Component;  
      
      @Component  
      public class CalculatorImpl 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;  
          }  
      }
      

第三步创建切面类

  1. 创建bean.xml,使用AOP约束,开启AOP功能和扫描功能

    <?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    http://www.springframework.org/schema/context/spring-context.xsd    http://www.springframework.org/schema/aop  
        http://www.springframework.org/schema/aop/spring-aop.xsd">  
        <!-- 开启组件扫描           -->  
        <context:component-scan base-package="com.example"/>  
        <!--开启aspectj自动代理,为目标对象生成代理-->  
        <aop:aspectj-autoproxy></aop:aspectj-autoproxy>  
    </beans>
    
  2. 创建LogAscept类,增加一个方法的前置切入点

    package com.example.annoAOP;  
      
    import org.aspectj.lang.annotation.Aspect;  
    import org.aspectj.lang.annotation.Before;  
    import org.springframework.stereotype.Component;  
      
    @Aspect//表明这是一个AOP文件  
    @Component//让IoC进行管理  
    public class LogAspect {  
      
        //设置切入点和通知类型  
        //通知类型:  
        // 前置   @Before(value="切入点表达式")  
        // 返回   @AfterReturning    // 异常   @AfterThrowing    // 后置   @After()    // 环绕   @Around()    //切入点表达式写法:execution(权限修饰 方法返回值 方法所在全类名.方法名 (参数列表))  
        //execution:固定语法  
        //权限修饰:这里写*表示权限修饰符和返回值任意  
        //方法所在全类名:写*表示任意包名;写*...表示包名任意同时包层次深度任意  
        //类名用*号代替表示类名任意,部分用*代替,如*Service,表示匹配以Service结尾的列或接口  
        //方法名:用*号代替表示方法名任意;部分用*代替,如get*,表示匹配以get开头的方法  
        //参数列表可以使用(...)形式表示参数列表任意  
      
        @Before(value = "execution(public int com.example.annoAOP.CalculatorImpl.add (int,int))")  
        public void beforeAdd() {  
            System.out.println("[前置通知][add()]计算开始");  
        }  
    }
    

    方法表达式写法:

    在这里插入图片描述

  3. 创建测试方法

    @Test
    public void testAOPAdd(){
       ApplicationContext applicationContext=new ClassPathXmlApplicationContext("bean.xml");
       Calculator calculator=applicationContext.getBean(Calculator.class);
       	calculator.add(1,1);
    }
    
  4. 输出结果

    [前置通知][add()]计算开始
    result=2
    

通知类型

  • 前置通知:在被代理的目标方法前执行
  • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
  • 异常通知:在被代理的目标方法异常结束后执行(死于非命)
  • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
  • 环绕通知:使用try.catch.finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
    修改LogAspect类,添加五种通知方法
package com.example.annoAOP;  
  
import org.aspectj.lang.JoinPoint;  
import org.aspectj.lang.ProceedingJoinPoint;  
import org.aspectj.lang.annotation.*;  
import org.springframework.stereotype.Component;  
  
@Aspect//表明这是一个AOP文件  
@Component//让IoC进行管理  
public class LogAspect {  
  
    //前置通知  
    @Before(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))")  
    public void beforeMethod(JoinPoint joinPoint) {  
        String MethodName = joinPoint.getSignature().getName();  
        Object[] args = joinPoint.getArgs();  
        System.out.println("[前置通知][CalculatorImpl.MethodName=" + MethodName + "()");  
        System.out.println("Args[]=" + args);  
    }  
  
    //后置通知  
    @After(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))")  
    public void afterMethod(JoinPoint joinPoint) {  
        String MethodName = joinPoint.getSignature().getName();  
        Object[] args = joinPoint.getArgs();  
        System.out.println("[后置通知][CalculatorImpl.MethodName=" + MethodName + "()");  
        System.out.println("Args[]=" + args);  
    }  
  
    //返回通知  
    @AfterReturning(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))", returning = "result")  
    public void afterReturnMethod(JoinPoint joinPoint, Object result) {  
        String MethodName = joinPoint.getSignature().getName();  
        System.out.println("[返回通知][CalculatorImpl.MethodName=" + MethodName + "()");  
        System.out.println("[返回通知]result=" + result);  
    }  
  
    //异常通知  
    @AfterThrowing(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))", throwing = "exp")  
    public void afterThrowing(JoinPoint joinPoint, Throwable exp) {  
        String MethodName = joinPoint.getSignature().getName();  
        System.out.println("[异常通知][CalculatorImpl.MethodName=" + MethodName + "()");  
        System.out.println(exp);  
    }  
  
    //环绕通知  
    @Around("execution(* com.example.annoAOP.CalculatorImpl.* (..))")  
    //ProceedingJoinPoint继承JoinPoint,比JoinPoint功能更强大,可以更好的调用目标方法  
    public Object around(ProceedingJoinPoint joinPoint) {  
        Object result = null;  
        try {  
            System.out.println("环绕通知-目标方法执行前");  
            result = joinPoint.proceed();  
            System.out.println("环绕通知-目标方法执行后");  
        } catch (Throwable throwable) {  
            System.out.println("环绕通知-目标方法执行异常");  
        } finally {  
            System.out.println("环绕通知-目标方法执行完成");  
        }  
        return result;  
    }  
}

输出结果

环绕通知-目标方法执行前
[前置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@62727399
result=2
[返回通知][CalculatorImpl.MethodName=add()
[返回通知]result=2
[后置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@62727399
环绕通知-目标方法执行后
环绕通知-目标方法执行完成

编写测试方法,使测试方法引发异常

@Test  
public void testAOPexp(){  
    ApplicationContext applicationContext=new ClassPathXmlApplicationContext("bean.xml");  
    Calculator calculator=applicationContext.getBean(Calculator.class);  
    calculator.div(1,0);  
}

运行结果

环绕通知-目标方法执行前
[前置通知][CalculatorImpl.MethodName=div()
Args[]=[Ljava.lang.Object;@4d9ac0b4
[异常通知][CalculatorImpl.MethodName=div()
java.lang.ArithmeticException: / by zero
[后置通知][CalculatorImpl.MethodName=div()
Args[]=[Ljava.lang.Object;@4d9ac0b4
环绕通知-目标方法执行异常
环绕通知-目标方法执行完成

[之后是异常报错信息]

重用切入点

  1. 定义一个切入点

    	package com.example.annoAOP;
    @Pointcut(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))")  
    public void pointCut() {}
    
  2. 使用切入点

    1. 内部使用切入点

      @After(value = "pointCut")  
      public void afterMethod(JoinPoint joinPoint) {  
          String MethodName = joinPoint.getSignature().getName();  
          Object[] args = joinPoint.getArgs();  
          System.out.println("[后置通知][CalculatorImpl.MethodName=" + MethodName + "()");  
          System.out.println("Args[]=" + args);  
      }  
      
    2. 外部使用切入点

      @After(value = "com.example.annoAOP.pointCut")  
      public void afterMethod(JoinPoint joinPoint) {  
          String MethodName = joinPoint.getSignature().getName();  
          Object[] args = joinPoint.getArgs();  
          System.out.println("[后置通知][CalculatorImpl.MethodName=" + MethodName + "()");  
          System.out.println("Args[]=" + args);  
      }  
      

切面的优先级

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

  • 优先级高的切面:外面
  • 优先级低的切面:里面
    使用@Order注解可以控制切面的优先级:
  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

XML形式配置AOP

  1. 创建新包xmlaop,复制上文接口、实现类、AOP配置类

  2. 删除LogAspect类的@Aspect注解和AOP注解

  3. 新建XmlAop.xml配置文件

    <?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    http://www.springframework.org/schema/context/spring-context.xsd    http://www.springframework.org/schema/aop    http://www.springframework.org/schema/aop/spring-aop.xsd">  
        <!-- 开启组件扫描           -->  
        <context:component-scan base-package="com.example.xmlAOP"/>  
        <!--配置AOP-->  
        <aop:config>  
            <!-- 配置切面类       -->  
            <aop:aspect ref="logAspect">  
                <!-- 配置切入点       -->  
                <aop:pointcut id="cutpoint" expression="execution(* com.example.xmlAOP.CalculatorImpl.* (..))"/>  
                <!-- 配置方法执行前通知       -->  
                <aop:before method="beforeMethod" pointcut-ref="cutpoint"/>  
                <!-- 配置方法执行后通知       -->  
                <aop:after method="afterMethod" pointcut-ref="cutpoint"/>  
                <!-- 配置方法返回后通知       -->  
                <aop:after-returning method="afterReturnMethod" pointcut-ref="cutpoint" returning="result"/>  
                <!-- 配置环绕通知       -->  
                <aop:around method="around" pointcut-ref="cutpoint"/>  
                <!-- 配置异常通知       -->  
                <aop:after-throwing method="afterThrowing" pointcut-ref="cutpoint" throwing="exp"/>  
            </aop:aspect>  
    
        </aop:config>  
    
    </beans>
    
  4. 编写测试方法

    @Test  
    public void testXML_AOP(){
        ApplicationContext applicationContext=new ClassPathXmlApplicationContext("XmlAop.xml");
        //本项目存在两个Calculator,需要注意使用的是哪个Calculator类
        com.example.xmlAOP.Calculator calculator=applicationContext.getBean(com.example.xmlAOP.Calculator.class);
        calculator.add(1,1);
    }
    
  5. 输出结果

[前置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@eda25e5
环绕通知-目标方法执行前
result=2
环绕通知-目标方法执行后
环绕通知-目标方法执行完成
[返回通知][CalculatorImpl.MethodName=add()
[返回通知]result=2
[后置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@eda25e5