• 静态代理、动态代理概念及使用


    1. 为什么要用静态代理

    个人博客地址:
    http://xiaohe-blog.top

    在分层开发中,哪个层次对我们来说更加重要呢 ?Dao?Service?Controller?

    肯定是Service层了,不管做什么样的项目,其主要任务就是通过各个Service的分工合作来满足用户的需求。Dao专注于与数据库打交道,Controller负责处理前端传来的数据。Service负责连接二者完成业务。

    那么Service中包含了哪些代码 ?

    image-20220824182106660

    我们要在Service中写两种代码 :附加功能与核心功能。

    附加功能例如开启事务、记录一下这个操作进行的日期、机器的运行状态…

    核心功能才是我们完成业务的代码。

    附加功能的特点是什么呢?通用,例如事务无非就是开启、提交、回滚。假如附加业务有20行,一个addUser需要这么多附加业务,那还有dalateUser、getUser、UpdateUser…甚至其他的StudentService也需要这些代码,一个项目加起来怕是有上千行这样重复的代码。

    那我们能不能把它们抽取出来制作为一个类,让这个类代替Service的附加功能,需要的时候直接调用呢?

    这就是代理,将附加业务抽取出来,让其他的类完成,需要的时候直接调用即可。

    这里有两个名词 :代理类、原始类。

    代理类 :被抽取出来的附加功能。

    原始类 :包含核心业务的类。


    如果你还没有搞懂,那我们来看看代理模式典型案例 :租房 。在早期租房过程中,有两个角色 :租户、房东。

    租户要去看租房信息、联系房东、看房、付钱。

    房东需要去发布房子广告、带领客户看房、收钱。

    按照一切皆对象的原则,我们可以将二者抽象为两个类

    image-20220824185421107

    设想一下你是房东,你的主要目的是什么 ?收钱啊,打广告、带人看房多累的事啊我tm一年啥也不干了就在外面打广告呢,老子只想拿钱,拿钱就是我的核心业务,其他我都不想干。

    这时候另一个行业兴起了 :中介。你不想打广告?我帮你打!你不想带客户看房?我帮你带!虽然我没房,但是我愿意花这个时间挣这个钱啊。于是房东将租房的事交给中介…

    image-20220824190825807

    中介去打广告、带租户看房,等到满意了直接让房东收钱就好。

    注意这里并不是我们想的房东调用中介的附加功能,而是全权交给中介,中介调用房东的核心功能。

    中介就是代理类,房东是原始类。

    于是我们就能总结静态代理的概念与好处 :

    1. 概念: 通过代理类为原始类增加额外功能

    2. 好处: 利于原始类功能的扩展与维护

    2. 静态代理的实现

    并且中介的方法名也要叫“出租房屋”,不叫出租房屋叫啥?出租电动车?肯定要与房东想要的一样啊。各位可能想的是将房东设计为接口,让中介实现它。但是房东的核心功能要实现,那么房东这个接口中全部都是默认方法了,很不美观。我们让房东和中介实现同样的接口不就行了?

    在这里,中介是代理对象;房东依然是原始对象。

    所以我们先定义一个接口:

    public interface UserService {
        // 出租房屋
        public void rentHome();
    }
    
    • 1
    • 2
    • 3
    • 4

    让房东和中介都实现这个接口

    // 房东
    public class UserServiceImpl implements UserService{
        @Override
        public void rentHome() {
            System.out.println("收钱...");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    中介完成自己的核心功能,它的核心功能就是给房东添加附加功能。

    // 中介
    public class UserServiceProxy implements UserService{
        private UserServiceImpl fangDong = new UserServiceImpl();
        @Override
        public void rentHome() {
            System.out.println("打广告...");
            System.out.println("带客户看房...");
            System.out.println("签合同...");
            // 调用核心业务
            fangDong.rentHome();
           
            System.out.println("给房客提供后续业务..");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    以后,我们的房东可以专注于核心业务:“收钱” 的完成,无需关心其他。

    这样,我们就完成了静态代理模式的开发。

    3. 静态代理的缺点

    虽然刚学静态代理,但是可以看出静态代理的缺点 :

    一个房东租房需要编写一个代理类 ,那如果还有什么租车、租游戏号都需要中介…那么我们是不是要写很多代理类?

    马上一个项目全是代理类得了。那我们能不能让别人替我们创建代理类呢?可以使用动态代理来解决这个问题。

    动态代理很重要!!!

    4. 动态代理

    动态代理解决了静态代理的“代理类繁杂”问题,但也付出了代价 :编写难度高。希望大家可以坚持。

    动态代理分为两种:

    1. JDK动态代理

    2. Cglib动态代理

    他们的区别会在下文介绍。

    4.1 JDK动态代理

    JDK为我们提供了一个接口完成动态代理的开发 :Proxy,它有一个静态方法 newProxyInstance()

    这个方法的参数有三个,初学者很容易被劝退,这将是我们学习的重点,也是难点。笔者将会将他们分为三个小模块分别讲解。(CGLIB代理也是这几个,现在学了,等会就不讲了)

    Proxy.newInstance(
        ClassLoader loader, // 类加载器
        Class<?>[] interfaces, // 接口Class数组
        InvocationHandler h // 某个神秘的接口
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20220909202520167

    4.1.1 InvocationHandler

    我们实现动态代理的主要目的是 “实现附加业务”,所以先来看看在哪里编写附加业务。

    InvocationHandler是一个接口,我们需要实现它并将实现类传进去。它只有一个方法 :invoke

    public interface InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) 
            throws Throwable;
    }
    
    • 1
    • 2
    • 3
    • 4

    参数 :

    Object proxy

    核心业务对象。你为谁添加代附加功能?房东?那proxy就是房东对象。值得一提的是,这个proxy是代理后的对象而不是原始对象。

    Method method

    核心业务。你为哪一个方法添加额外功能 ?租房?那method就是租房方法.

    可以通过method.invoke(原始对象的实例, args) 调用它。

    Object[] args

    核心业务的参数。

    返回值:

    Object :核心业务的返回值。附加业务要与核心业务的返回值相同,今天你为房东代理,返回值为钱,明天你为宠物代理,返回的是动物…那么我们怎么知道核心业务要返回什么呢?刚才看到参数中的 Method代表核心业务,那我们是否可以执行它,获取它的返回值!本来核心业务就需要执行,我不仅执行了,还获取返回值返回,一举两得!

    于是InvocationHandler可以这样写 :

    public MyInvocationHandler implements InvocationHandler {
        private UserService = new UserServiceImpl();
        public Object invoke(Object proxy, Method method, Object[] args) 
            throws Throwable {
            System.out.println("打广告...");
            System.out.println("带客户看房...");
            System.out.println("签合同...");
            // 调用核心业务,返回值保留,以后返回。
            Object ret = method.invoke(userService, args);
           
            System.out.println("给房客提供后续业务..");
            return ret;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (当然用匿名内部类也可以,下面的完整编码为了减少代码量就是用了匿名内部类)

    Proxy.newProxyInstance(
        x, 
        x, 
        new MyInvocationHandler());
    
    • 1
    • 2
    • 3
    • 4

    4.1.2 Class

    在静态代理的学习中我们说过 :代理类和原始类要实现相同的接口。并且 Class 这个参数的参数名也是 interfaces,所以这个参数就可以是 userService.getClass().getInterfaces()。

    userService.getClass().getInterfaces()
    
    • 1
    Proxy.newProxyInstance(
        x, 
        userService.getClass().getInterfaces();
        new MyInvocationHandler());
    
    • 1
    • 2
    • 3
    • 4

    4.1.3 ClassLoader

    学过反射应该知道这个东西,它叫类加载器,为什么需要类加载器?不管是核心业务类的读取还是动态代理类的创建,都需要用到类加载器,相比于上面的那个类,这个就很简单了。

    我们可能不知道类加载器分为几种,也不知道类加载器如何单独创建,但是我们可以借助别人的。

    // 用谁的都一样,只要不是JDK内置类的类加载器就行。
    // 可以是
    userService.getClass().getClassLoader();
    
    • 1
    • 2
    • 3
    Proxy.newProxyInstance(
        userService.getClass(), 
        userService.getClass().getInterfaces(),
        new MyInvocationHandler());
    
    • 1
    • 2
    • 3
    • 4

    4.2 JDK动态代理编码

    学习Proxy.newProxyInstance()的三个参数后,我们就可以完成JDK动态代理的代码编写了。

    动态代理编程分为三步 :

    1. 创建原始对象
    2. 完成 InvocationHandler 代理
    3. 调用 Proxy.newProxyInstance
    @Test
    public void test() {
        //1. 创建原始对象
        UserService userService = new UserServiceImpl();
        //2. 匿名内部类创建 InvocationHandler
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("打广告...");
                System.out.println("带客户看房...");
                System.out.println("签合同...");
                // 调用核心业务,返回值保留,以后返回。
                Object ret = method.invoke(userService, args);
    
                System.out.println("给房客提供后续业务..");
                return ret;
            }
        };
    	//3. 调用 Proxy.newProxyInstance
        UserService proxyInstance = (UserService) 
            Proxy.newProxyInstance(
            	userService.getClass().getClassLoader(),
            	userService.getClass().getInterfaces(),
            	new MyInvocationHandler()
        	)
        proxyInstance.rentHome();
    }
    
    • 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

    4.3 JDK动态代理编码注意事项

    刚才我们在完成InvocationHandler 类时这样写 :

    image-20220909213952811

    在完成另外两个参数时也是用了userService.getClass() 而没用使用 UserService.class来获取类实例,为什么呢?

    image-20220909214224436

    我们可以先试试能不能使用 :

    image-20220909214632020

    果不其然,报错!那么为什么呢?

    对于为什么不能使用proxy直接传参,刚才在介绍时已经说了,invoke需要的是原始方法,proxy是代理后对象。

    那么我们将proxy换成 userService行不行呢?

    image-20220909215755490

    可以看到依旧不行,那么问题一定在 UserService.classuserService.getClass() 的区别。

    在学习反射我们学了获取Class实例的方法有 .class、。getClass(),也学了他俩的区别 :

    .class是编译时Class实例,它不受多态影响。

    .getClass是运行时Class实例,说白了就是多态实例。

    大白话:

    对于:
    UserService userService = new UserServiceImpl();
    
    • 1
    • 2

    UserService.class获取的是 UserService 的Class实例。

    userService.getClass() 获取的是 UserServiceImpl 的Class实例

    我们可以进行验证 :

    UserService userService1 = new UserServiceImpl();
    UserServiceImpl userService2 = new UserServiceImpl();
    System.out.println(userService1.getClass() == UserService.class);
    System.out.println(userService2.getClass() == UserService.class);
    System.out.println(userService1.getClass() == userService2.getClass());
    // 无奖竞答:这个答案是什么?
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    UserService.class 获取的是 UserService 的Class

    userService1 获取的是 UserServiceImpl 的Class

    userService2 获取的是 UserServiceImpl 的Class

    所以答案是 false、false、true。

    所以在完成动态代理编码时一定要注意使用 原始对象的Class实例,而不是类的Class实例。

    4.4 Cglib动态代理

    学习JDK动态代理后,Cglib就容易一点了。

    JDK动态代理和Cglib动态代理的区别,先别说底层原理,就我们“肉眼可见”可见的最大区别,就是 :

    JDK动态代理基于接口;Cglib动态代理基于继承

    Cglib动态代理需要代理类继承原始类

    Cglib提供了一个类 :Enhancer,这个类有一个create()方法生成动态代理类。

    Enhancer不是接口,所以直接new Enhancer之后将它的成员变量赋值,例如要指定父类是谁、类加载器用啥样的…

    所以在调用create()方法之前,我们需要指定几个参数 :

    1、类加载器 setClassLoader();

    2、父类Class实例 setSuperclass();

    3、MethodInterceptor 实现类 setCallback();

    前两个类已经不用讲了,现在就是一个新的类 :MethodInterceptor 接口。

    (它与Springaop中的MethodInterceptor可不是同一个包下的啊,不要搞混)

    接下来学习一下它。

    4.4.1 MethodInterceptor

    public interface MethodInterceptor extends Callback {
    	Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) 
            throws Throwable;
    }
    
    • 1
    • 2
    • 3
    • 4

    参数 :

    (它跟InvocationHandler有三个一样的参数,就不再赘述,最后一个参数用不到,提一下。)

    Object o

    代理后的对象。

    Method method

    核心业务。

    Object[] args

    核心业务的参数。

    MethodProxy methodProxy

    生成的代理类对方法的代理引用

    返回值:

    Object

    这个不用说了吧,跟上面JDK动态代理InvocationHandler.invoke()的返回值一样,都代表核心业务的返回值。

    于是 MethodInterceptor可以这样写 :

    public MyMethodInterceptor implements MethodInterceptor {
        private UserService = new UserServiceImpl();
        Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) 
            throws Throwable {
            System.out.println("打广告...");
            System.out.println("带客户看房...");
            System.out.println("签合同...");
            // 调用核心业务,返回值保留,以后返回。
            Object ret = method.invoke(userService, args);
           
            System.out.println("给房客提供后续业务..");
            return ret;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (当然用匿名内部类也可以,下面的完整编码为了减少代码量就是用了匿名内部类)

    4.5 Cglib动态代理编码

    原始对象为父,代理类为子。

    // 原始对象
    public class UserServiceImpl {
        public void rendHome() {
            System.out.println("收钱...");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    完整代码如下 :

    @Test
    public void testCglib() {
        UserServiceImpl userService = new UserServiceImpl();
        MethodInterceptor methodInterceptor = new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("打广告...");
                System.out.println("带客户看房...");
                System.out.println("签合同...");
                // 调用核心业务,返回值保留,以后返回。
                Object ret = method.invoke(userService, args);
    
                System.out.println("给房客提供后续业务..");
                return ret;
            }
        };
        Enhancer enhancer = new Enhancer();
        // 设置三个属性: 类加载器、父类Class实例、MethodInterceptor实例
        enhancer.setClassLoader(userService.getClass().getClassLoader());
        enhancer.setSuperclass(userService.getClass());
        enhancer.setCallback(methodInterceptor);
        // 获取代理类
        UserServiceImpl userServiceProxy = (UserServiceImpl) enhancer.create();
        userServiceProxy.rentHome();
    }
    
    • 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

    5. 对于两种方式的总结

    我画了一张流程图,希望对大家有帮助 :

    image-20220910004825959

    6. JDK动态代理与Cglib动态代理的区别

    1、使用技术不同

    • JDK动态代理原理是拦截器+反射机制,将原始类拦下进行包装。

    • CGLIB动态代理原理是动态字节码,通过修改字节码生成子类。

    2、受用对象不同

    • JDK动态代理是针对接口的代理技术
    • CGLIB动态代理针对继承的代理技术

    当有接口时使用JDK动态代理,没有接口只能继承时,使用CGLIB动态代理。

    不写了,累了,种地去了😒

  • 相关阅读:
    信息论随笔(三)交互信息量
    LocalDateTime ZonedDateTime Instant 的相互转换
    【vue基础】黑马vue视频笔记(五)
    2024年五大科技与创业趋势:从AI退热到IPO挑战
    Spring+Vue工程部署在Linux
    学生HTML网页作业:基于HTML+CSS+JavaScript画家企业8页
    APP自动化测试-6.断言处理assert与hamcrest
    图片怎么转换成PDF格式?这两种方法赶紧记下
    【前端】政务服务大数据可视化监控平台(源码+html+css+js)
    PromptDet: Towards Open-vocabulary Detection using Uncurated Images (ECCV2022)
  • 原文地址:https://blog.csdn.net/qq_62939743/article/details/126791985