Java 动态代理


有一个设计模式是代理模式,常见的实现都是为一个类特地编写一个代理类,这种代理叫做静态代理。

而动态代理中,代理对象是在运行时候生成的,可以针对多种不同的类生成代理。

本文代码示例地址:https://github.com/letiantian/demo/tree/master/java-dynamic-proxy-demo

动态代理入门

示例代码包结构:

demo01
├── CustomInvocationHandler.java
├── HelloImpl.java
├── IHello.java
└── Main.java

定义接口 IHello:

package proxy01;

public interface IHello {

    void hello();
    void hi();

}

增加接口 IHello 的实现类:

package proxy01;

public class HelloImpl implements IHello {
    @Override
    public void hello() {
        System.out.println("hello");
    }

    @Override
    public void hi() {
        System.out.println("hi");
    }
}

自定义 InvocationHandler:

package proxy01;


import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class CustomInvocationHandler implements InvocationHandler {

    private Object target;

    public CustomInvocationHandler(Object target) {
        this.target=target;
    }

    /**
     * 被代理的类,在执行方法时,会经过这里
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(String.format("proxy类名: %s", proxy.getClass().getCanonicalName()));
        System.out.println(String.format("调用类: %s, 调用方法: %s", target.getClass().getCanonicalName(), method.getName()));
        return method.invoke(target,args);
    }
}

生成代理对象,并测试效果:

package proxy01;

import org.junit.Test;

import java.lang.reflect.Proxy;

public class Main {

    public static void main(String[] args) {
        HelloImpl helloImpl = new HelloImpl();  // 要被代理的对象
        ClassLoader classLoader = HelloImpl.class.getClassLoader();  // 类加载
        Class<?>[] interfaces = new Class[] { IHello.class };        // 接口

        // 用自定义 InvocationHandler 包装 helloImpl
        CustomInvocationHandler handler = new CustomInvocationHandler(helloImpl);  

        // 生成代理对象
        IHello hello = (IHello) Proxy.newProxyInstance(classLoader, interfaces, handler); 
        hello.hello();
    }

}

运行结果:

proxy类名: com.sun.proxy.$Proxy0
调用类: demo01.HelloImpl, 调用方法: hello
hello

关于类名com.sun.proxy.$Proxy0: 因为代理类是动态生成的,所以特地用了一个独有的包com.sun.proxy,类名用$开头,后缀是数字。如果有多个动态代理类产生,会发现数字是不同的。

Java 动态代理是基于接口的

Java 动态代理是基于接口的 ,所以对Proxy.newProxyInstance生成的对象,只能用接口进行类型装换,也就是:

IHello hello = (IHello) Proxy.newProxyInstance(classLoader, interfaces, handler); 

下面这种方式是错误的:

// 错误的用法,运行时会报错
HelloImpl hello = (HelloImpl) Proxy.newProxyInstance(classLoader, interfaces, handler);

动态代理长什么样子?

要看长什么样子,需要两步。

第一步,将动态生成的代理类字节码写入文件:

import sun.misc.ProxyGenerator;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Proxy;

public class Main {

    public static void main(String[] args) {
        HelloImpl helloImpl = new HelloImpl();
        ClassLoader classLoader = HelloImpl.class.getClassLoader();
        Class<?>[] interfaces = new Class[] { IHello.class };
        CustomInvocationHandler handler = new CustomInvocationHandler(helloImpl);

        Object proxy = Proxy.newProxyInstance(classLoader, interfaces, handler);
        addClassToDisk(proxy.getClass().getName(), HelloImpl.class,"/Users/letian/Proxy.class");
    }


    /**
     * 这个代码来自:https://www.jianshu.com/p/e2917b0b9614 ,作用是将类的字节码写入指定path对应的文件
     */
    private static void addClassToDisk(String className, Class<?> cl, String path) {
        //用于生产代理对象的字节码
        byte[] classFile = ProxyGenerator.generateProxyClass(className, cl.getInterfaces());
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(path);
            //将代理对象的class字节码写到硬盘上
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out!=null) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

我用的macOS,家目录是/Users/letian,所以写到了这个目录下。

第2步,反编译生成的Proxy.class文件。使用 Intellij IDEA 打开该文件即可,反编译的结果如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.sun.proxy;

import demo02.IHello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements IHello {
    private static Method m1;
    private static Method m4;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void hi() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void hello() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m4 = Class.forName("demo02.IHello").getMethod("hi");
            m3 = Class.forName("demo02.IHello").getMethod("hello");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

本质上,和我们常见的静态代理相同。

动态代理的好处:编写一次,到处使用

代码见 demo03 。

我们把 CustomInvocationHandler 改造的更通用些:

public class CustomInvocationHandler implements InvocationHandler {

    private Object target;
    public CustomInvocationHandler(Object target) {
        this.target=target;
    }

    // 增加了生成代理的通用函数
    public static Object getProxy(Object target) {
        ClassLoader classLoader = target.getClass().getClassLoader(); // 获取被代理类的类加载器
        Class<?>[] interfaces = target.getClass().getInterfaces();    // 获取被代理类的接口
        CustomInvocationHandler handler = new CustomInvocationHandler(target);  // 生成 InvocationHandler
        return Proxy.newProxyInstance(classLoader, interfaces, handler);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(String.format("proxy类名: %s", proxy.getClass().getCanonicalName()));
        System.out.println(String.format("调用类: %s, 调用方法: %s", target.getClass().getCanonicalName(), method.getName()));
        return method.invoke(target,args);
    }
}

使用方法如下:

public class Main {

    public static void main(String[] args) {
        HelloImpl helloImpl = new HelloImpl();
        IHello hello = (IHello) CustomInvocationHandler.getProxy(helloImpl);
        hello.hello();
    }

}

执行 Main 类,会输出:

proxy类名: com.sun.proxy.$Proxy0
调用类: demo03.HelloImpl, 调用方法: hello
hello

为什么说可以到处使用? 只要类是基于接口实现,便可以使用基于上面的基于 CustomInvocationHandler 的动态代理。

被代理对象的自调用

动态代理的代理粒度是类,所以执行被代理对象的某个方法A时,若方法内部调用了另一个方法B,方法B的执行是不经过代理的。

看下示例(demo04):

稍微修改被代理对象:

public class HelloImpl implements IHello {
    @Override
    public void hello() {
        System.out.println("hello");
        hi();  // 这里调用了另一个 hi 函数
    }

    @Override
    public void hi() {
        System.out.println("hi");
    }
}

测试效果:

public class Main {
    public static void main(String[] args) {
        HelloImpl helloImpl = new HelloImpl();
        IHello hello = (IHello) CustomInvocationHandler.getProxy(helloImpl);
        hello.hello();
        hello.hi();
    }
}

执行后,输出:

proxy类名: com.sun.proxy.$Proxy0
调用类: demo04.HelloImpl, 调用方法: hello
hello
hi
proxy类名: com.sun.proxy.$Proxy0
调用类: demo04.HelloImpl, 调用方法: hi
hi

其中执行 hi 方法的结果是:

proxy类名: com.sun.proxy.$Proxy0
调用类: demo04.HelloImpl, 调用方法: hi
hi

执行 hello 方法的结果是:

proxy类名: com.sun.proxy.$Proxy0
调用类: demo04.HelloImpl, 调用方法: hello
hello
hi

可以看到,执行 hello 方法时,内部执行了 hi方法,hi方法未经过代理。

如果被代理对象有多个接口

代码示例在 demo05 中:

$ tree demo05
demo05
├── CustomInvocationHandler.java
├── HelloAndHelloWorldImpl.java
├── IHello.java
├── IHelloWorld.java
└── Main.java

除了 IHello 接口:

package demo05;

public interface IHello {

    void hello();
    void hi();

}

我们增加了一个 IHelloWorld 接口:

package demo05;

public interface IHelloWorld {
    void helloWorld();
}

HelloAndHelloWorldImpl 实现了这两个接口:

package demo05;

public class HelloAndHelloWorldImpl implements IHello, IHelloWorld {
    @Override
    public void hello() {
        System.out.println("hello");
    }

    @Override
    public void hi() {
        System.out.println("hi");
    }

    @Override
    public void helloWorld() {
        System.out.println("hello world");
    }
}

在动态代理中,调用一个接口的方法前,对代理对象类型转换即可:

package demo05;

public class Main {
    public static void main(String[] args) {
        HelloAndHelloWorldImpl helloImpl = new HelloAndHelloWorldImpl();
        Object proxy = CustomInvocationHandler.getProxy(helloImpl);

        // 调用 IHello 接口的方法
        IHello hello = (IHello) proxy;
        hello.hello();

        // 调用 IHelloWorld 接口的方法
        IHelloWorld helloWorld = (IHelloWorld) proxy;
        helloWorld.helloWorld();
    }
}

运行结果如下:

proxy类名: com.sun.proxy.$Proxy0
调用类: demo05.HelloAndHelloWorldImpl, 调用方法: hello
hello
proxy类名: com.sun.proxy.$Proxy0
调用类: demo05.HelloAndHelloWorldImpl, 调用方法: helloWorld
hello world


(本文完)


Java 教程