• spring复习04,静态代理动态代理,AOP


    概念引入

    先用一个小例子,来引入我们为什么需要代理,以及代理是什么?
    假设当前有一个业务需求,一个加减乘除的四则运算:
    方法接口:

    package com.gothic.sunset;
    
    public interface Calculate {
    
        public int add(int a,int b);
    
        public int sub(int a,int b);
    
        public int mul(int a,int b);
    
        public int div(int a,int b);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    方法实现类:

    package com.gothic.sunset;
    
    public class CalculateImpl implements Calculate{
    
        @Override
        public int add(int a, int b) {
            int result = a + b ;
            return result;
        }
    
        @Override
        public int sub(int a, int b) {
            int result = a - b ;
            return result;
        }
    
        @Override
        public int mul(int a, int b) {
            int result = a * b ;
            return result;
        }
    
        @Override
        public int div(int a, int b) {
            int result = a / b ;
            return result;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    测试类|用户端:

    package com.gothic.sunset;
    
    public class Client {
        public static void main(String[] args) {
            CalculateImpl calculate = new CalculateImpl();
            System.out.println(calculate.add(1,2));//3
            System.out.println(calculate.sub(2,1));//1
            System.out.println(calculate.mul(1,2));//2
            System.out.println(calculate.div(2,1));//2
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    新的需求:需要看到每次执行的是什么方法,以及返回的结果是什么,和参与运算的数字分别是什么。
    大家可以想一下,怎么修改,以及在原有的业务代码上面修改还是通过别的方法进行修改更好一点。答案当然是后者,因为当你原有的业务代码如果有很多呢,那你一个一个的修改,岂不是很浪费时间和拉低效率,而且假设你别的业务也需要添加类似的功能,怎么办也是在原有的业务代码上修改吗?


    java代理模式

    java代理模式,即Proxy Pattern,23种java常用设计模式之一。代理模式的定义:对其他对象提供一种代理以控制对这个对象的访问。

    代理模式的主要作用是为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不想或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
    代理模式的思想是为了提供额外的处理或者不同的操作而在实际对象与调用者之间插入一个代理对象。这些额外的操作通常需要与实际对象进行通信。

    在这里插入图片描述


    静态代理

    需求:需要看到每次执行的是什么方法,以及返回的结果是什么,和参与运算的数字分别是什么。

    解决上面的需求,需要抽象出对应的代理类,然后在代理类中添加新增的业务功能,并且调用其对应的实体类对象。
    如下:
    代理类的抽象,代理类也需要实现相同的接口,从而添加新增的业务功能。需要注意的是,它需要将真实角色注入,然后在原有的方法中去调用真实角色的方法即可。

    package com.gothic.sunset;
    
    public class CalculateProxy implements Calculate{
        private Calculate calculate;
    
        public void setCalculate(Calculate calculate) {
            this.calculate = calculate;
        }
    
        @Override
        public int add(int a, int b) {
            argPrint(a,b);
            logBack("add");
            int rs = calculate.add(a,b);
            rsPrint(rs);
            return rs;
        }
    
        @Override
        public int sub(int a, int b) {
            argPrint(a,b);
            logBack("sub");
            int rs = calculate.sub(a,b);
            rsPrint(rs);
            return rs;
        }
    
        @Override
        public int mul(int a, int b) {
            argPrint(a,b);
            logBack("mul");
            int rs = calculate.mul(a,b);
            rsPrint(rs);
            return rs;
        }
    
        @Override
        public int div(int a, int b) {
            argPrint(a,b);
            logBack("div");
            int rs = calculate.div(a,b);
            rsPrint(rs);
            return rs;
        }
    
        public void argPrint(int a,int b){
            System.out.println("参数A:"+a+"参数B:"+b);
        }
    
        public void logBack(String msg){
            System.out.println("执行了"+msg+"方法");
        }
    
        public void rsPrint(int rs){
            System.out.println("结果为--->"+rs);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    (用户端)测试输出:

    package com.gothic.sunset;
    
    public class Client {
        public static void main(String[] args) {
            CalculateImpl calculate = new CalculateImpl();
            CalculateProxy cp = new CalculateProxy();
            cp.setCalculate(calculate);
            cp.add(1,2);
            cp.sub(8,1);
            cp.mul(2,3);
            cp.div(8,4);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    静态代理模式的好处:

    • 可以使真实角色的操作更加纯粹,不用去关心一些公共的业务
    • 公共就是交给代理角色,实现了业务的分工
    • 公共业务发生扩展的时候,方便集中管理

    静态代理模式的缺点:

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


    动态代理

    动态代理:

    • 动态代理和静态代理角色一样
    • 动态代理的代理类是动态生成的,不是我们直接写好的
    • 动态代理分为两大类:基于接口的动态代理 ,基于类的动态代理
    • 基于接口的动态代理:jdk动态代理
    • 基于类的动态代理:cglib

    jdk动态代理

    JDK动态代理是指:代理类实例在程序运行时,由JVM根据反射机制动态的生成。也就是说代理类不是用户自己定义的,而是由JVM生成的。

    1. InvocationHandler接口

    代理实例的调用处理器需要实现InvocationHandler接口,并且每个代理实例都有一个关联的调用处理器。当一个方法在代理实例上被调用时,这个方法调用将被编码并分派到其调用处理器的invoke方法上。

    也就是说,我们创建的每一个代理实例都要有一个关联的InvocationHandler,并且在调用代理实例的方法时,会被转到InvocationHandler的invoke方法上。
    publicObject invoke(Object proxy, Method method, Object[] args) throws Throwable;
    该invoke方法的作用是:处理代理实例上的方法调用并返回结果。

    其有三个参数,分别为:

    1. proxy:是调用该方法的代理实例。
    2. method:是在代理实例上调用的接口方法对应的Method实例。
    3. args:一个Object数组,是在代理实例上的方法调用中传递的参数值。如果接口方法为无参,则该值为null。
      其返回值为:调用代理实例上的方法的返回值。

    2. Proxy类

    Proxy类提供了创建动态代理类及其实例的静态方法,该类也是动态代理类的超类。

    代理类具有以下属性:

    代理类的名称以 “$Proxy” 开头,后面跟着一个数字序号。代理类继承了Proxy类。代理类实现了创建时指定的接口(JDK动态代理是面向接口的)。
    每个代理类都有一个公共构造函数,它接受一个参数,即接口InvocationHandler的实现,用于设置代理实例的调用处理器。
    Proxy提供了两个静态方法,用于获取代理对象。

    getProxyClass,用于获取代理类的Class对象,再通过调用构造函数创建代理实例。

    	@CallerSensitive
        public static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces)throws IllegalArgumentException
    
    • 1
    • 2

    该方法有两个参数:

    1. loader:为类加载器。
    2. intefaces:为接口的Class对象数组。

    方法返回值为动态代理类的Class对象。

    newProxyInstance,用于创建一个代理实例。

    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h)  throws IllegalArgumentException
    
    • 1

    该方法有三个参数:

    1. loader:为类加载器。
    2. interfaces:为接口的Class对象数组。
    3. h:指定的调用处理器。

    返回值为指定接口的代理类的实例。

    Proxy类主要用来获取动态代理对象,InvocationHandler接口主要用于方法调用的约束与增强。

    jdk动态代理代码

    创建一个代理实例的调用处理器,需要实现InvocationHandler接口

    proxy:代理对象
    method 表示要执行的方法,
    args要执行的方法的参数列表。
    通过method.getName可以获取对应方法名

    package com.gothic.sunset;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.util.Arrays;
    
    public class ProxyInvocationHandler implements InvocationHandler {
    
        //被代理的接口
        private Object target;
    
        //set注入不同的接口
        public void setTarget(Object target) {
            this.target = target;
        }
    
        //得到生成的代理类
        public Object getProxy(){
            return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
        }
    
        //处理代理类实例,并且返回结果
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object rs =null;
            try {
                System.out.println("日志,方法"+method.getName()+",参数"+ Arrays.toString(args));
                //proxy 表示代理对象 ,method 表示要执行的方法,args要执行的方法的参数列表
                rs = method.invoke(target, args);
                System.out.println("日志,方法"+method.getName()+",结果"+ rs);
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("日志,方法"+method.getName()+",异常"+ e);
            }  finally {
                System.out.println("日志,方法"+method.getName()+",执行完毕");
            }
            return rs;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    方法测试:

    package com.gothic.sunset;
    
    public class Client {
        public static void main(String[] args) {
            //实际角色
            CalculateImpl calculate = new CalculateImpl();
            //代理角色 不存在
            ProxyInvocationHandler pih = new ProxyInvocationHandler();
            //设置要代理的对象
            pih.setTarget(calculate);
            //动态生成代理类
            Calculate proxy = (Calculate) pih.getProxy();
            proxy.add(1, 2);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    输出:
    在这里插入图片描述


    AOP

    AOP的概念

    AOP为Aspect Oriented Programming的缩写,意为:面向切面编程通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

    相关术语:

    1. 连接点(join Point):程序执行的某个特定位置,比如(类开始初始化前,初始化后,方法前,方法后,异常后) 一个类或者一段程序代码拥有一些具有边界的特定点,这些特定点称为连接点
    2. 切点(pointcut):每个程序都拥有多个连接点,比如一个类有2个方法,这2个方法就是连接点,连接点就是程序中具体的事物,AOP 通过切点来定位特定的连接点,连接点相当于数据库中的记录,而切点相当于是查询记录的条件。在程序中,切点是连接点位置的集合
    3. 增强(advice):就是我们要具体做的事情,也就在原有的方法之上添加新的能力
    4. 目标对象:就是我们要增强的类;
    5. 引入:特殊的增强,为类添加属性和方法。哪怕这个类没有任何的属性和方法我们也可以通过aop去添加方法和属性实现逻辑
    6. 织入(weaving):就是把增强添加到目标类具体的连接点上的过程
    7. 代理:一个类被AOP织入增强后产生的结果类,他是原类和增强后的代理类,更具代理s不同的方式,代理类可能是和原类具有相同接口的类,也可能是原类的子类
    8. 切面(Aspect):切面由切点和增强组成,他既包含横切的定义,也包括了连接点的定义,spring aop就是负责实施切面的框架,他将切面定义为横切逻辑织入到切面所指定的连接点

    spirng中aop的实现

    首先在pom.xml中导入依赖:

    	<dependencies>
            
            <dependency>
                <groupId>org.springframeworkgroupId>
                <artifactId>spring-contextartifactId>
                <version>5.3.1version>
            dependency>
            
            <dependency>
                <groupId>junitgroupId>
                <artifactId>junitartifactId>
                <version>4.12version>
                <scope>testscope>
            dependency>
            
            <dependency>
                <groupId>org.springframeworkgroupId>
                <artifactId>spring-aspectsartifactId>
                <version>5.3.1version>
            dependency>
        dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    方式一:spring原生的接口

    然后观察其导入的jar包
    在这里插入图片描述

    只要实现下面对应的spring接口即可完成原生的springAop
    在这里插入图片描述
    新建一个SpringProxyTest 类,然后实现spring中的aop相关的一些接口:

    package com.gothic.sunset;
    
    import org.springframework.aop.AfterReturningAdvice;
    import org.springframework.aop.MethodBeforeAdvice;
    
    import java.lang.reflect.Method;
    import java.util.Arrays;
    
    public class SpringProxyTest implements MethodBeforeAdvice, AfterReturningAdvice {
    
        /**
         *
         * @param method 要执行的目标对象的方法名
         * @param objects 参数数组
         * @param o 要执行的目标对象
         * @throws Throwable
         */
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("执行开始--->"+o.getClass().getName()+"中的方法"+method.getName()+"----参数为:"+ Arrays.toString(objects));
        }
    
        @Override
        public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
            System.out.println("执行完毕--->"+target.getClass().getName()+"中的方法"+method.getName()+"----参数为:"+ Arrays.toString(args)+"结果为:"+returnValue.toString());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    我这里只实现类前置通知和后置通知,其余的大家可自行补充,和jdk动态代理也差不多(其实还是主要用注解去设置,这里只是另一种方法,注解会在下面使用)。
    编写spring配置文件:

    1. 在spring中向ioc容器注册bean:
     	
        <bean id="calculate" class="com.gothic.sunset.CalculateImpl"/>
        <bean id="springProxyTest" class="com.gothic.sunset.SpringProxyTest"/>
    
    • 1
    • 2
    • 3
    1. 在spring配置文件中的头文件导入aop约束:
    xmlns:aop="http://www.springframework.org/schema/aop"
    
    • 1

    3.配置aop:

    	
        <aop:config>
            
            <aop:pointcut id="pt" expression="execution(* com.gothic.sunset.CalculateImpl.*(..))"/>
            
            <aop:advisor advice-ref="springProxyTest" pointcut-ref="pt"/>
        aop:config>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    junit测试用例:

    import com.gothic.sunset.Calculate;
    import com.gothic.sunset.CalculateImpl;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class Mytest {
        @Test
        public void testAop(){
            ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            Calculate calculate = (Calculate) context.getBean("calculate");
            calculate.add(1,2);
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    输出:
    在这里插入图片描述

    方式二:spring中注解配置aop

    注解解释:

    1. 通过注解 @Aspect标识一个类作为切面类
    2. 前置通知:使用@Before注解标识,在被代理的目标方法前执行
    3. 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行
    4. 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行
    5. 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行
    6. 环绕通知:使用@Around注解标识,使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

    JointPoint对象:
    JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.
    常用API:

    方法名功能
    Signature getSignature();获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
    Object[] getArgs();获取传入目标方法的参数对象
    Object getTarget();获取被代理的对象
    Object getThis();获取代理对象

    新建一个AnnoatationPointCut 类,用来实现切面(将切面类通过注解@Aspect标识):

    package com.gothic.sunset;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.*;
    
    import java.util.Arrays;
    
    @Aspect //标注这个类是一个切面类
    public class AnnoatationPointCut {
    
        //切入点的重用,通过 @PointCut注解的value属性值来设置,然后其他的各种通知注解的value属性值,即可简写
        @Pointcut("execution(* com.gothic.sunset.CalculateImpl.*(..))")
        public void pointCut(){};
    
    
        //获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参
        //前置通知
        @Before("pointCut()")
        public void beforeAdviceMethod(JoinPoint joinPoint){
            //获取目标对象签名信息
            Signature signature = joinPoint.getSignature();
            //获取目标对象的参数列表
            Object[] args = joinPoint.getArgs();
            System.out.println("LoggerAspect,前置通知,方法:"+signature.getName()+",参数:"+ Arrays.toString(args));
        }
    
        //后置通知
        @After("pointCut()")
        public void afterAdviceMethod(JoinPoint joinPoint){
            Signature signature = joinPoint.getSignature();
            System.out.println("LoggerAspect,后置通知,方法:"+signature.getName());
        }
    
    
        //回溯通知
        @AfterReturning(value = "pointCut()",returning = "result")
        public void afterReturningAdviceMethod(JoinPoint joinPoint,Object result){
            Signature signature = joinPoint.getSignature();
            System.out.println("LoggerAspect,回溯通知,方法:"+signature.getName()+",结果:"+result.toString());
        }
    
    
        //异常通知
        @AfterThrowing(value = "pointCut()",throwing = "ex")
        public void afterThrowException(JoinPoint joinPoint,Throwable ex){
            Signature signature = joinPoint.getSignature();
            System.out.println("LoggerAspect,回溯通知,方法:"+signature.getName()+",异常:"+ex);
        }
    
    
        //环绕通知   --- 切面类型的返回值必须与目标对象类型一致
        @Around("pointCut()")
        public Object aroundMethod(ProceedingJoinPoint joinPoint){
            String methodName = joinPoint.getSignature().getName();
            String args = Arrays.toString(joinPoint.getArgs());
            Object result = null;
            try {
                System.out.println("环绕通知-->目标对象方法执行之前");
                //目标方法的执行,目标方法的返回值一定要返回给外界调用者
                result = joinPoint.proceed();
                System.out.println("环绕通知-->目标对象方法返回值之后");
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                System.out.println("环绕通知-->目标对象方法出现异常时");
            } finally {
                System.out.println("环绕通知-->目标对象方法执行完毕");
            }
            return result;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    修改spring配置文件:

    基于注解的aop需要使用,开启AspectJ的自动代理,为目标对象自动生成代理

    测试输出:

    import com.gothic.sunset.Calculate;
    import com.gothic.sunset.CalculateImpl;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class Mytest {
        @Test
        public void testAop(){
            ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            Calculate calculate = (Calculate) context.getBean("calculate");
            calculate.add(1,2);
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述


    好嘞,AOP就暂时到这里啦!!!!AOP的相关术语大家理解即可,不需要死究定义(根据例子慢慢感悟)。
    在这里插入图片描述

  • 相关阅读:
    设计模式概述
    查看进程与对应的线程
    用Unity重现《空洞骑士》的苦痛之路(2)——地图篇
    PHP对接钉钉群机器人
    Ubuntu搭建Hadoop环境
    Linux中Ctrl+C和Ctrl+Z的区别_实战讲解(超详细)
    Python 图片处理笔记
    修改origin分支名
    如何使用正则表达式实现Java日志信息的抓取与收集
    【无标题】力扣报错:member access within null pointer of type ‘struct ListNode‘
  • 原文地址:https://blog.csdn.net/m0_63622279/article/details/128082960