初尝Java动态代理

2018-11-22 by Victor Lv Filed under 技术博客

有动自有静,在看动态代理之前先看更简单明了的“静态代理”。下面以实例描述。

(静态)代理

定义一个 Human 接口: Human.java

public interface Human {
    void goToWork();
}

我们假设所有 Human 都有一个动作就是 goToWork。然后在 Human 的基础上衍生出 Man 和 Woman : Man.java

public class Man implements Human {
    @Override
    public void goToWork() {
        System.out.println("Man go to work");
    }
}

Woman.java

public class Woman implements Human {
    @Override
    public void goToWork() {
        System.out.println("Woman go to work");
    }
}

当我们想分别驱使上面的 Man 和 Woman 都去工作时,很简单,分别 new 一个 Man 和 Woman 出来,然后分别调用 goToWork 方法即可:

Man man = new Man();
man.goToWork();

Woman woman = new Woman();
woman.goToWork();

/**
* Output:
* Man go to work
* Woman go to work
*/

然后,如果我们想在人们 goToWork 之前叫他们干点别的必备的事情呢?比如让他们出门 goToWork 之前都把工作服穿上。或者让他们 goToWork 完成之后把工作服脱掉?   把 Man 和 Woman 的实现都修改一遍?No,我们可以用代理来帮我们实现这件事。

一开始,当我们(假设我们是有钱的大老板)想驱使 Man 和 Woman 去干活时,我们需要一一去叫他们:“Hey, Man,go to work!” 和 “Hey, Woman, go to work!",然后,我们觉得他们的穿的衣服太丑了,想叫他们工作之前统一穿上工作服,那我们同样需要给他们分别进行“思想改造”:"Hey, Man, dress up before you go to work!","Hey, Woman, dress up before you go to work!"。

这样传统的方法,有两个问题,第一:作为有钱的大老板,对于这些琐碎而重复的事,当然想节省点口舌;第二:这些底层劳工,要对他们进行“思想改造”,难滴很。

于是,机智的老板想到了一个办法:请一个监工(代理),让这个监工自动帮他们穿上和脱下工作服,用代码描述如下: HumanProxy.java

/**
 * @ClassName: HumanProxy
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/11/22 10:41
 * @Version: 1.0
 */

public class HumanProxy implements Human{
    Human human;

    HumanProxy(Human human) {
        this.human = human;
    }

    @Override
    public void goToWork() {
        beforeGoToWork();
        this.human.goToWork();
        afterGoToWork();
    }

    private void beforeGoToWork(){
        System.out.println("Dress up work clothes before go to work");
    }

    private void afterGoToWork(){
        System.out.println("Take off work clothes after go to work");
    }
}

然后,老板只需要做同样的工作量,如下:

Human man = new HumanProxy(new Man());
man.goToWork();

Human woman = new HumanProxy(new Woman());
woman.goToWork();

/**
* Output:
*
* Dress up work clothes before go to work
* Man go to work
* Take off work clothes after go to work
* Dress up work clothes before go to work
* Woman go to work
* Take off work clothes after go to work
*/

就可以做到更多的事情了。一来解放了自己的双手,同样的工作量;二来,也无须对 Man 和 Woman 进行艰难的“思想改造”了,是不是很棒!

上面就是所谓的静态代理模式。


动态代理

上面还只是一个很简单的任务叠加,使用静态代理模式还没啥问题。但是,随着本工厂的业务的扩展和工人数量的增多,我们生产出了一个打卡器用来监控每个工人的上下班时间,工人们上班前和下班后都要打一次卡。

于是,用上面静态代理的方式,我们很容易用同样的方法造出一个“打卡器代理”: TimeProxy.java

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @ClassName: TimeProxy
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/11/22 10:54
 * @Version: 1.0
 */

public class TimeProxy implements Human {
    Human human;

    TimeProxy(Human human) {
        this.human = human;
    }

    @Override
    public void goToWork() {
        logTime();
        human.goToWork();
        logTime();
    }

    private void logTime(){
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        String currentTime = dateFormat.format(new Date());
        System.out.println("Current time is: " + currentTime);
    }
}

进行调用:

Human man = new TimeProxy(new Man());
man.goToWork();

Human woman = new TimeProxy(new Woman());
woman.goToWork();

/**
* Output:
*
* Current time is: 2018-11-22 16:07:36:161
* Man go to work
* Current time is: 2018-11-22 16:07:36:163
* Current time is: 2018-11-22 16:07:36:164
* Woman go to work
* Current time is: 2018-11-22 16:07:36:165
*/

老板发现“打卡器--TimeProxy”这玩意很好用,于是想把它推向市场,适配更多用户,而不仅仅是本工厂的 Man 和 Woman。

