设计模式系列 — 代理模式

2020-10-31  本文已影响0人  一角钱技术

点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章。

前言

23种设计模式快速记忆的请看上面第一篇,本篇和大家一起来学习代理模式相关内容。

模式定义

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问。

在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。

根据代理的创建时期,代理模式分为静态代理和动态代理。

解决的问题

在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

模式组成

组成(角色) 作用
抽象主题(Subject)类 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
真实主题(Real Subject)类 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
代理(Proxy)类 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

静态代理

使用步骤

步骤1:定义抽象主题类

//抽象主题
interface Subject {
    void request();
}

步骤2:定义真实主题类

//真实主题
class RealSubject implements Subject {
    public void request() {
        System.out.println("访问真实主题方法...");
    }
}

步骤3:定义代理类

//代理
class Proxy implements Subject {
    private RealSubject realSubject;

    public void request() {
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        preRequest();
        realSubject.request();
        postRequest();
    }

    public void preRequest() {
        System.out.println("访问真实主题之前的预处理。");
    }

    public void postRequest() {
        System.out.println("访问真实主题之后的后续处理。");
    }
}

步骤4:测试

public class ProxyPattern {

    public static void main(String[] args) {
        Proxy proxy = new Proxy();
        proxy.request();
    }
}

输出结果如下

访问真实主题之前的预处理。
访问真实主题方法...
访问真实主题之后的后续处理。

可以看到,主题接口是Subject,真实主题是RealSubject 实现了Subject接口,代理类是Proxy,在代理类的方法里实现了Subject类,并且在代码里写死了代理前后的操作,这就是静态代理的简单实现,可以看到静态代理的实现优缺点十分明显。

静态代理优点

使得真实主题处理的业务更加纯粹,不再去关注一些公共的事情,公共的业务由代理来完成,实现业务的分工,公共业务发生扩展时变得更加集中和方便。

静态代理缺点

这种实现方式很直观也很简单,但其缺点是代理类必须提前写好,如果主题接口发生了变化,代理类的代码也要随着变化,有着高昂的维护成本。

针对静态代理的缺点,是否有一种方式弥补?能够不需要为每一个接口写上一个代理方法,那就动态代理。

动态代理

动态代理,在java代码里动态代理类使用字节码动态生成加载技术,在运行时生成加载类。

生成动态代理类的方法很多,比如:JDK 自带的动态处理、CGLIB、Javassist、ASM 库。

我们这里介绍两种非常常用的动态代理技术,面试时也会常常用到的技术:JDK 自带的动态处理CGLIB 两种。

JDK动态代理

Java提供了一个Proxy类,使用Proxy类的newInstance方法可以生成某个对象的代理对象,该方法需要三个参数:

  1. 类装载器【一般我们使用的是被代理类的装载器】
  2. 指定接口【指定要被代理类的接口】
  3. 代理对象的方法里干什么事【实现handler接口】

使用步骤

步骤1:定义抽象主题类

//抽象主题
interface Subject {
    void request();
}

步骤2:定义真实主题类

//真实主题
class RealSubject implements Subject {
    public void request() {
        System.out.println("访问真实主题方法...");
    }
}

步骤3:使用Proxy.newProxyInstance生成代理对象

class ProxyHandler implements InvocationHandler {

    private Subject subject; // 定义主题接口

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 如果是第一次调用,生成真实主题
        if (subject == null) {
            subject = new RealSubject();
        }

        if ("request".equalsIgnoreCase(method.getName())) {
            System.out.println("访问真实主题之前的预处理。");
            Object result = method.invoke(subject, args);
            System.out.println("访问真实主题之后的后续处理。");
            return result;
        } else {
            // 如果不是调用request方法,返回真实主题完成实际操作
            return method.invoke(subject, args);
        }
    }

    // 使用Proxy.newProxyInstance生成代理对象
    static Subject createProxy() {
        Subject proxy = (Subject) Proxy.newProxyInstance(
                ClassLoader.getSystemClassLoader(), // 当前类的类加载器
                new Class[]{Subject.class}, //被代理的主题接口
                new ProxyHandler() // 代理对象,这里是当前对象
        );
        return proxy;
    }
}

步骤4:测试输出

public class ProxyPattern {
    public static void main(String[] args) {
        Subject subject = ProxyHandler.createProxy();
        subject.request();
    }
}

输出结果如下

