动态代理——从一窍不通到恍然大悟

来源:互联网 发布:淘宝家装物流干线运费 编辑:程序博客网 时间:2024/06/11 05:46

前言

动态代理是Java常见的一种设计模式,很多文章都介绍了什么是代理、静态代理和动态代理的实现方式,然而这些都偏理论,一篇好的文章要让大家知道知识点的具体用处,本文在博主看了几篇博文之后的总结和细化,希望能帮助大家的理解。

一个简单的代理

代理就是为其他对象提供一个代理以控制对某个对象的访问。比如火车票代售点就是一个代理,它控制要买火车票的人(其他对象)对火车站售票点(某个对象)的访问,由它向火车票售票点买票。

为实现达到这样的效果,可以如下图实现功能设计:
这里写图片描述

Client类就是想买火车票的人,Proxy类就是火车票代售点,RealSubject类是火车站售票点,Proxy和RealSubject类提供的买票能力体现在DoAction()方法中,Proxy的DoAction()调用RealSubject的DoAction方法(体现代理的思想),当然Proxy类的DoAction()可以有其他操作,比如欢迎买票人等。

这样一个流程就是体现了简单的代理思想。

稍微实用的代理

换一个复杂点的例子来看,考虑一个字体提供功能,字体库可能源自本地磁盘、网络或者系统。
先考虑从本地磁盘中获取字体,和上面的例子一样,采用代理的方式实现,定义一个提供字体的接口FontProvider:

public interface FontProvider {    Font getFont(String name);}

接口的实现类FontProviderFromDisk,也是我们真正提供获取磁盘字体库的类:

class FontProviderFromDisk implements FontProvider{    @Override    Font getFont(String name){        System.out.println("磁盘上的字体库");    }}

接着就是实现我们的代理类ProxyForFont:

class ProxyForFont implements FontProvider{    private FontProvider fontProvider;    ProxyForFont(FontProvider fontProvider){        this.fontProvider=fontProvider;    }    @Override    Font getFont(String name){        System.out.println("调用代理方法前可以做点事情");        fontProvider.getFont(name);        System.out.println("调用代理方法后可以再做点事情");    }}

当我们需要从磁盘获取字体库时,直接调用ProxyForFont就可以了:

public class MyFontProvider{    public static void main(String[] args) {        FontProvider fp=new ProxyForFont(new FontProviderFromDisk());        fp.getFont("字体库名");    }}

这样实现的好处在哪儿呢?比如,每次从磁盘获取字体库的时候,磁盘的I/O比较耗时,想通过缓存将读到的字体库暂存一份。此时,我们直接修改ProxyForFont类:

class ProxyForFont implements FontProvider{    private FontProvider fontProvider;    ProxyForFont(FontProvider fontProvider){        this.fontProvider=fontProvider;    }    @Override    Font getFont(String name){        System.out.println("检查磁盘缓存中是否存在字体库");        if(不存在){            fontProvider.getFont(name);            System.out.println("将磁盘读到的字体库保存到缓存");        }else{            System.out.println("如果存在直接从缓存中获取");        }    }}

当然,也可以直接修改FontProviderFromDisk类的getFont方法,但是这样会有一个问题。上文中我们提到,字体库的获取源除了磁盘还有系统和网络等,所以还存在FontProviderFromSystem和FontProviderFromNet两个类,如果这两个类也需要缓存功能的时候,还得再改动这两个类的getFont实现,而代理模式中,只需要在代理类ProxyForFont中修改即可。此时从磁盘、系统和网络获取字体库的方式如下:

public class MyFontProvider{    public static void main(String[]args) {        FontProvider fp0=new ProxyForFont(new FontProviderFromDisk());        fp0.getFont("字体库名");        FontProvider fp1=new ProxyForFont(new FontProviderFromSystem());        fp1.getFont("字体库名");        FontProvider fp2=new ProxyForFont(new FontProviderFromNet());        fp2.getFont("字体库名");    }}

可扩展性不言而喻,以上都是静态代理的实现方式,是不是感觉静态代理已经无所不能了呢?我们再来看一个需求。

