设计模式-代理模式(四)

一、概述

  代理模式我们接触的就比较多了,所谓的代理模式就是,给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。比如,在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之前起到中介的作用。其实代理模式就两个字:中介

  1. 代理模式主要解决的问题:

直接访问对象相关的问题,比如无法直接访问对象,或者由于安全问题不建议直接访问的对象,亦或者是访问对象太过于复杂的问题;

  1. 为什么使用代理:

由于设计模式的开闭原则,不建议直接修改已有的代码,我们可以使用代理,在目标对象基础上合理的扩展功能;

  1. 代理模式的一些应用场景:
  1. 比如我最近要买ThinkPadT470p,国内网站上全是9千元左右,这时我可以通过海淘从国外买,可能会便宜不少,这其中的海淘就相当于代理的角色。
  2. 只要我们随便一想,生活中涉及到代理的实在太多,比如火车站通过黄牛买票,通过4S店买车,通过中介租房,外卖小哥送餐等等,都是代理模式的应用。
  1. 代理模式的分类:

根据代理对象生成的时期不同,代理模式可以大致分为静态代理和动态代理。其中JDK中的动态代理的实现是通过Java的反射来实现的。

代理模式角色

我们先来一张图看一下代理模式的角色:

Proxy.jpg

注:图片来源:图说设计模式-代理模式-角色图解
通过以上结构,我们可以大概了解到代理模式的几个角色:

  1. Subject:抽象主题角色,底层一般是接口实现,该接口定义了代理类和真实主题角色的公共的对外方法,是对象和它的代理共用的接口。
  2. RealSubject:真实主题角色,实现了抽象主题接口,实现了真正的业务逻辑;
  3. Proxy:代理角色,内部含有对真实对象RealSubject的引用,从而可以实现对真实对象的代理。代理对象提供与真实对象相同的接口,以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,以扩展相应的功能。
  4. Client:客户端的调用,不算代理模式的角色。

二、静态代理

静态代理是说由程序员手动编写或工具生成的代理类,在程序运行前就已经编译完成,这就是所谓的静态代理类。我们通过一个简单的保存数据的例子来看一下静态代理的实现,代码转载自:
Java三种代理模式:静态代理、动态代理和cglib代理,静态代理代码实现

  1. 底层接口:IUserDao,只有一个保存数据的接口。
public interface IUserDao {
    void save();
}
  1. 真实对象:UserDaoImpl,实现了接口中的save方法
public class UserDaoImpl implements IUserDao {
    @Override
    public void save() {
        System.out.println("保存数据");
    }
}
  1. 静态代理对象:UserDaoProxy,也实现了接口IUserDao
public class UserDaoProxy implements IUserDao {
    private IUserDao iUserDao;

    public UserDaoProxy(IUserDao iUserDao) {
        this.iUserDao = iUserDao;
    }

    @Override
    public void save() {
        iUserDao.save();
    }
}
  1. 测试类:Main
public class Main {
    public static void main(String[] args) {
        //目标对象
        IUserDao target = new UserDaoImpl();
        //代理对象
        UserDaoProxy proxy = new UserDaoProxy(target);
        proxy.save();
    }
}

运行测试代码后,打印:

保存数据

这时候,如果要扩展功能,比如说,对原先的保存接口添加事务处理。当然,我们可以直接修改具体实现类UserDaoImpl,但根据设计模式的开闭原则,不建议我们直接修改已实现的类,这个时候我们就可以通过代理来实现,我们通过给UserDaoProxy添加对应的方法即可:

public class UserDaoProxy implements IUserDao {
    private IUserDao iUserDao;

    public UserDaoProxy(IUserDao iUserDao) {
        this.iUserDao = iUserDao;
    }

    @Override
    public void save() {
        before();
        iUserDao.save();
        after();
    }

    private void before() {
        System.out.println("开启事务");
    }
    private void after() {
        System.out.println("关闭事务");
    }
}

重新运行程序:

