Spring AOP—深入动态代理 万字详解(通俗易懂)

news2024/11/26 0:45:06

目录

一、前言

二、动态代理快速入门

        1.为什么需要动态代理? : 

        2.动态代理使用案例:

        3.动态代理的灵活性 : 

三、深入动态代理

        1.需求 : 

        2.实现 : 

            2.1 接口和实现类

            2.2 提供代理对象的类

            2.3 测试类

        3.引出AOP : 

四、总结


一、前言

  • 第四节内容,up打算和大家分享Spring 动态代理 相关的内容动态代理本质上是Spring AOP的一个前置的引入内容,一个AOP开篇之作,但它却相当重要,且本身难度较大
  • 注意事项——①代码中的注释也很重要;不要眼高手低,自己跟着过一遍才真正有收获;点击文章的侧边栏目录或者文章开头的目录可以进行跳转。
  • 良工不示人以朴,up所有文章都会适时补充完善。大家如果有问题都可以在评论区进行交流或者私信up。感谢阅读!

二、动态代理快速入门

        1.为什么需要动态代理? : 

        在日常开发中,往往存在这样一种需求——同时存在多个对象,这些对象对应的类都实现了同一接口,并且这些对象会去调用这个接口中的某一个方法,即多态,但是我们要求这几个对象在调用方法前,和调用方法后都要做一些业务处理,eg : 权限校验、事务管理、日志管理、安全校验等。

        如果我们将这些相同的业务处理,都下沉到每一个具体的类,就会造成代码冗余,并且没有办法进行对象的统一管理和调用;而动态代理的出现,尤对其症地解决了这个问题。

        2.动态代理使用案例:

                up先在dynamic_proxy包下创建Animal接口,以及Cat, Dog类,Cat类和Dog类都实现了Animal接口。如下图所示 : 

                Animal接口代码如下 : 

package com.cyan.spring.dynamic_proxy;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public interface Animal {
    public abstract void eat();
}

                Cat类代码如下 :  

package com.cyan.spring.dynamic_proxy;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public class Cat implements Animal{
    @Override
    public void eat() {
        System.out.println("小猫爱吃鱼捏~");
    }
}

                Dog类代码如下 : 

package com.cyan.spring.dynamic_proxy;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public class Dog implements Animal{
    @Override
    public void eat() {
        System.out.println("小狗爱吃骨头捏~");
    }
}

                然后,我们创建一个AnimalProxyProvider类,见名知意,这个类可以提供一个Animal接口的代理对象,所以,该类中肯定会定义一个方法,用来返回Animal接口的代理实例,当然,这个方法稍微有点复杂,大家可以借助up的代码注释逐渐理解。
                AnimalProxyProvider类代码如下 : (尤其注意匿名内部类中实现的invoke方法,其中的两条输出语句表示实现类对象相同的业务逻辑代码

package com.cyan.spring.dynamic_proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public class AnimalProxyProvider {
    //利用接口类型对传入的对象做接收 (多态)
    private Animal targetAnimal;

    //通过带参构造传入一个Animal接口实现类的对象
    public AnimalProxyProvider(Animal targetAnimal) {
        this.targetAnimal = targetAnimal;
    }

    //编写一个方法,用于返回代理对象 (用到反射机制)
    public Animal getAnimalProxy() {
        /**
         java.lang.reflect.Proxy类中的 newProxyInstance方法可以返回一个代理对象。
         public static Object newProxyInstance(ClassLoader loader,
             Class<?>[] interfaces,
             InvocationHandler h)
             throws IllegalArgumentException {...}

            该方法需要传入三个实参 ———
            (1) ClassLoader loader : 类加载器
            (2) Class<?>[] interfaces : 接口信息
            (3) InvocationHandler h : 调用处理器,其本身也是一个接口,内部声明了抽象方法invoke。
         */

        //(1)得到类加载器
        ClassLoader classLoader = targetAnimal.getClass().getClassLoader();

        //(2)得到被执行对象的接口信息(因为newProxyInstance方法底层是通过接口来调用的,即接口多态)
        Class<?>[] interfaces = targetAnimal.getClass().getInterfaces();

        //(3)得到处理器对象(通过匿名内部类实现,最终返回的是一个匿名内部类对象)
        //!!![注意,处理器对象本身也是newProxyInstance方法的一个形参]
        InvocationHandler invocationHandler = new InvocationHandler() {
            /**
             * @param proxy the proxy instance that the method was invoked on
             *
             * @param method the {@code Method} instance corresponding to
             * the interface method invoked on the proxy instance.  The declaring
             * class of the {@code Method} object will be the interface that
             * the method was declared in, which may be a superinterface of the
             * proxy interface that the proxy class inherits the method through.
             *
             * @param args an array of objects containing the values of the
             * arguments passed in the method invocation on the proxy instance,
             * or {@code null} if interface method takes no arguments.
             * Arguments of primitive types are wrapped in instances of the
             * appropriate primitive wrapper class, such as
             * {@code java.lang.Integer} or {@code java.lang.Boolean}.
             *
             * @return : the results of method instance invoked
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /*
                    将相同的业务逻辑代码,放在处理器对象的invoke对象中,
                    避免了代码下沉到每个实现类所造成的代码冗余。
                 */
                System.out.println("要吃饭的嘛~");

                //通过反射调用实现类中的方法
                Object results = method.invoke(targetAnimal, args);

                System.out.println("吃完了捏~");

                return results;
            }
        };

        Animal animalProxy = (Animal) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);

        return animalProxy;
    }
}

                接着,up定义一个测试类,在测试类中定义一个单元测试方法,用以测试我们的动态代理是否生效。
                TestAnimal类代码如下 : 

