• 设计模式之代理模式


    概述

    • 什么是代理模式?
    • 解决什么问题(即为什么需要)?
    • 什么是静态代理?
    • 什么是动态代理模式?二者什么关系?
    • 具体如何实现?

    代理模式

    先看百度百科的定义:

    为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

    定义总是抽象而晦涩难懂的,我们通过一个例子简单说明下。

    假设我们想邀请一位明星,那么并不是直接连接明星,而是联系明星的经纪人,来达到同样的目的。明星就是一个「目标对象」,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决。这就是代理思想在现实中的一个例子。

    用图表示如下:

    在这里插入图片描述

    简单来说,代理模式提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

    简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。

    通过代理模式,我们可以做到两点:

    • 隐藏委托类的具体实现。
    • 实现客户与委托类(目标对象)的解耦,在不改变委托类代码的情况下添加一些额外的功能(日志、权限)等。

    代理模式结构

    在这里插入图片描述

    代理模式的主要角色如下:

    • Subject(抽象主题角色):定义 RealSubject 和 Proxy 角色都应该实现的接口。
    • RealSubject(真实主题角色):真实类,也就是被代理类、委托类,用来真正完成业务服务功能。
    • Proxy(代理类):用来代理和封装真实主题,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

    代码实现

    代理模式有三种类型:

    1. 静态代理
    2. 动态代理:
      1. JDK 代理,接口代理
      2. CGLIB 代理,在内存中动态的创建目标对象的子类

    静态代理

    静态代理是指代理类在程序运行前就已经存在,这种情况下的代理类通常都是我们在 Java 代码中定义的。在程序运行之前,代理类的 .class 文件就已经生成。

    静态代理需要先定义接口,被代理对象与代理对象一起实现相同的接口,然后通过调用相同的方法来调用目标对象的方法。

    在这里插入图片描述

    可以看见,代理类无非是在调用委托类方法的前后增加了一些操作。委托类的不同,也就导致代理类的不同。

    静态代理简单实现

    举例:保存用户功能的静态代理实现

    • 接口类:
    public interface IUserDao {
        void save();
    }
    
    • 1
    • 2
    • 3
    • 目标对象:
    public class UserDaoImpl implements IUserDao {
    
        @Override
        public void save() {
            System.out.println("保存用户信息");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 静态代理对象:UserDaoProxy 需要实现 IUserDao 接口!
    public class UserDaoProxy implements IUserDao {
    
        private IUserDao target;
    
        public UserDaoProxy(IUserDao target) {
            this.target = target;
        }
    
        @Override
        public void save() {
            //扩展了额外功能
            System.out.println("开启事务");
            target.save();
            System.out.println("提交事务");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 测试类:
    public class TestProxy {
        @Test
        public void testStaticProxy(){
            //目标对象
            IUserDao target = new UserDaoImpl();
    
            //代理对象
            UserDaoProxy proxy = new UserDaoProxy(target);
            proxy.save();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 输出结果:
    开启事务
    保存用户信息
    提交事务
    
    • 1
    • 2
    • 3

    静态代理总结:

    • 优点:可以在不修改目标对象的前提下扩展目标对象的功能。
    • 缺点:
      • 代码冗余,由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
      • 不易维护,一旦接口增加方法,目标对象与代理对象都要进行修改。

    如何解决静态代理中的问题呢?答案是可以使用动态代理方式。

    动态代理

    代理类在程序运行时创建的代理方式被成为动态代理。

    我们上面静态代理的例子中,代理类(UserDaoProxy)是自己定义好的,在程序运行之前就已经编译完成。然而动态代理,代理类并不是在代码中定义的,而是在运行时根据我们在代码中的“指示”动态生成的。

    相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。

    我们先来介绍 JDK 自带的动态代理,代理类需要实现一个 InvocationHandler 接口,并且调用 Proxy 类的静态方法生成一个动态代理类。

    JDK 动态代理

    还是上面保存用户信息的功能,我们用 jdk 动态代理实现。

    • 动态代理对象:
    public class UserDaoJdkProxy implements InvocationHandler {
        private Object target;
    
        public UserDaoJdkProxy(Object target) {
            this.target = target;
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("开启事务");
    
            // 执行目标对象方法
            Object returnValue = method.invoke(target, args);
    
            System.out.println("提交事务");
            return returnValue;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 测试类:
    public class TestProxy {
    
        @Test
        public void testJdkDynamicProxy() {
            //创建一个实例对象,这个对象是被代理的对象
            IUserDao target = new UserDaoImpl();
    
            //输出目标对象信息
            System.out.println(target.getClass());
            //定义一个handler
            InvocationHandler handler = new UserDaoJdkProxy(target);
    
            //获得类的class loader
            ClassLoader cl = target.getClass().getClassLoader();
    
            //动态产生一个代理者
            IUserDao proxy = (IUserDao) Proxy.newProxyInstance(cl, new Class[]{IUserDao.class}, handler);
    
            //输出代理对象信息
            System.out.println(proxy.getClass());
    
            //执行代理方法
            proxy.save();
        }
    }
    
    • 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
    • 输出结果:
    class com.atu.design.pattern.proxy.UserDaoImpl
    class com.sun.proxy.$Proxy4
    开启事务
    保存用户信息
    提交事务
    
    • 1
    • 2
    • 3
    • 4
    • 5

    JDK 动态代理的原理是将被代理的每个方法都交给 invoke 这个方法去处理,这个代理类可以代理任何类,而不再是具体的固定类了。

    在这里插入图片描述
    JDK 动态代理的特点如下:

    • 通过实现 InvocationHandler 接口完成代理逻辑。
    • 通过反射代理方法,比较消耗系统性能,但可以减少代理类的数量,使用更灵活。
    • 代理类必须实现接口。

    JDK 动态代理有一个最致命的问题是它只能代理实现了某个接口的实现类,并且代理类也只能代理接口中实现的方法,要是实现类中有自己私有的方法,而接口中没有的话,该方法不能进行代理调用。

    怎么解决这个问题呢?我们可以用 CGLIB 动态代理机制。

    CGLIB 动态代理

    静态代理和 JDK 代理都需要某个对象实现一个接口,有时候代理对象只是一个单独对象,此时可以使用 Cglib 代理。

    Cglib 代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展。

    简单来说,Cglib 继承被代理的类,覆写其业务方法来实现代理。因为采用继承机制,所以不能对 final 修饰的类进行代理。

    在这里插入图片描述
    使用 cglib 需要引入 cglib 的 jar 包,如果你已经有 spring-core 的 jar 包,则无需引入,因为 spring 中包含了cglib。

    • cglib 的 Maven 坐标
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.3.0</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    继续用上述保存用户的例子。

    • 代理对象:
    public class UserDaoCGProxy implements MethodInterceptor {
        /**
         * 维护一个目标对象
         */
        private Object target;
    
        public UserDaoCGProxy(Object target) {
            this.target = target;
        }
    
        /**
         * 为目标对象生成代理对象
         *
         * @return
         */
        public Object getProxyInstance() {
            //工具类
            Enhancer en = new Enhancer();
            //设置父类
            en.setSuperclass(target.getClass());
            //设置回调函数
            en.setCallback(this);
            //创建子类对象代理
            return en.create();
        }
    
        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("开启事务");
            // 执行目标对象的方法
            Object returnValue = method.invoke(target, args);
            System.out.println("关闭事务");
            return returnValue;
        }
    }
    
    • 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
    • 测试类:
    public class TestProxy {
    
        @Test
        public void testCglibProxy() {
            //目标对象
            UserDaoImpl target = new UserDaoImpl();
            System.out.println(target.getClass());
    
            //代理对象
            UserDaoImpl proxy = (UserDaoImpl) new UserDaoCGProxy(target).getProxyInstance();
            System.out.println(proxy.getClass());
    
            //执行代理对象方法
            proxy.save();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 输出结果:
    class com.atu.design.pattern.proxy.UserDaoImpl
    class com.atu.design.pattern.proxy.UserDaoImpl$$EnhancerByCGLIB$$4d1e7fa9
    开启事务
    保存用户信息
    关闭事务
    
    • 1
    • 2
    • 3
    • 4
    • 5

    总结

    • 静态代理实现较简单,只要代理对象对目标对象进行包装,即可实现增强功能,但静态代理只能为一个目标对象服务,如果目标对象过多,则会产生很多代理类。
    • 静态代理在编译时产生 class字节码文件,可以直接使用,效率高。
    • JDK 动态代理必须实现 InvocationHandler 接口,通过反射代理方法,比较消耗系统性能,但可以减少代理类的数量,使用更灵活。
    • cglib 代理无需实现接口,通过生成类字节码实现代理,比反射稍快,不存在性能问题,但 cglib 会继承目标对象,需要重写方法,所以目标对象不能为 final 类。
  • 相关阅读:
    keepalived 主备都存在vip, keepalived主备跨网段配置;keepalived主备服务器不在同一个网段怎么配置
    基础选择器汇总——标签选择器,类选择器、id选择器、通配符选择器
    java-性能排查工具
    分析系统变慢或卡死
    基于OneNet平台设计的多节点温度采集系统-有人云4G模块+STM32
    Flutter:构建美观应用的跨平台方案
    Java实现扫雷小游戏【优化版】
    Pod详解
    canal + rabbitmq监听mysql数据库变化
    【国科大——认知计算】认知计算 第一次研讨课
  • 原文地址:https://blog.csdn.net/D812359/article/details/125468463