动态代理


原文地址

代理模式

为其他对象提供一个代理以控制对某个对象的访问。代理类主要负责为委托了(真实对象)预处理:消息、过滤消息、传递消息给委托类,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理。类似于生活中的中介。

有一个打印机的类

public class Printer {
    public void print(){
        System.out.println("打印!");
    }
}

我想在打印之前先记录一下日志怎么做?

最简单的方法:在打印的功能前面直接加上记录日志的功能。

public class Printer {
    public void print(){
        System.out.println("记录日志!");
        System.out.println("打印!");
    }
  }

看上去好像没有问题,但是我们修改了打印机的源代码,破坏了面向对象的开闭原则,有可能影响到其它功能。怎么解决呢?很容易可以想到,既然不能修改原来的代码,那我新建一个类吧。

public class LogPrinter extends Printer {
    public void print(){
        System.out.println("记录日志!");
        System.out.println("打印!");
    }
}

这个类继承了打印机的类,重写了打印机的 print 方法,提供了记录日志的功能,以后需要打印机的时候使用这个类就好。问题似乎得到了解决,我们可以在这个解决方案的基础上进一步的优化:

静态代理

先抽象出一个接口:

public interface IPrinter {
    void print();
}

打印机类(被代理类)实现这个接口:

public class Printer implements IPrinter {
       @Override
       public void print(){
               System.out.println("打印!");
    }
}

创建打印机代理类也实现该接口

被代理类被传递给了代理类PrinterProxy,代理类在执行具体方法时通过所持用的被代理类完成调用。

在构造函数中将打印机对象传进去,实现接口的打印方法时调用打印机对象的打印方法并在前面加上记录日志的功能:

public class PrinterProxy implements IPrinter {
    private Printer printer;
    public PrinterProxy(){
        this.printer = new printer();
    }
    @Override
    public void print() {
        System.out.println("记录日志");
        printer.print();
    }
}

试一把:

public class Test {
    public static void main(String[] args) {
        PrinterProxy proxy = new PrinterProxy();
        proxy.print();
    }
}

结果出来了:

记录日志
打印

以后我们就可以直接实例化 PrinterProxy 对象调用它的打印方法了,这就是静态代理模式,通过抽象出接口让程序的扩展性和灵活性更高了。

静态代理的缺点

静态代理是完美无缺的吗?

考虑一下,如果我的打印机类中还有别的方法,也需要加上记录日志的功能,但是静态代理只能为一个类服务,就不得不将记录日志的功能写 n 遍。进一步如果我还有电视机,电冰箱的类里面的所有方法也需要加上记录日志的功能,那要重复的地方就更多了。

怎么办?

动态代理闪亮登场:

动态代理

要想不重复写记录日志的功能,针对每一个接口实现一个代理类的做法肯定不可行了,可不可以让这些代理类的对象自动生成呢?

利用反射机制在运行时创建代理类。

Jdk 提供了 invocationHandler 接口和 Proxy 类,借助这两个工具可以达到我们想要的效果。

invocationHandler 接口上场:

//Object proxy:被代理的对象 
//Method method:要调用的方法 
//Object[] args:方法调用时所需要参数 
public interface InvocationHandler {
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

接口里只有一个方法 invoke,这个方法非常重要,先混个脸熟,稍后解释。

Proxy 类上场,它里面有一个很重要的方法 newProxyInstance:

//CLassLoader loader:被代理对象的类加载器 
//Class<?> interfaces:被代理类全部的接口 
//InvocationHandler h:实现InvocationHandler接口的对象 
 public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException 

调用 Proxy 的 newProxyInstance 方法可以生成代理对象

一切准备就绪动态代理模式千呼万唤始出来:

接口 IPrinter 和 该接口的实现类 Printer 的代码同前。

实现一个类,该类用来创建代理对象,_它实现了_InvocationHandler 接口

public class ProxyHandler implements InvocationHandler {
    private Object targetObject;//被代理的对象
    //将被代理的对象传入获得它的类加载器和实现接口作为Proxy.newProxyInstance方法的参数。
    public Object newProxyInstance(Object targetObject){
        this.targetObject = targetObject;
        //targetObject.getClass().getClassLoader():被代理对象的类加载器
        //targetObject.getClass().getInterfaces():被代理对象的实现接口
        //this 当前对象,该对象实现了InvocationHandler接口所以有invoke方法,通过invoke方法可以调用被代理对象的方法
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);
    }
    //该方法在代理对象调用方法时调用
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("记录日志");
        return method.invoke(targetObject,args);
    }
}

被代理的对象 targetObject 可以通过方法参数传进来:

public Object newProxyInstance(Object targetObject){
       this.targetObject=targetObject;

我们重点来分析一下这段代码:

return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);

动态代理对象就是通过调用这段代码被创建并返回的

方法有三个参数:

第一个参数:

targetObject.getClass().getClassLoader():targetObject 对象的类加载器。

第二个参数:

targetObject.getClass().getInterfaces():targetObject 对象的所有接口

第三个参数:

