java 设计模式 —— 浅析代理模式

本文通过一个实际的爬虫案例,介绍了Java中的静态代理和动态代理。在静态代理中,通过代理类增加了被代理类的功能,但存在代码冗余和维护困难的问题。动态代理则在运行时创建,解决了静态代理的局限,可以在运行时处理被代理类的所有方法,且不限制被代理类的类型。动态代理类在Java中由`java.lang.reflect.Proxy`类生成,实现了指定接口,并通过`InvocationHandler`进行方法调用。

静态代理业务场景

这天你早早地到了公司,打开电脑准备写代码,组里突然给你下发了一个任务,由于 java 后台组这几天有点缺人,临时需要你帮个忙,让你做一个 百度网站的爬虫,虽然你满肚子不情愿,但是又没有办法,谁让你是一个小全栈呢~

幸运的是,等你来到后台组的时候,后台的大佬告诉你基本的框架已经搭建好了,你只需要完成网页爬取的代码就行了,于是你啪啪啪打起键盘写下了如下的代码 ——

1.首先创建一个爬虫接口:

public interface Spider {
    void spider() throws IOException;
}

2.再是各个类:

public class JavaSpider implements Spider {
    private final String URL = "http://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=92751549_hao_pg&wd=JAVA&oq=%25E5%258A%25A8%25E6%2580%2581%25E4%25BB%25A3%25E7%2590%2586%25E7%2594%259F%25E6%2588%2590%25E7%25B1%25BB%25E5%259C%25A8%25E5%2593%25AA%25E9%2587%258C&rsv_pq=ff31a7ae0006648a&rsv_t=3c2aOlwowmM8l8mE7hifw23DGvHmskmk4d9%2BbGw%2BmtCDL%2Ftwh2IqdiK85K07EXK8z5Z7IXOF&rqlang=cn&rsv_enter=1&rsv_sug3=4&rsv_sug1=3&rsv_sug7=100&rsv_sug2=0&inputT=1149&rsv_sug4=1149";

    @Override
    public void spider() throws IOException {
        HttpURLConnection connection = HTTPUtil.connection(URL);
        InputStream stream = connection.getInputStream();
        // 对网页的操作
        if (stream != null) {
        // 爬取各个 html 标签下的内容
            byte[] buffer = new byte[1024];
            int read = 0;
            while ((read = stream.read(buffer)) != -1) {
                System.out.print(new String(buffer, 0, read));
            }
        }
        // 插入数据到数据库中
        insert2Database();
    }

    private void insert2Database() {
        // 模拟插入数据库
        System.out.println(getClass().getName() + ": 已插入数据库\n========================");
    }
}

完毕!但你收拾正要起身的时候,突然又被后台组的老大叫住了——“我去,小超你就写完了?速度可以啊,不过我看你也不是很忙,继续帮我把其他7个子界面一起爬了吧,公司都要这些数据呢。”虽然你此时此刻想和这个后台组的老大单挑一波,但是最终还是忍住了,写写爬虫也不是很耗脑子的事情,那就继续写吧,代码如下 ——

public class AndroidSpider implements Spider {
    private final String URL = "http://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=92751549_hao_pg&wd=Android&oq=JAVA&rsv_pq=d0b52cbe0003764e&rsv_t=41f18noX9t6D9n050OkCQLVTUNkKMtmZBwI4%2FpcyiEue4dfGWNK6kQzCvcdLchSNfMO25viL&rqlang=cn&rsv_enter=1&inputT=2310&rsv_sug3=11&rsv_sug1=9&rsv_sug7=100&rsv_sug2=0&rsv_sug4=2310";

    @Override
    public void spider() throws IOException {
        HttpURLConnection connection = HTTPUtil.connection(URL);
        InputStream stream = connection.getInputStream();
        // 对网页的操作
        if (stream != null) {
            // 爬取各个 html 标签下的内容
            byte[] buffer = new byte[1024];
            int read = 0;
            while ((read = stream.read(buffer)) != -1) {
                System.out.print(new String(buffer, 0, read));
            }
        }
        // 插入数据到数据库中
        insert2Database();
    }

    private void insert2Database() {
        // 模拟插入数据库
        System.out.println(getClass().getName() + ": 已插入数据库\n========================");
    }
}