访问真实主题之前的预处理。
访问真实主题方法...
访问真实主题之后的后续处理。

用debug的方式启动,可以看到方法被代理到代理类中实现,在代理类中执行真实主题的方法前后可以进行很多操作。

虽然这种方法实现看起来很方便,但是细心的同学应该也已经观察到了,JDK动态代理技术的实现是必须要一个接口才行的,所以JDK动态代理的优缺点也非常明显

JDK动态代理优点

JDK动态代理缺点

由于必须要有接口才能使用JDK的动态代理,那是否有一种方式可以没有接口只有真实主题实现类也可以使用动态代理呢?这就是第二种动态代理:CGLIB

CGLIB动态代理

使用 CGLIB 生成动态代理,首先需要生成 Enhancer 类实例,并指定用于处理代理业务的回调类。在 Enhancer.create() 方法中,会使用 DefaultGeneratorStrategy.Generate() 方法生成动态代理类的字节码,并保存在 byte 数组中。接着使用 ReflectUtils.defineClass() 方法,通过反射,调用 ClassLoader.defineClass() 方法,将字节码装载到 ClassLoader 中,完成类的加载。最后使用 ReflectUtils.newInstance() 方法,通过反射,生成动态类的实例,并返回该实例。基本流程是根据指定的回调类生成 Class 字节码—通过 defineClass() 将字节码定义为类—使用反射机制生成该类的实例。

使用步骤

步骤1:定义真实主题

class WorkImpl {

    void addWorkExperience() {
        System.out.println("增加工作经验的普通方法...");
    }
}

步骤2:创建代理类

class WorkImplProxyLib implements MethodInterceptor {

    // 创建代理对象
    Object getWorkProxyImplInstance() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(WorkImpl.class);
        // 回调方法
        enhancer.setCallback(this);
        // 创建代理对象
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("开始...");
        methodProxy.invokeSuper(obj, args);
        System.out.println("结束...");
        return null;
    }
}

步骤3:测试输出

public class CglibProxy {

    public static void main(String[] args) {
        WorkImplProxyLib cglib = new WorkImplProxyLib();
        WorkImpl workCglib = (WorkImpl) cglib.getWorkProxyImplInstance();
        workCglib.addWorkExperience();
    }
}

输出结果如下

开始...
增加工作经验的普通方法...
结束...

CGLIB动态代理优点

CGLIB通过继承的方式进行代理、无论目标对象没有没实现接口都可以代理,弥补了JDK动态代理的缺陷。

CGLIB动态代理缺点

  1. CGLib创建的动态代理对象性能比JDK创建的动态代理对象的性能高不少,但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。
  2. 由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。

应用场景

当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。

代理模式有多种应用场合,如下所述:

  1. 远程代理,也就是为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。比如说 WebService,当我们在应用程序的项目中加入一个 Web 引用,引用一个 WebService,此时会在项目中声称一个 WebReference 的文件夹和一些文件,这个就是起代理作用的,这样可以让那个客户端程序调用代理解决远程访问的问题;
  2. 虚拟代理,是根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象。这样就可以达到性能的最优化,比如打开一个网页,这个网页里面包含了大量的文字和图片,但我们可以很快看到文字,但是图片却是一张一张地下载后才能看到,那些未打开的图片框,就是通过虚拟代里来替换了真实的图片,此时代理存储了真实图片的路径和尺寸;
  3. 安全代理,用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候;
  4. 指针引用,是指当调用真实的对象时,代理处理另外一些事。比如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它,或当第一次引用一个持久对象时,将它装入内存,或是在访问一个实际对象前,检查是否已经释放它,以确保其他对象不能改变它。这些都是通过代理在访问一个对象时附加一些内务处理;
  5. 延迟加载,用代理模式实现延迟加载的一个经典应用就在 Hibernate 框架里面。当 Hibernate 加载实体 bean 时,并不会一次性将数据库所有的数据都装载。默认情况下,它会采取延迟加载的机制,以提高系统的性能。Hibernate 中的延迟加载主要分为属性的延迟加载和关联表的延时加载两类。实现原理是使用代理拦截原有的 getter 方法,在真正使用对象数据时才去数据库或者其他第三方组件加载实际的数据,从而提升系统性能。

其它代理模式

PS:以上代码提交在 Githubhttps://github.com/Niuh-Study/niuh-designpatterns.git

文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。

上一篇下一篇

猜你喜欢

热点阅读