this:也就是当前对象即实现了 InvocationHandler 接口的类的对象,在调用方法时会调用它的 invoke 方法。

再来看一下这段代码:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      //在这里可以通过判断方法名来决定执行什么功能
      System.out.println("记录日志");
      //调用被代理对象的方法
      return method.invoke(targetObject, args);
}

这个方法就是生成的代理类中的方法被调用时会去自动调用的方法,可以看到在这个方法中调用了被代理对象的方法: method.invoke(targetObject, args);

我们可以在这里加上需要的业务逻辑,比如调用方法前记录日志功能.

见证奇迹的时刻到了:

public class Test {
   public static void main(String[] args){
   ProxyHandler proxyHandler=new ProxyHandler();
   IPrinter printer = (IPrinter) proxyHandler.newProxyInstance(new Printer());
   printer.print();
   }
}

打印结果:

记录日志
打印

当执行 printer.print(); 时会自动调用 invoke 方法,很多初学者不理解为什么能调用这个方法,回忆一下创建代理对象的时候是通过

return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);

来创建的,方法的第三个参数 this 是实现了 InvocationHandler 接口的对象, InvocationHandler 接口有 invoke 方法。现在有点思路了吧~

将被代理的对象作为参数传入就可以执行里面的任意方法,所有的方法调用都通过 invoke 来完成。不用对每个方法进行处理,动态代理是不是很简洁。

动态代理的优势

  1. Proxy 类的代码量被固定下来,不会因为业务的逐渐庞大而庞大;
  2. 可以实现 AOP 编程,实际上静态代理也可以实现,总的来说,AOP 可以算作是代理模式的一个典型应用;
  3. 解耦,通过参数就可以判断真实类,不需要事先实例化,更加灵活多变。

复习对象的创建

很多初学 Java 的朋友眼中创建对象的过程

实际上可以换个角度,也说得通

所谓的 Class 对象,是 Class 类的实例,而 Class 类是描述所有类的,比如 Person 类,Student 类

可以看出,要创建一个实例,最关键的就是得到对应的 Class 对象。只不过对于初学者来说,new 这个关键字配合构造方法,实在太好用了,底层隐藏了太多细节,一句 Person p = new Person(); 直接把对象返回给你了。我自己刚开始学 Java 时,也没意识到 Class 对象的存在。

分析到这里,貌似有了思路:

能否不写代理类,而直接得到代理 Class 对象,然后根据它创建代理实例(反射) ?

Class 对象包含了一个类的所有信息,比如构造器、方法、字段等。如果我们不写代理类,这些信息从哪获取呢?苦思冥想,突然灵光一现:代理类和目标类理应实现同一组接口。之所以实现相同接口,是为了尽可能保证代理对象的内部结构和目标对象一致,这样我们对代理对象的操作最终都可以转移到目标对象身上,代理对象只需专注于增强代码的编写。还是上面这幅图:

所以,可以这样说:接口拥有代理对象和目标对象共同的类信息。所以,我们可以从接口那得到理应由代理类提供的信息。但是别忘了,接口是无法创建对象的,怎么办?

JDK 提供了 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 类,这两个类相互配合,入口是 Proxy,所以我们先聊它。

Proxy

Proxy 有个静态方法:getProxyClass(ClassLoader, interfaces),只要你给它传入类加载器和一组接口,它就给你返回代理 Class 对象。

用通俗的话说,getProxyClass() 这个方法,会从你传入的接口 Class 中,“拷贝” 类结构信息到一个新的 Class 对象中,但新的 Class 对象带有构造器,是可以创建对象的。打个比方,一个大内太监(接口 Class),空有一身武艺(类信息),但是无法传给后人。现在江湖上有个妙手神医(Proxy 类),发明了克隆大法(getProxyClass),不仅能克隆太监的一身武艺,还保留了小 DD(构造器)…(这到底是道德の沦丧,还是人性的扭曲,欢迎走进动态代理)

所以,一旦我们明确接口,完全可以通过接口的 Class 对象,创建一个代理 Class,通过代理 Class 即可创建代理对象。

所以,按我理解,Proxy.getProxyClass() 这个方法的本质就是:以 Class 造 Class。

有了 Class 对象,就很好办了,具体看代码:

完美。

根据代理 Class 的构造器创建对象时,需要传入 InvocationHandler。每次调用代理对象的方法,最终都会调用 InvocationHandler 的 invoke() 方法:

怎么做到的呢?

上面不是说了吗,根据代理 Class 的构造器创建对象时,需要传入 InvocationHandler。通过构造器传入一个引用,那么必然有个成员变量去接收。没错,代理对象的内部确实有个成员变量 invocationHandler,而且代理对象的每个方法内部都会调用 handler.invoke()!InvocationHandler 对象成了代理对象和目标对象的桥梁,不像静态代理这么直接。

大家仔细看上图右侧的动态代理,我在 invocationHandler 的 invoke() 方法中并没有写目标对象。因为一开始 invocationHandler 的 invoke() 里确实没有目标对象,需要我们手动 new。