public class OkHttpSpider implements Spider {
    private final String URL = "http://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=92751549_hao_pg&wd=OKHTTP&oq=Android&rsv_pq=974ab75f00038319&rsv_t=0d8cHJ8skuhEa3sCpOxD%2FiaT60LsiOMBqlJnnqnGJ2CXmzEN2GiFA0ft3Hz2SHK5osgvOX%2B0&rqlang=cn&rsv_enter=1&inputT=1394&rsv_sug3=17&rsv_sug1=14&rsv_sug7=100&rsv_sug2=0&rsv_sug4=1394";

    @Override
    public void spider() throws IOException {
        HttpURLConnection connection = HTTPUtil.connection(URL);
        InputStream stream = connection.getInputStream();
        // 对网页的操作
        if (stream != null) {
            // 爬取各个 html 标签下的内容
            byte[] buffer = new byte[1024];
            int read = 0;
            while ((read = stream.read(buffer)) != -1) {
                System.out.print(new String(buffer, 0, read));
            }
        }
        // 插入数据到数据库中
        insert2Database();
    }

    private void insert2Database() {
        // 模拟插入数据库
        System.out.println(getClass().getName() + ": 已插入数据库\n========================");
    }
}

// ....

3.最后就是测试类了:

public class Main {
    public static void main(String[] args) {
        Spider javaSpider = new JavaSpider();
        Spider androidSpider = new AndroidSpider();
        Spider okHttpSpider = new OkHttpSpider();
        // .......

        javaSpider.spider();
        androidSpider.spider();
        okHttpSpider.spider();
        // .......
    }
}

测试一下:

Markdown

加上最开始做的首页数据,你一共做了7个页面,好不容易做完了,被后台组老大夸了几句之后也终于能走了,继续做自己项目组的项目去了。时间一转到了下午三点多,后台组老大火急火燎地跑来——“小超啊,你过来下吧,嘿嘿,之前的爬虫出了点问题。”

这个时候可能你已经把后台组老大家的亲戚问候了一遍,但是抱着都是一个公司抬头不见低头见的心态,你还是去帮忙了。

“小超啊,估计是之前测试的时候,我们没注意调整访问的速率和次数,A 司的服务器又比较菜,压力过大崩了几次之后,他们那边估计做了调整,我们的爬虫失效了,不过幸运地是也只有 AndroidSpiderOkHttpSpider 爬不了数据,你看看来帮个忙呗,嘿嘿。”

“行吧。”带着满肚子的不情愿和之前的代码,你回到了自己的项目组。

幸运地是,经过短暂的测试,你发现只需要在 HTTP 请求的请求头中添加一个 Host 请求头就行了。于是你屁颠屁颠地跑过去找到后台组的老大告诉他在 HTTPUtil 中初始化 HttpUrlConnection 的时候添加一下 Host 请求头,但是没想到被后台组老大果断的拒绝了,“这可不行啊小超,因为 A 司有些网页的请求中也需要 Host 请求头,而它的这个值和你所要求的这个值不同啊(实际上这在日常开发中几乎不存在),如果按照你所要求的设置了的话,你的爬虫是没问题了,但是别的爬虫要失效了。不过我们的架构师在 HTTPUtil 里面暴露了一个 setHeader(key, value) 方法,你完全可以使用这个方法满足你的需求!”

HTTPUtil 代码如下:

public class HTTPUtil {
    private static Map<String, String> headers = new HashMap<>();

    public static void setHeader(String header, String value) {
        headers.put(header, value);
    }

    public static HttpURLConnection connection(String url) {
        if (headers.size() == 0) {
            System.out.println("获取到了一个最普通的 HttpURLConnection");
        } else {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("获取到了一个携带有:\n");
            headers.forEach((header, value) ->
                    stringBuilder.append(header).append(":").append(value).append("\n")
            );
            stringBuilder.append("的 HttpURLConnection\n");
            System.out.println(stringBuilder.toString());
        }

        HttpURLConnection httpURLConnection = null;
        try {
            URL connection = new URL(url);
            httpURLConnection = (HttpURLConnection) connection.openConnection();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return httpURLConnection;
    }
}

回到组里,你想到要针对两个类都要修改他们的代码,一个一个地添加重复枯燥的代码,况且指不定以后还有的爬虫会失效然后还要做一些修改,你就不由自主深深地叹了一口气,恰巧小源大佬路过,看你愁眉苦脸的,他笑着问道,“怎么了小超?遇到什么麻烦了?”你把你的烦恼告诉了他,只见大佬笑了一笑抿了一口茶,说道“你去看看 java 中的代理模式,你的问题就解决了。”小源大佬总是能一针见血,听完小源大佬的指点,你立马打开百度了解了相关知识后,写下了如下的代码 ——

1.创建一个 HostAdder 类实现 Spider 接口:

public class HostAdder implements Spider {
    private final Spider spider;