package com.cyan.spring.dynamic_proxy;

import org.junit.jupiter.api.Test;

public class TestAnimal {
    @Test
    public void testGeyAnimalProxy() {
        //构造接口多态
        Animal animal = new Cat();
        Animal animal2 = new Dog();

        //传入需要被代理的对象
        AnimalProxyProvider animalProxyProvider = new AnimalProxyProvider(animal);
        AnimalProxyProvider animalProxyProvider2 = new AnimalProxyProvider(animal2);

        //得到代理对象
        /*
            注意!
            此处代理对象animalProxy的编译类型仍然是Animal类型,但运行类型却不再是Cat or Dog类型,
            而是代理类型 ——— class com.sun.proxy.$Proxy9。
         */
        Animal animalProxy = animalProxyProvider.getAnimalProxy();
        Animal animalProxy2 = animalProxyProvider2.getAnimalProxy();
        System.out.println("animalProxy's RuntimeType = " + animalProxy.getClass());
        System.out.println("animalProxy2's RuntimeType = " + animalProxy2.getClass());

        //通过代理对象调用实现类方法
        animalProxy.eat();
        System.out.println("==============================");
        animalProxy2.eat();
    }
}

                运行结果 : 

                现在我们进行Debug断点调试,断点如下图所示 : 

                跳入eat()方法时会发现,IDEA直接跳到了AnimalProxyProvider类的getAnimalProxy方法中——匿名内部类实现的invoke方法里面,如下图所示 : 

                再往下执行便是通过反射调用对应的method,一直往下追,最终会跳到实现类的eat()方法中,如下图所示 : 

        3.动态代理的灵活性 : 

                动态代理的灵活性体现在哪里?
                首先,被代理的对象是可变的。并且,代理对象所调用的方法也是可变的

                比方说,up在Animal接口中新定义了一个sleep方法,如下图所示 : 

                然后,up在Cat类中实现了sleep方法,如下图所示 : 

                接着,修改匿名内部类实现的invoke方法中的“业务逻辑”代码,如下图所示 : 

                最后,up在测试类中新定义一个单元测试方法,测试动态代理是否生效。
                testSleep()方法代码如下 : 

    @Test
    public void testSleep() {
        //1.构造接口多态
        Animal animal = new Cat();

        //2.传入需要被代理的对象
        AnimalProxyProvider animalProxyProvider = new AnimalProxyProvider(animal);

        //3.获取代理对象
        Animal animalProxy = animalProxyProvider.getAnimalProxy();

        //4.通过代理对象调用实现类方法
        String sleepMinutes = animalProxy.sleep(Long.valueOf(1314));
    }

                运行结果 : 


三、深入动态代理

        1.需求 : 

        定义Calculator接口,表示一个计算器,该接口中定义有可以完成简单加减乘除运算的方法,要求在每次执行运算方法前后,都打印出运算日志(运算法则和运算参数,以及运算结果)

        2.实现 : 

            2.1 接口和实现类

                首先分析需求,既然要求在每次执行运算方法前后都打印出运算日志,显然我们会想到——仍是在匿名内部类实现的invoke方法中动手脚。
                别的不说,先来定义Calculator接口和一个它的实现类
                Calculator接口如下 : (声明了“加减乘除”四个抽象方法)

package com.cyan.spring.dynamic_proxy;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public interface Calculator {
    public abstract double add(double n1, double n2);
    public abstract double subtract(double n1, double n2);
    public abstract double multiply(double n1, double n2);
    public abstract double divide(double n1, double n2);
}

                定义一个实现类Calculator_Demo1,代码如下 : 

