前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >动态代理——从一知半解到恍然大悟

动态代理——从一知半解到恍然大悟

作者头像
蜻蜓队长
发布2018-08-03 10:38:30
2170
发布2018-08-03 10:38:30
举报
文章被收录于专栏:Android机动车Android机动车
前言

动态代理是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 CachedProviderHandler(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自带的动态处理框架,可以帮助开发者节约代码量,提高开发效率。知道了这一点,在提到动态代理的时候,大家就不用疑惑为啥动态代理得这么玩了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android机动车 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个简单的代理
  • 静态代理
  • 动态代理
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档