    public HostAdder(Spider spider) {
        this.spider = spider;
    }

    @Override
    public void spider() throws IOException {
        // 添加请求头
        HTTPUtil.setHeader("Host", "www.baidu.com");
        spider.spider();
    }
}

2.直接修改测试类——

public class Main {
    public static void main(String[] args) throws IOException {
        Spider javaSpider = new JavaSpider();
        // Spider androidSpider = new AndroidSpider();
        Spider androidSpider = new HostAdder(new AndroidSpider());
        // Spider okHttpSpider = new OkHttpSpider();
        Spider okHttpSpider = new HostAdder(new OkHttpSpider());
        // .......

        javaSpider.spider();
        androidSpider.spider();
        okHttpSpider.spider();
        // .......
    }
}

运行一下——

Markdown

完美成功!因为在 HostAdderAAboutSpiderAContentSpider 在通过HTTPUtil 获取 HttpUrlConnection 连接前,HostAdder 通过调用 HTTPUtilsetHeader() 方法,给 HTTP 连接中添加了相应的请求头,而如果是仅在 AAboutSpiderAContentSpiderspider() 方法中增加添加请求头的方法,那么势必会减少代码的复用性。你高高兴兴地把代码拿给了后台组的老大,后台组老大非常高兴,嚷嚷着要请你吃饭,你嘴上说着不要这么客气,实际上心里早就乐开了花!

静态代理剖析

这就是 java 中的静态代理,通过代理类 HostAdder 添加了被代理类 Spider 接口原本没有的功能。熟悉装饰者模式的小伙伴可以发现,代理模式和装饰模式有一些相似之处,实际上不仅仅是装饰模式,甚至状态模式和适配器模式都与代理模式有一些相似之处。实际上,从广义的角度上来说,这三种设计模式都可以看成特殊的代理模式。

动态代理业务场景

好日子没过两天,Java 组的老大竟然又跑来找你了——

“小超呀,嘿嘿嘿,上次的爬虫又出现问题了,原因也和上次差不多,是得添加一个新的请求头 User-Agent:Mozilla/5.0 (Macintosh; U; Intel Mac OS X10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1Safari/534.50,不过,我还得有个小问题要告诉你一下,你上次做了7个页面的爬虫,实际上一共有15个页面的,还有8个页面的爬虫是小王做的,好消息是小王和你写的代码逻辑是一模一样的,坏消息是你的接口是 Spider,而他写的接口名字叫 WebSpider,而且他已经打成了 jar 包,嘿嘿,就靠你了哈,我还有事,先走了~”

“wtf?!”

虽然嘴上说着不愿意,但是实际上你的心里早已有底了,你啪啪啪敲起键盘,写下如下的代码——

1.创建一个自定义的 InvocationHandler

public class HTTPInvocationHandler implements InvocationHandler {
    private final Object object;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        beforeMethodInvoke();
        Object o = method.invoke(object, args);
        afterMethodInvoke();

        return o;
    }

    /**
     * 方法调用前我们需要做的事情
     */
    private void beforeMethodInvoke() {
        HTTPUtil.setHeader("Host", "www.baidu.com"); 
        HTTPUtil.setHeader("User-Agent", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1Safari/534.50");
    }

    /**
     * 方法调用后我们需要做的事情
     */
    private void afterMethodInvoke() {

    }
}

首先是构造函数中我们需要传入一个 Object 对象(被代理类),然后在 invoke 方法中代码也写得很清楚了,如果有希望在被代理类方法调用前需要做的事,那么就写在 beforeMethodInvoke() 方法中,希望在被代理类方法调用后需要做的事,那么就写在 afterMethodInvoke() 方法中,此处所需要做的就是在方法调用前先调用 HTTPUtilsetHeader() 方法添加相应的请求头。

2.编写测试类

2.1、首先是你所编写的实现了 Spider 接口的类——

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
    Class<Spider> spiderClass = Spider.class;

    Spider javaSpiderProxy = (Spider) Proxy.newProxyInstance(spiderClass.getClassLoader(), new Class[]{spiderClass}, new HTTPInvocationHandler(new AndroidSpider()));
    Spider androidSpiderProxy = (Spider) Proxy.newProxyInstance(spiderClass.getClassLoader(), new Class[]{spiderClass}, new HTTPInvocationHandler(new AndroidSpider()));
    Spider okHttpSpiderProxy = (Spider) Proxy.newProxyInstance(spiderClass.getClassLoader(), new Class[]{spiderClass}, new HTTPInvocationHandler(new OkHttpSpider()));

    javaSpiderProxy.spider();
    androidSpiderProxy.spider();
    okHttpSpiderProxy.spider();
}

2.2、再其次是小王的实现了 WebSpider 接口的类,一共是两个,分别是 IOSSpiderKotlinSpider——

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
    Class<WebSpider> webSpiderClass = WebSpider.class;