package com.cyan.spring.dynamic_proxy;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public class Calculator_Demo1 implements Calculator {
    @Override
    public double add(double n1, double n2) {
        return n1 + n2;
    }

    @Override
    public double subtract(double n1, double n2) {
        return n1 - n2;
    }

    @Override
    public double multiply(double n1, double n2) {
        return n1 * n2;
    }

    @Override
    public double divide(double n1, double n2) {
        //分母不允许为0
        if (n2 != 0) {
            return n1 / n2;
        }
        return -1;
    }
}

            2.2 提供代理对象的类

                定义一个CalculatorProxyProvider类,与上文 “动态代理使用案例” 中定义的“提供代理对象的类”类似,都需要定义一个方法用于返回代理对象。
                CalculatorProxyProvider类代码如下 : 

package com.cyan.spring.dynamic_proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.LocalDateTime;

public class CalculatorProxyProvider {
    private Calculator calculator;

    public CalculatorProxyProvider(Calculator calculator) {
        this.calculator = calculator;
    }

    /*
        底层仍然使用java.lang.reflect包下的Proxy类的newProxyInstance方法来获取代理对象。
     */
    public Calculator getCalculatorProxy() {
        //1.获取newProxyInstance方法的第一个参数————类加载器
        ClassLoader classLoader = calculator.getClass().getClassLoader();

        //2.获取newProxyInstance方法的第二个参数————接口信息
        Class<?>[] interfaces = calculator.getClass().getInterfaces();

        //3.获取newProxyInstance方法的第三个参数————处理器对象
            //仍然借助匿名内部类来实现,并通过构造接口多态的形式做接收。
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //获取到当前传入的参数
                double n1 = (double) args[0];
                double n2 = (double) args[1];

                //获取当前方法名
                String name = method.getName();

                //定义运算结果
                double result = 0.0;

                try {
                    //Δ在运算方法执行前打印出运算日志
                    System.out.println("运算日志————运算法则 = " + name + ",传入两个参数分别是 " + n1 + " 和 " + n2);

                    //执行运算
                    result = (double) method.invoke(calculator, args);
                    System.out.println("result = " + result);

                    //Δ在运算方法执行后打印出运算日志
                    System.out.println("运算日志————运算法则 = " + name + ",运算结果 = " + result);

                    //返回运算结果
                    return result;
                } catch (Exception e) {
                    System.out.println("异常日志————" + LocalDateTime.now() + ",方法" + method.getName() + "执行异常");
                    throw new RuntimeException(e);
                } finally {
                    System.out.println("执行日志————" + method.getName() + "方法执行结束。");
                }
            }
        };

        //4.调用newProxyInstance方法,得到代理对象
        Calculator instance = (Calculator) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);

        //5.返回获得的代理对象
        return instance;
    }
}

            2.3 测试类

                最后,仍然是在测试类中定义一个单元测试方法,up新定义了一个TestCalculator类,代码如下 : 

package com.cyan.spring.dynamic_proxy;

import org.junit.jupiter.api.Test;

public class TestCalculator {
    @Test
    public void testArithmetic() {
        //1.构造接口多态
        Calculator calculator = new Calculator_Demo1();

        //2.传入需要被代理的对象
        CalculatorProxyProvider calculatorProxyProvider = new CalculatorProxyProvider(calculator);

        //3.获取代理对象
        Calculator calculatorProxy = calculatorProxyProvider.getCalculatorProxy();

        //4.通过代理对象调用实现类方法
        double addResult = calculatorProxy.add(200.333, 33);
        System.out.println("-----------------------------");
        double subtractResult = calculatorProxy.subtract(141, 5);
        System.out.println("-----------------------------");
        double multiplyResult = calculatorProxy.multiply(11.11, 2);
        System.out.println("-----------------------------");
        double divideResult = calculatorProxy.divide(3917.0, 500.00);
    }
}

                运行结果 : 

                注意——
                (1) CalculatorProxyProvider类的这段代码,如下图所示 : 

                在AOP中,称为“横切关注点”,也叫“前置通知

                (2) 而下面的这段代码,如下图所示 : 

                从AOP的角度来看,也称为一个“横切关注点”,但也叫“后置通知

                (3) 此外,异常处理——catch语句块中的这段代码,如下图所示 : 

                从AOP看,也称为一个“横切关注点”,但又称为“异常通知”。

                (4) 最后,finally代码块中的内容,如下图所示 : 

                从AOP看,也称为一个“横切关注点”,但又称为“最终通知

        3.引出AOP : 

                分析一下我们方才写得代码,如下图所示 : 

                可以看到,无论是“前置通知”,“后置通知”,还是“异常通知”,“最终通知”。我们都只是草草地用了一条输出语句敷衍过去,这使得我们的代码不够牛逼,功能不够强大,且代码死板,不够灵活。
                而作为一名OOP程序员,我们会容易联想到——假如此处的输出语句都替换成方法,用一个方法直接切入,那不就既满足灵活性,又可以实现强大的功能吗?