然后问题来了,我们有几百类目标用户,总不能让"TimeProxy" 去 implements这几百类接口或者写几百个这样的静态代理吧?

而动态代理就能解决这个问题,把静态代理进行动态的拓展。下面就来看看动态代理是怎么做到这件事的:

DynamicTimeProxy.java

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @ClassName: DynamicTimeProxy
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/11/22 11:10
 * @Version: 1.0
 */

public class DynamicTimeProxy implements InvocationHandler {

    private Object target;

    public Object bind(Object target) {
        this.target = target;
        Object result =  Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                this);

        System.out.println("target address:" + System.identityHashCode(target));
        System.out.println("result address:" + System.identityHashCode(result));
        return result;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        logTime();
        result = method.invoke(target, args);
        logTime();
        return result;
    }

    private void logTime(){
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        String currentTime = dateFormat.format(new Date());
        System.out.println("Current time is: " + currentTime);
    }
}

调用测试:

DynamicTimeProxy dynamicTimeProxy = new DynamicTimeProxy();
System.out.println("dynamicTimeProxy address:" + System.identityHashCode(dynamicTimeProxy));
Human human1 = (Human) dynamicTimeProxy.bind(new Man());
System.out.println("human1 address:" + System.identityHashCode(human1));
human1.goToWork();

Human human2 = (Human) dynamicTimeProxy.bind(new Woman());
System.out.println("human2 address:" + System.identityHashCode(human2));
human2.goToWork();

/**
* Output:
dynamicTimeProxy address:1163157884
target address:2125039532
result address:312714112
human1 address:312714112
Current time is: 2018-11-23 10:48:33:055
Man go to work
Current time is: 2018-11-23 10:48:33:057
target address:1878246837
result address:929338653
human2 address:929338653
Current time is: 2018-11-23 10:48:33:060
Woman go to work
Current time is: 2018-11-23 10:48:33:060
*/

关于动态代理的原理,网上很多文章都写的不明不白,自己也翻了 JDK 源码,有点被层层调用绕晕了,并且到了最底层调用的是无法直接追寻源码的 native 方法(native 方法表明它的具体实现用 JDK 底层语言实现的,比如 C 语言)。直到我看到了 IBM developerWorks 的这篇剖析地很棒的文章:Java 动态代理机制分析及扩展,第 1 部分

用一张图就能看的很直白了:

动态代理类的继承图(转载自Java 动态代理机制分析及扩展,第 1 部分)

动态代理类的继承图

解释:ProxyN 继承_extends 了 Proxy(对应上述例子的 DynamicTimeProxy),并且实现_implements 了 InterfaceA/B/X(对应上述例子的 Bird 接口)。

有了这张图我们就能很好地解释动态代理的原理了。上述动态代理最关键的一处代码是 DynamicTimeProxybind()方法中的Proxy.newProxyInstance。这里干了些什么呢?就是上面继承图描述的,这里它生成了一个对象 ProxyN,这个 ProxyN 继承于 DynamicTimeProxy ,并且实现了 Human 接口(具体实现就是用的 invoke 方法里面的内容),一个 extends 加一个 implements,于是,这个 ProxyN 既拥有 Human 所有的方法,又拥有一个内部属性 target。

1、所以我们也就能理解为什么我们能把 Proxy.newProxyInstance生成出来的对象(ProxyN)安全转换成 Human了,因为它 implements 了 Human。那这个 ProxyN 是不是一个 Man/Woman 呢?测试一下就知道了: ```java DynamicTimeProxy dynamicTimeProxy = new DynamicTimeProxy(); Man man = (Man) dynamicTimeProxy.bind(new Man()); man.goToWork();

/ Output(Exception): Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to Man / ``` 可以发现 ProxyN 并不能转成 Man,说明它并不是(extends)一个 Man。

2、所以之后对于Proxy.newProxyInstance生成对象 human1/human2 方法的调用,实际上调用的是这个 ProxyN Override出来的方法(假设是 newGoToWork()),而 newGoToWork() 一方面在 goToWork 之前和之后加上了新功能,另一方面它内部调用的 goToWork 实际上又是 ProxyN 内部的 target(Man/Woman)的 goToWork 方法。于是这就能完美解释程序的输出结果了。

此外,上述程序中为了证明生成的对象是不一样的(地址),我把他们的内存地址用 System.identityHashCode打印了出来。

于是乎,有了这个动态代理,工厂老板就可以把它的“打卡器“推向更广阔的市场应用场景了。 最后,再次推荐看下这篇文章,关于动态代理的原理说的够清楚了: Java 动态代理机制分析及扩展,第 1 部分


本文作者为 Victor Lv ,原出处为Victor Lv's Blog(http://langlv.me),转载请保留此句。


标签 动态代理
Fork me on GitHub