Java 代理使用与原理

代理 指的是代表授权方执行处理事务。在编程中,通常是通过一个代理对象代表目标对象去执行方法,是对调用目标的一个包装。这样来保证目标对象方法的安全性、或者增强目标对象的方法功能。

Java 有 3 种代理方式:

静态代理

通过手动创建代理对象,来实现对目标对象的代理。在这过程中,一般存在 3 种对象:客户端、目标对象、代理对象。
客户端通过代理对象去调用目标对象的真实方法。
对象的接口

public interface IService {
    void request();
}

真正的目标对象

public class RealService implements IService {
    @Override
    public void request() {
        System.out.println("真正处理请求内容");
    }
}

代理类

public class ProxyService implements IService {
    private IService realService;

    public ProxyService(IService service) {
        this.realService = service;
    }

    @Override
    public void request() {
        // ...执行真实 request() 前其他逻辑
        System.out.println("request() 方法前逻辑");
        this.realService.request();
        System.out.println("request() 方法后逻辑");
        // ...执行真实 request() 后其他逻辑
    }
}

客户端实现

public class StaticProxyClient {
    public void callRequest() {
        // 目标对象
        IService realService = new RealService();
        // 代理对象
        IService proxyService = new ProxyService(realService);
        // 通过代理对象执行真正的逻辑,并且在代理对象中对请求进行增强
        proxyService.request();
    }
}

静态代理需要手动地为每个目标类创建代理类,而代理类中的逻辑都是类似的简单的,为了更简单地使用代理出现了动态代理,通过在运行期动态创建接口对象的代理,避免了手动去创建静态代理类。
动态代理是在运行时动态生成的,即编译完成后没有实际的 class 文件,而是在运行时动态生成类字节码,并加载到 JVM 中。

JDK 原生动态代理

JDK 提供了 Proxy.newProxyInstance() 来创建动态代理。实现步骤如下:

  • 创建被代理的目标类及它的接口
  • 创建接口 InvocationHandler 的实现类,它必须实现 invoke 方法
  • 通过 Proxy 的静态方法 newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 创建一个代理对象
  • 通过代理对象调用方法,代理的方法会被 InvocationHandler 的实现类接管
// 实现 InvocationHandler 类,它的 invoke() 方法会接管代理调用的方法
public class MyInvocationHandler implements InvocationHandler {
    private Object target;
    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理方法前逻辑");
        Object result = method.invoke(this.target, args);
        System.out.println("代理方法后逻辑");
        return result;
    }
}

// JDK 动态代理测试
public class JDKDynamicProxyClient {
    public static void main(String[] args) {
        RealService realService = new RealService();
        InvocationHandler handler = new MyInvocationHandler(realService);

        IService service = (IService) Proxy.newProxyInstance(
            // 通常是接口类的 ClassLoader
            IService.class.getClassLoader(),
            // 要实现的接口数组
            new Class[]{IService.class},
            // 代理用来处理调用方法的 InvocationHandler
            handler
        );

        // 通过代理类调用方法
        service.request();
    }
}

原理分析

主要是看 Proxy.newProxyInstance() 方法。

  • Proxy 类的静态变量加载时初始化 proxyClassCache,代理类缓存中初始化代理类的创建工厂为 ProxyClassFactory()
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
  • Proxy.newProxyInstance() 静态方法返回代理类对象,它的调用过程如下:
Proxy.newProxyInstance()  
      Proxy.getProxyClass0()
        ProxyClassFactory.get() #先从代理类缓存中查询,没有时从代理类工厂中创建
            ProxyClassFactory.apply()   #代理类工厂创建
                ProxyGenerator.generateProxyClass()  #代理类生成器,生成代理类代码
            ProxyClassFactory.defineClass0()    #加载生成的 class 文件到 jvm,java 接着就能调用了

最终生成的代理类关键代码如下:

// 代理类继承 Proxy 类,并实现 IService 接口
// 代理类名称固定位 "com.sun.proxy.$Proxy"+自增数字
public final class com.sun.proxy.$Proxy0 extends Proxy implements IService {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    // 构造函数中设置代理类的 InvocationHandler
    public com.sun.proxy.$Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    // ...