四、总结

  • 🆗,以上就是Spring系列博文第四小节的全部内容了。
  • 总结一下,我们应该明白动态代理究竟“动态”在哪里?—— 不止是被代理对象可变,且代理对象执行的方法也是可变的。我们还需要知道newProxyInstance方法的三个形参分别有什么作用,以及如何获取这三个形参(尤其是第三个形参——处理器对象的获取,用到了匿名内部类)。总之,这篇博文其实是为了给“Spring—AOP”的分享做一个铺垫,我们以一个问题开始,又以一个问题结尾,也符合这篇博客的定位。
  • 下一节内容——Spring AOP—切入表达式和基于XML配置AOP,我们不见不散。感谢阅读!

        System.out.println("END---------------------------------------"); 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1342972.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习笔记|电动汽车上CAN报文解析

电动汽车&#xff0c;以电池和电机系统取代了内燃机汽车的发动机系统&#xff0c;使得汽车上主要的结构和电气件发生了很大变化。在传统汽车上已经比较成熟的CAN总线技术&#xff0c;电动汽车仍然需要作出必要调整才能够使用。 1 电动汽车的CAN协议 常用车辆CAN总线通讯协议&a…

【数据结构】排序之插入排序

排序目录 1.前言2. 排序的概念及其运用2.1 排序的概念2.2 排序的运用2.3 常见的排序算法 3. 插入排序3.1 基本思想3.2 直接插入排序3.2.1 直接插入排序实现3.2.1.1 分析3.2.1.2 代码实现 3.3 希尔排序3.3.1 希尔排序实现3.3.1.1 分析3.3.1.2 代码实现 4. 附代码4.1 sort.h4.2 s…

【第七在线】为什么智能商品管理是对传统商品管理模式的颠覆?

智能商品管理确实在一定程度上颠覆了传统的商品管理模式。传统商品管理通常依赖于人工的经验和决策&#xff0c;而智能商品管理则利用人工智能和相关技术来提供更智能化和高效化的解决方案。 智能商品管理通过数据分析和预测能力&#xff0c;可以更准确地预测市场需求和销售趋…

AI智能分析网关V4区域人数超员算法模型的应用原理及使用场景

视频AI智能分析技术已经深入到人类生活的各个角落&#xff0c;与社会发展的方方面面紧密相连。从日常生活中的各种场景&#xff0c;如人脸识别、车牌识别&#xff0c;到工业生产中的安全监控&#xff0c;如工厂园区的翻越围栏识别、入侵识别、工地的安全帽识别、车间流水线产品…

医院绩效考核系统源码,java源码,商业级医院绩效核算系统源码

医院绩效定义&#xff1a; “医院工作量绩效方案”是一套以工作量&#xff08;RBRVS&#xff0c;相对价值比率&#xff09;为核算基础&#xff0c;以工作岗位、技术含量、风险程度、服务数量等业绩为主要依据&#xff0c;以工作效率和效益、工作质量、患者满意度等指标为综合考…

D9741 PWM控制器电路,定时闩锁、短路保护电路,输出基准电压(2.5V) 采用SOP16封装

D9741是一块脉宽调制方三用于也收路像机和笔记本电的等设备上的直流转换器。在便携式的仪器设备上。 主要特点&#xff1a;● 高精度基准电路 ● 定时闩锁、短路保护电路 ● 低电压输入时误操作保护电路 ● 输出基准电…

MAC运行Windows专用软件 CrossOver v23.7.1中文版 macOS

CrossOver v23.7.1中文版是一款系统兼容软件&#xff0c;让您可以在 Mac 和 Linux 系统上运行 Windows 应用&#xff0c;不必购买 Windows 授权&#xff0c;不必重启系统&#xff0c;不必使用虚拟机。通过 CrossOver&#xff0c; 您可以从 dock 直接启动 Windows 应用&#xff…

WEB 3D技术 three.js 色彩空间讲解