    WebSpider iOSSpiderProxy = (WebSpider) Proxy.newProxyInstance(webSpiderClass.getClassLoader(), new Class[]{webSpiderClass}, new HTTPInvocationHandler(new IOSSpider()));
    WebSpider kotlinSpiderProxy = (WebSpider) Proxy.newProxyInstance(webSpiderClass.getClassLoader(), new Class[]{webSpiderClass}, new HTTPInvocationHandler(new KotlinSpider()));

    iOSSpiderProxy.spider();
    kotlinSpiderProxy.spider();
}

测试结果如下:

Markdown

整合重构一下:

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
    Class<Spider> spiderClass = Spider.class;

    Spider javaSpiderProxy = getProxy(spiderClass, new AndroidSpider());
    Spider androidSpiderProxy = getProxy(spiderClass, new AndroidSpider());
    Spider okHttpSpiderProxy = getProxy(spiderClass, new OkHttpSpider());

    javaSpiderProxy.spider();
    androidSpiderProxy.spider();
    okHttpSpiderProxy.spider();

    Class<WebSpider> webSpiderClass = WebSpider.class;

    WebSpider iOSSpiderProxy = getProxy(webSpiderClass, new IOSSpider());
    WebSpider kotlinSpiderProxy = getProxy(webSpiderClass, new KotlinSpider());

    iOSSpiderProxy.spider();
    kotlinSpiderProxy.spider();
}

@SuppressWarnings("unchecked")
private static <T> T getProxy(Class<T> inter, T obj) {
    return (T) Proxy.newProxyInstance(inter.getClassLoader(), new Class[]{inter}, new HTTPInvocationHandler(obj));
}

小超拿着这样美如画的代码由衷的开心,将它郑重交给了后台组的老大后,后台组老大也很高兴,嚷嚷着以后一定要请小超吃饭。

动态代理剖析