    public final void request() throws  {
        try {
            // 实际是 handler 中调用目标类的方法
            super.h.invoke(this, m3, (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"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            // 代理的方法
            m3 = Class.forName("proxy.IService").getMethod("request");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

当代理执行到 request() 方法时,实际执行的是 InvocationHandler 的 invoke() 方法,并在方法中通过反射调用目标类的真实方法。

JDK 动态代理要求被代理的对象必须有对应的一个或多个接口,不能给原类生成动态代理类。

CGLIB 动态代理

CGLIB 是一个强大的高性能代码生成包,可以在运行期转换字节码并生成目标类的子类。
CGLIB 创建代理类对象是通过 Enhancer 类实现的,创建代理的主要步骤:

  • 创建被代理的目标类
  • 创建 Enchaner 类对象,设置对象的父类(为被代理的目标类)及 Callback 对象,通过 Enchaner 类的 create() 方法创建代理类对象
  • 通过代理对象调用方法
// 被代理的类不需要实现接口,但是被代理的方法和类不能是 final
public class RequestService {
    public void request() {
        System.out.println("真正处理请求内容");
    }
}

// CGLIB 代理测试
public class CGLIBProxyClient {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(RequestService.class);
        // 这里是一个匿名类,被代理方法的处理类,需要实现 MethodInterceptor 接口的 intercept 方法
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects,
                MethodProxy methodProxy) throws Throwable {
                System.out.println("真实方法前逻辑...");
                methodProxy.invokeSuper(o, objects);
                System.out.println("真实方法后逻辑...");
                return null;
            }
        });
        RequestService requestService = (RequestService) enhancer.create();
        requestService.request();
    }
}

在项目中通过增加下面设置,可以将生成的动态类保存到代码的根目录

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./");

运行创建代理后,就可以在项目根目录中看到 enhancer.create 创建的动态类,它的关键代码如下:

// 继承目标类 RequestService
public class RequestService$$EnhancerByCGLIB$$6a2da525 extends RequestService implements Factory {
    // 重写了 request() 方法
    public final void request() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (this.CGLIB$CALLBACK_0 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        // enhancer.setCallback() 已经设置 callback,所以这里不为空
        if (var10000 != null) {
            // 调用 callback 中实现的 intercept() 方法
            var10000.intercept(this, CGLIB$request$0$Method, CGLIB$emptyArgs, CGLIB$request$0$Proxy);
        } else {
            super.request();
        }
    }
}

在 Callback 中通过 methodProxy.invokeSuper(o, objects); 来调用目标类的方法

public class MethodProxy {
    public Object invokeSuper(Object obj, Object[] args) throws Throwable {
        try {
            // 初始化代理类、目标类信息
            // fci.f2 为代理类; fci.i2 为代理类中的代理方法的方法下标
            init();
            FastClassInfo fci = fastClassInfo;
            // 代理类通过下标调用代理方法
            return fci.f2.invoke(fci.i2, obj, args);
        }
        catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }
}

自动生成的 FastClass 类主要代码如下:

// 继承 FastClass 类
public class RequestService$$FastClassByCGLIB$$86d56cc6 extends FastClass {

    // 根据方法签名的 hash 值映射方法的下标
    public int getIndex(String var1, Class[] var2) {
        switch(var1.hashCode()) {
        // ...
        case 1095692943:
            if (var1.equals("request")) {
                switch(var2.length) {
                case 0:
                    return 0;
                }
            }
        }

        return -1;
    }

    // 代理类实际调用的地方,通过代理方法的下标调用目标类方法(不是反射方式)
    public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
        RequestService var10000 = (RequestService)var2;
        int var10001 = var1;

        try {
            switch(var10001) {
            case 0:
                var10000.request();
                return null;
            case 1:
                return new Boolean(var10000.equals(var3[0]));
            case 2:
                return var10000.toString();
            case 3:
                return new Integer(var10000.hashCode());
            }
        } catch (Throwable var4) {
            throw new InvocationTargetException(var4);
        }

        throw new IllegalArgumentException("Cannot find matching method/constructor");
    }
}

最后

有问题,欢迎留言交流

参考内容

https://time.geekbang.org/column/article/10076
https://zhuanlan.zhihu.com/p/35144462

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容