但这种写法不够优雅,属于硬编码。我这次代理 A 对象,下次想代理 B 对象还要进来改 invoke() 方法,太差劲了。改进一下,让调用者把目标对象作为参数传进来:

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        CalculatorImpl target = new CalculatorImpl();
                //传入目标对象
                //目的:1.根据它实现的接口生成代理对象 2.代理对象调用目标对象方法
        Calculator calculatorProxy = (Calculator) getProxy(target);
        calculatorProxy.add(1, 2);
        calculatorProxy.subtract(2, 1);
    }

    private static Object getProxy(final Object target) throws Exception {
        //参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口
        Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
        Object proxy = constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        });
        return proxy;
    }
}

这样就非常灵活,非常优雅了。无论现在系统有多少类,只要你把实例传进来,getProxy() 都能给你返回对应的代理对象。就这样,我们完美地跳过了代理类,直接创建了代理对象!

不过实际编程中,一般不用 getProxyClass(),而是使用 Proxy 类的另一个静态方法:Proxy.newProxyInstance(),直接返回代理实例,连中间得到代理 Class 对象的过程都帮你隐藏:

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        CalculatorImpl target = new CalculatorImpl();
        Calculator calculatorProxy = (Calculator) getProxy(target);
        calculatorProxy.add(1, 2);
        calculatorProxy.subtract(2, 1);
    }

    private static Object getProxy(final Object target) throws Exception {
        Object proxy = Proxy.newProxyInstance(
                target.getClass().getClassLoader(),/*类加载器*/
                target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
                new InvocationHandler(){/*代理对象的方法最终都会被JVM导向它的invoke方法*/
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println(method.getName() + "方法开始执行...");
                        Object result = method.invoke(target, args);
                        System.out.println(result);
                        System.out.println(method.getName() + "方法执行结束...");
                        return result;
                    }
                }
        );
        return proxy;
    }
}

现在,我想应该能看懂动态代理了。

最后讨论一下代理对象是什么类型。

首先,请区分两个概念:代理 Class 对象和代理对象。

单从名字看,代理 Class 和 Calculator 的接口确实相去甚远,但是我们却能将代理对象赋值给接口类型:

千万别觉得名字奇怪,就怀疑它不能用接口接收,只要实现该接口就是该类型。

代理对象的本质就是:和目标对象实现相同接口的实例。代理 Class 可以叫任何名字,whatever,只要它实现某个接口,就能成为该接口类型。

我写了一个 MyProxy 类,那么它的 Class 名字必然叫 MyProxy。但这和能否赋值给接口没有任何关系。由于它实现了 Serializable 和 Collection,所以 myProxy(代理实例)同时是这两个接口的类型。

小结

我想了个很骚的比喻,希望能解释清楚:

接口 Class 对象是大内太监,里面的方法和字段比做他的一身武艺,但是他没有小 DD(构造器),所以不能 new 实例。一身武艺后继无人。

那怎么办呢?

正常途径(implements):

写一个类,实现该接口。这个就相当于大街上拉了一个人,认他做干爹。一身武艺传给他,只是比他干爹多了小 DD,可以 new 实例。

非正常途径(动态代理):

通过妙手圣医 Proxy 的克隆大法(Proxy.getProxyClass()),克隆一个 Class,但是有小 DD。所以这个克隆人 Class 可以创建实例,也就是代理对象。

代理 Class 其实就是附有构造器的接口 Class,一样的类结构信息,却能创建实例。

JDK 动态代理生成的实例

CGLib 动态代理生成的实例

如果说继承的父类是亲爹(只有一个),那么实现的接口是干爹(可以有多个)。

实现接口是一个类认干爹的过程。接口无法创建对象,但实现该接口的类可以。

比如

class Student extends Person implements A, B

这个类 new 一个实例出来,你问它:你爸爸是谁啊?它会告诉你:我只有一个爸爸 Person。

但是 student instanceof A interface,或者 student instanceof B interface,它会告诉你两个都是它干爹(true),都可以用来接收它。

然而,凡是有利必有弊。

也就是说,动态代理生成的代理对象,最终都可以用接口接收,和目标对象一起形成了多态,可以随意切换展示不同的功能。但是切换的同时,只能使用该接口定义的方法。


类加载器

初学者可能对诸如 “字节码文件”、Class 对象比较陌生。所以这里花一点点篇幅介绍一下类加载器的部分原理。如果我们要定义类加载器,需要继承 ClassLoader 类,并覆盖 findClass() 方法:

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
    try {
        /*自己另外写一个getClassData()
                  通过IO流从指定位置读取xxx.class文件得到字节数组*/
        byte[] datas = getClassData(name);
        if(datas == null) {
            throw new ClassNotFoundException("类没有找到:" + name);
        }
        //调用类加载器本身的defineClass()方法,由字节码得到Class对象
        return this.defineClass(name, datas, 0, datas.length);
    } catch (IOException e) {
        e.printStackTrace();
        throw new ClassNotFoundException("类找不到:" + name);
    }
}

所以,这就是类加载之所以能把 xxx.class 文件加载进内存,并创建对应 Class 对象的深层原因。


  TOC