看完以上的文章,你可能会有以下的问题:

  • 动态代理优于静态代理的地方在哪?


    静态代理的缺点:

    1)被代理类(例如小超第一次写的 AndroidSpiderOkHttpSpider)的一个接口(例如 HostAdder)只服务于一种类型的对象(例如 HostAdder 代理类只服务于 Spider 接口,而不能服务于其他接口),如果被代理类很多的话,势必要针对每一个接口都要书写一个代理类,这样就相当繁琐了。

    2)如果接口新增了一个方法,除了被代理类需要实现这个方法外,所有代理类也需要实现此方法。(例如 Spider 接口添加了一个方法,不仅仅 AndroidSpiderOkHttpSpider 要实现这个新方法,而且 HostAdder 也要实现这个方法)

    动态代理与静态代理相比较,动态代理会在 InvocationHandler 中统一处理被代理类的所有方法,并且对于被代理类的类型没有任何限制,所以无论接口做什么修改,对动态代理的代码都不会有所影响,只需要创建一个自定义的 InvocationHandler 而不用去针对每一个被代理类都去创建一个静态代理类。

  • 动态代理和静态代理的区别是什么,也就是“动”体现在那里?

    静态代理类的编写(例如上面的 HostAdder)我们在代码中是写死了的,也就是在运行前我们的代理类就是已经存在了的,而动态代理则是在运行时创建的,我们不妨在程序中添加如下一段代码:

    private static void createProxyClassFile() {
        final String name = "$Proxy0";
        byte[] data = ProxyGenerator.generateProxyClass(name, new Class[]{Spider.class});
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(name + ".class");
            System.out.println((new File(name)).getAbsolutePath());
            out.write(data);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != out) try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    在 main 方法中调用这个方法,在打印台就会打印出文件的位置,然后用 jd-gui 就可以查看源码了,如下图:

    Markdown

    去除异常处理的代码后如下:

    public final class $Proxy0 extends Proxy implements Spider {
        private static Method m1;
        private static Method m2;
        private static Method m3;
        private static Method m0;
    
        public $Proxy0(InvocationHandler paramInvocationHandler) {
            super(paramInvocationHandler);
        }
    
        public final boolean equals(Object paramObject) {
            return ((Boolean) this.h.invoke(this, m1, new Object[]{paramObject})).booleanValue();
        }
    
        public final String toString() {
            return (String) this.h.invoke(this, m2, null);
        }
    
        public final void spider()
                throws IOException {
            this.h.invoke(this, m3, null);
            return;
        }
    
        public final int hashCode() {
            return ((Integer) this.h.invoke(this, m0, null)).intValue();
        }
    
        static {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("dynamicProxy.spider.inter.Spider").getMethod("spider", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
        }
    }
    

    仔细观察一下我们可以看到,代码中所包含的方法实际上就是它所实现的接口的方法以及 Object 当中的 hasCode()equals()toString() 等方法,而这些方法的内部逻辑实际上是一模一样的,都是交给了 h 去处理,那么 h 是什么呢,我们可以看到这个动态代理类的构造函数,实际上是调用了父类的构造函数,所以我们不妨打开 Proxy 类来看一下,如下图:

    Markdown

    所以 h 实际上就是一个 InvocationHandler 对象,当然,针对于我们的动态代理类来说,它肯定就是我们所创建的 HTTPInvocationHandler 了。所以对于被代理类的所有方法来说,最后都将转发给代理类来实现。

动态代理属性

1.动态代理类的属性

  • 如果所有的代理接口都是 public 的,那么代理类就是 public、final 的,且不是 abstract 的
  • 动态代理类的名称以”$ProxyN”开头,N 是代理类的唯一编号
  • 动态代理类都继承于 java.lang.reflect.Proxy
  • 动态代理类实现了其创建时指定的接口,且保持接口指定的顺序
  • 如果动态代理类实现了一个非 public 接口,那么它将定义和接口相同的包名;否则代理类的包是不确定的,默认是 com.sun.proxy,运行时,包密封性不防止特定包成功定义代理类;如果都不是,动态代理类将由同一个类加载器和相同的包与特定签名定义.
  • 动态代理类实现了其创建时指定的所有接口,调用代理类 Class 对象的 getInterfaces 将返回和创建时指定接口顺序相同的列表,调用 getMethods 方法返回所有接口方法的数组对象,调用 getMethod 会返回代理类接口中期望的 method
  • 调用 Proxy.isProxyClass 方法时,传入 Proxy.getProxyClass 返回的 class 或者 Proxy.newProxyInstance 返回对象的 class,都会返回 true,否则返回 false
  • 代理类的 java.security.ProtectionDomain 是由系统根类加载器(Bootstrap ClassLoader)加载的,代理类的代码也是系统信任的代码生成的,此保护域通常被授予 java.security.AllPermission
  • 每一个代理类都有一个 public 的,含有一个 InvocationHandler 实现为参数的构造方法,设置了调用处理器接口,就不必使用反射 api 访问构造方法,通过 Proxy.newProxyInstance 可以产生和 Proxy.getProxyClass 和调用句柄相同的调用构造函数行为.

2.动态代理实例的属性

  • 给定一个代理实例 proxy,Foo 实现的接口之一,表达式 proxy instanceof Foo 返回 true,(Foo) proxy 能成功转换
  • 每个代理实例都关联一个 InvocationHandler, 通过 Proxy.getInvocationHandler 方法,将返回代理类关联的 InvocationHandler.
  • 代理类实例调用其代理接口中所声明的方法时,这些方法将被编码,并最终由调用处理器 (InvocationHandler) 的 invoke 方法执行.
  • 代理类根类 java.lang.Object 中的 hashCode,equals 和 toString 方法,也会被分派到调用处理其的 invoke 方法执行;可能的原因有:一是因为这些方法为 public 且非 final 类型,能够被代理类覆盖;二是因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。
  • 当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。

此部分参考自:Java之动态代理

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值