开启事务
保存数据
关闭事务
静态代理总结
  1. 静态代理可以做到在不修改目标实现的情况下,对目标功能进行扩展;并且代理对象作为客户端和目标对象之间的中介,起到了保护目标对象的作用;
  2. 一般来说,静态代理具体实现类与代理类要一一对应,并且代理对象需要与目标对象实现一样的接口,所以有可能会有很多代理类。并且,一旦接口增加方法,目标对象与代理对象都需要维护,无形之中增加了系统的复杂度;

三、动态代理

  相比静态代理,动态代理有更强的灵活性。动态代理是在程序运行时,通过反射机制动态创建生成的。动态代理类使用字节码动态生成加载技术,在运行时生成加载类。生成动态代理类的方法很多,如JDK自带的动态代理、CGLIB、Javassist 或者 ASM 库等。我们先来看下JDK自带的生成动态代理的方式。

JDK动态代理
1. 实现类和方法

我们首先来看下JDK动态代理中涉及到的基础类和方法:

类或接口:java.lang.reflect.Proxyjava.lang.reflect InvocationHandler,位于Java反射包下;
方法:Proxy中的newProxyInstance方法,InvocationHandler中的invoke方法;

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) {...}

该方法返回一个目标接口的代理类实例。参数简单说明:

  1. ClassLoader loader:指定当前目标对象使用的类加载器
  2. Class<?>[] interfaces:目标对象实现的接口的类型
  3. InvocationHandler h:用于事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
public Object invoke(Object proxy, Method method, Object[] args)

这个方法用于实现目标对象具体方法的调用。

2. 实现步骤

JDK代理的大致实现步骤如下:

a. 创建底层接口及真实的对象;
b. 创建一个InvocationHandler接口的实现类,实现invoke()方法;
c. 调用Proxy的静态方法,创建一个代理类
d. 通过代理调用方法

3. 代码实现

接下来,我们来看一下代码实现,我们先新建代理类:

public class DynamicProxy implements InvocationHandler {

    /** 要代理的对象 */
    private Object object;

    /**
     * 将被代理者的实例传进动态代理类的构造函数中
     * @param object
     */
    public DynamicProxy(Object object) {
        this.object = object;
    }

    /**
     * 覆盖InvocationHandler接口的invoke方法,代理实现具体方法的调用,并可以添加我们的实现
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(this.object, args);
        after();
        return result;
    }

    /**
     * 获取代理对象
     *
     * @return the instance
     */
    public Object getInstance() {
        return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), this);
    }

    private void before() {
        System.out.println("开启事务");
    }
    private void after() {
        System.out.println("关闭事务");
    }
}

测试程序:

public static void main(String[] args) {
    //目标对象,并且打印下
    IUserDao target = new UserDaoImpl();
    System.out.println(target.getClass());
    // 代理对象,并且打印下
    IUserDao userDao = (IUserDao)new DynamicProxy(target).getInstance();
    System.out.println(userDao.getClass());
    userDao.save();
}

打印结果:

class com.proxy.UserDaoImpl
class com.sun.proxy.$Proxy0
开启事务
保存数据
关闭事务
4.JDK代理总结
  1. JDK动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到一个集中的方法中处理,即使接口方法数量比较多的时候,我们也可以进行灵活的处理;
  2. JDK动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。而CGLIB动态代理则是弥补了这部分的不足。
CGLIB动态代理
1. CGLIB简介

  我们上面所了解的静态代理和JDK动态代理模式,都是要求目标对象是实现一个接口的类,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这时候就可以考虑通过CGLIB代理来实现了。

  CGLIB(Code Generation Library),是一个第三方代码生成库,它的原理是程序运行的时候,为指定的目标类生成一个子类,从而实现对目标对象功能的扩展。所以有时候CGLIB代理也可以叫做子类代理。由于是通过继承来实现的,所以我们不能对final修饰的类进行代理。

CGLIB有以下特点:

  • Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截);
  • CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它需要你对JVM内部结构包括class文件的格式和指令集都很熟悉。
2. CGLIB代码实现

