设计模式系列 — 代理模式

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

前言

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

模式定义

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

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

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

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

  • 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
  • 动态:在程序运行时,运用反射机制动态创建而成

解决的问题

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

模式组成

组成(角色) 作用
抽象主题(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 的动态代理使用简单,它内置在 JDK 中,因此不需要引入第三方 Jar 包,但相对功能比较弱。
  • CGLIB 和 Javassist 都是高级的字节码生成库,总体性能比 JDK 自带的动态代理好,而且功能十分强大。
  • ASM 是低级的字节码生成工具,使用 ASM 已经近乎于在使用 Java bytecode 编程,对开发人员要求最高,当然,也是性能最好的一种动态代理生成工具。但 ASM 的使用很繁琐,而且性能也没有数量级的提升,与 CGLIB 等高级字节码生成工具相比,ASM 程序的维护性较差,如果不是在对性能有苛刻要求的场合,还是推荐 CGLIB 或者 Javassist。

我们这里介绍两种非常常用的动态代理技术,面试时也会常常用到的技术: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动态代理,真实主题 必须实现的主题接口,如果真实主题 没有实现主图接口,或者没有主题接口,则不能生成代理对象

由于必须要有接口才能使用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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348