上文 WEB 3D技术 three.js 设置环境贴图 高光贴图 场景设置 光照贴图 我们讲了基础材质的各种纹理 但是 我们的图片 到了界面场景中 好像绿的程度有点不太一样了 这里的话 涉及到我们的色彩空间 他有两种 一种是线性的 一种是 sRGB类型的 线性呢 就是根据光照强度 去均匀分…

nodejs+vue+ElementUi农产品团购销售系统zto2c

目标是为了完成小区团购平台的设计和实现&#xff0c;在疫情当下的环境&#xff0c;方便小区业主购入生活所需&#xff0c;减小居民的生活压力 采用B/S模式架构系统&#xff0c;开发简单&#xff0c;只需要连接网络即可登录本系统&#xff0c;不需要安装任何客户端。开发工具采…

OrientDB使用教程:全面了解图数据库

图数据库在当今数据处理领域中扮演着越来越重要的角色&#xff0c;而OrientDB作为一种多模型的数据库&#xff0c;具有图数据库、文档数据库和对象数据库的特性&#xff0c;为应对不同场景提供了灵活的解决方案。本教程将简要介绍OrientDB的使用&#xff0c;包括基本概念、安装…

3-链表-删除链表的倒数第 N 个结点

这是链表的第三篇算法&#xff0c;力扣链接。 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5]示例 2&#xff1a; 输入&#xff1a;head [1],…

为什么TCP会粘包

硬核图解|tcp为什么会粘包&#xff1f;背后的原因让人暖心 数据包报文格式&#xff08;IP包、TCP报头、UDP报头&#xff09; TCP&#xff0c;Transmission Control Protocol。传输控制协议&#xff0c;是一种面向连接的、可靠的、基于字节流的传输层通信协议。 TCP粘包是指发…

vue2中使用百度地图BMapGL

1、npm 命令安装 npm install vue-bmap-gl --save2、main.js 中文件引入 import VueBMap from vue-bmap-gl import vue-bmap-gl/dist/style.css VueBMap.initBMapApiLoader({// 百度的keyak:*********,// 这个密钥请使用自己注册的 }) Vue.use(VueBMap)3、页面调用 <temp…

一篇文章带你入门PHP魔术方法

PHP魔术方法 PHP 中的"魔术方法"是一组特殊的方法&#xff0c;它们在特定情况下自动被调用。这些方法的名称都是以两个下划线&#xff08;__&#xff09;开头。魔术方法提供了一种方式来执行各种高级编程技巧&#xff0c;使得对象的行为可以更加灵活和强大。以下是一…

SourceTree的安装和使用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、安装&#xff1a;二、使用步骤1.获取地址2.放入sourceTree 3.点击推送 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 简单讲解一…

F12开发者工具如何找到对应接口

Web问题定位 1、进入 NetWork页面2、点击Fetch/XHR&#xff0c;这里可以看到页面发起的接口3、找到出问题的接口4、NetWork页面怎么看接口详情5、问题定位 最常用的定位前后端问题的方法。即&#xff1a;一般用来查看是后端返回给前端的数据有误&#xff0c;还是前端显示有误。…

【27.5K⭐】Spacedrive:功能强大且便捷的跨平台文件管理器

【27.5K⭐】Spacedrive&#xff1a;功能强大且便捷的跨平台文件管理器 在日常生活和工作中&#xff0c;我们经常需要使用不同的设备和云存储服务来存储和管理我们的文件。然而&#xff0c;这样做往往会导致文件的分散、重复和混乱&#xff0c;给我们带来不便和困扰。那么&…

免费在线客服软件推荐:经济实用的客户沟通解决方案

好用的在线客服软件是企业是必不可少的工具&#xff0c;他让企业流程更流畅高效&#xff0c;让客户服务更完善优质。市场上的在线客服软件有很多&#xff0c;说着免费使用的软件也不在少数。今天小编就来推荐一款免费在线客服软件。 不过&#xff0c;我们选择免费在线客服软件…

如何让python在手机上运行,python程序在手机上运行

大家好&#xff0c;给大家分享一下python怎么在手机上运行爱心代码&#xff0c;很多人还不知道这一点。下面详细解释一下。现在让我们来看看&#xff01; 1. 写在前面的话 天天都在PC端运行Python代码的我&#xff0c;今天突然灵光一现&#xff0c;想着是不是能够在移动端运行P…

R503S指纹识别模块的指令系统(一)

1.采集指纹图像 GetImage&#xff08;0x01&#xff09; 功能说明&#xff1a;探测手指&#xff0c;探测到后录入指纹图像存于 ImageBuffer&#xff0c;并返回录入成功确认码&#xff1b;若探测不到手指&#xff0c;直接返回无手指确认码(模块对于每一条指令都快速反应&#xf…