由于CGLIB是一个第三方包,所以我们在使用的时候需要手动引入cglib的jar,并且由于CGLIB底层使用了ASM字节码开源包,所以还需要引入ASM的jar包。对应的maven引入是:

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.6</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>5.2</version>
</dependency>

其实,CGLIB的实现和JDK动态代理的实现是类似的,我们简单来看一下代理类:

public class CGLIBProxy implements MethodInterceptor {
    /** 代理的对象 */
    private Object object;

    public CGLIBProxy(Object object) {
        this.object = object;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        before();
        Object result = method.invoke(object, args);
        after();
        return result;
    }

    /**
     * 获取代理对象
     *
     * @return the instance
     */
    public Object getInstance() {
        //工具类
        Enhancer en = new Enhancer();
        //设置父类
        en.setSuperclass(object.getClass());
        //设置回调函数
        en.setCallback(this);
        //创建子类对象代理
        return en.create();
    }

    private void before() {
        System.out.println("开启事务");
    }

    private void after() {
        System.out.println("关闭事务");
    }
}

进行测试:

public static void main(String[] args) {
    // 实际对象 并打印下
    UserDaoImpl userDao = new UserDaoImpl();
    System.out.println(userDao.getClass());

    // 代理对象 并打印下
    UserDaoImpl userProxy = (UserDaoImpl)new CGLIBProxy(userDao).getInstance();
    System.out.println(userProxy.getClass());

    userProxy.save();
}

打印结果:

class com.proxy.UserDaoImpl
class com.proxy.UserDaoImpl$$EnhancerByCGLIB$$ceeeb256
开启事务
保存数据
关闭事务
3. CGLIB总结
  1. CGLIB与JDK动态代理的最大区别就是要代理的对象是否实现了接口;
  2. CGLIB是基于继承来实现的动态代理,所以要求被代理的类不能是final类型;
  3. Spring的AOP 是JDK动态代理和CGLIB代理的很好的一个实现。如果对象实现了接口,则默认情况下AOP会采用JDK动态代理,也可以强制使用CGLIB代理;如果目标对象没有实现接口,则AOP会采用CGLIB代理,也就是说Spring会自动在JDK动态代理和CGLIB之间进行选择;
其他动态代理
Javassist代理
  1. Javassist, 也是一个开源的Java字节码的类库,是和CGLIB类似,属于一种高级的字节码生成库,通过Javassist对字节码操作来实现代理,比如使用Javassist对JBoss动态的实现AOP框架。如果有需要,我们可以看下相应的实现。
ASM代理
  1. ASM,同样也是一种字节码生成库,ASM能够以二进制形式修改已有类或者动态生成类,ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
  2. 不过相对于Javassist来说,ASM属于一种相对低级的字节码库,在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。虽然ASM是性能最高的一种动态代理方式,但由于操作繁琐,要求较高,所以一般情况下,如果不是在对性能有苛刻要求的场合,还是推荐 CGLIB 或者 Javassist。

四、 应用场景

以下摘录自:IBM-代理模式的应用场合
代理模式有多种应用场合,如下所述:

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

五、回顾与总结

在以上内容中,我们学习了代理模式的两种情况:静态代理和动态代理,并且学习了动态代理中JDK代理和CGLIB代理的实现。我们简单总结下:

  1. 所谓代理,其实就是为了解决直接访问对象相关的问题,其实数白了就两个字:中介;
  2. 静态代理和动态代理其实类似,都需要生成代理类,只是生成代理类的时期不同,静态代理是在编译期就已生成,而动态代理是在程序运行期间动态生成的;
  3. JDK代理和CGLIB代理最大的区别就是要代理的对象是否实现了接口;他们两者不是相互对立的局面,而是相互协作的,而Spring中的AOP正是他们协作的最佳实践;
  4. 了解与合理的使用设计模式,不但能让我们能容易的理解别人优秀的代码,也可以让我们写出优秀的代码;

参考资料:
《大话设计模式》
Java三种代理模式:静态代理、动态代理和cglib代理
Java设计模式——代理模式实现及原理
代理模式原理及实例讲解

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

推荐阅读更多精彩内容