更实用的代理

以上都是获取字体库,如果想获取图片、音乐等其他资源呢?这个时候一个FontProvider接口就不够用了,还得提供ImageProvider和MusicProvider接口,实现对应的功能类ImageProviderFromDisk,ImageProviderFromSystem,ImageProviderFromNet,MusicProviderFromDisk,MusicProviderFromNet,MusicProviderFromDisk以及代理类ProxyForImage和ProxyForMusic。当要给获取图片和获取音乐等都加上缓存功能的时候,ProxyForImage和ProxyForMusic都需要改动,而缓存的逻辑三个类又是相同的,如此写的代码就不够优美。怎么办,动态代理就发挥作用了:

public class CachedProviderHandler implements InvocationHandler {    private Map<String, Object> cached = new HashMap<>();    private Object target;    public CachedProviderHandler(Object target) {        this.target = target;    }    public Object invoke(Object proxy, Method method, Object[] args)        throws Throwable {        Type[] types = method.getParameterTypes();//获取方法的名、参数等信息        if (method.getName().matches("get.+") && (types.length == 1) &&                (types[0] == String.class)) {//根据函数的开头名、参数个数、参数类型判断是否是获取资源的方法            String key = (String) args[0];//本文的例子中,接口方法只有一个参数,获取的就是这个传入的参数            //先读缓存            Object value = cached.get(key);            //没有就调用从其它媒介获取资源的方法            if (value == null) {                value = method.invoke(target, args);                //缓存资源                cached.put(key, value);            }            return value;        }        return method.invoke(target, args);    }}

注意:有了CachedProviderHandler类,原来的ProxyForFont,ProxyForImage和ProxyForMusic类就不需要了。
主函数中依赖CachedProviderHandler和接口功能实现类.+ProviderFrom.+实现代理访问。以从磁盘获取字体集为例子:

public class Main {    public static void main(String[] args) {        FontProvider realFontProvider = new FontProviderFromDisk();        InvocationHandler handler = new MyInvocationHandler(realFontProvider);        FontProvider fontProvider = (FontProvider) Proxy.newProxyInstance(realFontProvider.getClass().getClassLoader(), realFontProvider.getClass().getInterfaces(), handler);//fontProvider就是动态创建的代理类实例        fontProvider.getFont("我的字体集名字", 0);        }

main函数中由Proxy.newProxyInstance动态创建的代理对象实例fontProvider调用getFont方法后,JVM提供的动态代理框架会自动调用CachedProviderHandler的invoke方法,invoke方法中可以拿到fontProvider的对象、getFont方法对象以及getFont的String name参数。动态代理中,无需手动实现代理类,传入接口功能实现类(如FontProviderFromDisk)、接口和CachedProviderHandler对象后,Proxy.newProxyInstance动态创建了一个代理类对象。
invoke方法中的功能就是静态代理ProxyForFont等三个类中重写的getFont方法需要提供的功能。因为ProxyForFont和ProxyForMusic和ProxyForImage中相同的getFont方法实现,现在都在invoke方法中一次实现。
可见,动态代理和静态代理处理问题的思路没有差别,它们的差别在于创建代理时的代码不一样,动态代理有Java提供的框架支持,而静态代理需要开发者编码,所以动态代理节省了代码量,避免相同功能的重复代码。

总结

代理作为一种常见的设计模式,无论是静态还是动态,它们解决问题的思路上是没差的,但是动态代理由于框架的存在,比如Java自带的动态处理框架,可以帮助开发者节约代码量,提高开发效率。知道了这一点,在提到动态代理的时候,大家就不用疑惑为啥动态代理得这么玩了。
本文只是通过一个例子对代理有肤浅的介绍,等博主对反射有进一步了解后,再来分析下JDK的动态代理框架具体实现方式,以及cglib这种和JDK思路不完全一样的代理框架。
本文的诞生,要感谢Java动态代理封装、 Java JDK 动态代理(AOP)使用及实现原理分析、Java动态代理的作用是什么?和Java代理和动态代理机制分析和应用。
很惭愧,做了一点微小的贡献!

1 0
原创粉丝点击