JDK 动态代理踩坑

这篇文章围绕学习 JDK 动态代理时遇到的几个典型困惑展开,通过一组示例代码解释动态代理背后的运行机制。正文重点讨论了 Proxy.newProxyInstance() 中 ClassLoader 参数为什么看起来“随便填都能用”、为什么代理对象常常会触发 $Proxy0 cannot be cast to xxx 的类型转换错误,以及 InvocationHandler.invoke() 方法里 Object proxy 参数实际指向什么。文章通过逐个分析错误示例和代理类生成后的调用方式,把这些容易混淆的细节串起来,帮助理解 JDK 动态代理的正确用法。

最近阅读 Hadoop 的源码,看到各种动态代理,打算学习下。虽然以前也学过,但是感觉就是死记硬背的代码,有些地方根本不懂。温习了一遍网上 JDK 动态代理的教程后,自己尝试再一次死记硬背的写一写,结果错误百出。于是打算狠下心来系统的学习下动态代理。

在这篇博客中我不会具体的说明动态代理的实现,但是我会贴上我参考的博客供大家参考。

疑问

如果你和我一样,JAVA 基础不好,初次接触 JDK 动态代理,你一定会有下面的几个疑问。

我先贴上我的示例代码:

package org;

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

interface Person {
    void say();
}

class PersonImpl implements Person {
    public void say() {
        System.out.println("Hello World!");
    }
}

class CustomInvocation implements InvocationHandler {

    private Object target;

    public CustomInvocation(Object obj) {
        this.target = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // Q3
        System.out.println("before invoke");
        Object result =  method.invoke(target, args);
        System.out.println("after invoke");
        return result;
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Person person = new PersonImpl();
        // Q1
        Person proxyPerson1 = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), person.getClass().getInterfaces(), new CustomInvocation(person));

        Person proxyPerson2 = (Person) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), new CustomInvocation(person));

        Person proxyPerson3 = (Person) Proxy.newProxyInstance(Main.class.getClassLoader(), person.getClass().getInterfaces(), new CustomInvocation(person));
        // Q2
        PersonImpl proxyPerson4 = (PersonImpl) Proxy.newProxyInstance(Person.class.getClassLoader(), person.getClass().getInterfaces(), new CustomInvocation(person));

        Person proxyPerson5 = (PersonImpl) Proxy.newProxyInstance(Person.class.getClassLoader(), person.getClass().getInterfaces(), new CustomInvocation(person));

        Person proxyPerson6 = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), Person.class.getInterfaces(), new CustomInvocation(person));

        Person proxyPerson7 = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), PersonImpl.class.getInterfaces(), new CustomInvocation(person));

        Person proxyPerson8 = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{Person.class}, new CustomInvocation(person));
  }
}

Q1:ClassLoader 之谜,仿佛无论哪个 ClassLoader 都不报错?

Q2:$Proxy0 cannot be cast to xxxx ,我怎么动不动就报这个这个错!

Q3:InvocationHandler 中的 invoke 方法中的第一个参数 Object proxy 究竟是何方神圣?

Q1 到底填哪个ClassLoader ?

在理清应该填哪个 ClassLoader 之前,我们先理清 a.getClass()A.class 的区别。

  • a.getClass() 返回运行过程中 a 的 class 类型。比如你有 A a = new B(); 然后执行 a.getClass() 会返回 B
  • A.class 则是静态的认为是 A

例如下面这个函数判断一个类的包名是不是默认的,我们需要使用 .getClass() 进行判断。

boolean isDeclaredInDefaultPkg(Object o) {
    // 在这里,o 的类型是动态的,我不可能提前预知,所以使用 getClass()。
    return o.getClass().getPackage() == null;
}

再比如这个,我们明确的知道我们需要的类是 DayOfWeek.class,就可以这么写

Set<DayOfWeek> allDays = EnumSet.allOf(DayOfWeek.class);

参考来源:https://programming.guide/java/difference-between-class-and-getclass.html

回到 Q1,所以我们可以推断出,其实在这里,Person.classperson.getClass() 都是可以的。但是如果我们要写一个通用性的动态代理,肯定还是用 getClass() 的办法,毕竟我们不可能提前知道我们要动态代理哪一个类。

接下来我们看看 ClassLoader,到底应该是哪个类的 ClassLoader 呢?事实上,随便你哪个类,都可以,反正都是 AppClassLoader。不信你可以打印看看。

System.out.println(Person.class.getClassLoader());
System.out.println(person.getClass().getClassLoader());
System.out.println(Main.class.getClassLoader());

// 输出结果:
// sun.misc.Launcher$AppClassLoader@18b4aac2
// sun.misc.Launcher$AppClassLoader@18b4aac2
// sun.misc.Launcher$AppClassLoader@18b4aac2

至于为什么,推荐你去看看 https://blog.csdn.net/briblue/article/details/54973413

Q2 $Proxy0 cannot be cast to xxxx

这个问题就是我自己尝试写动态代理时最常遇到的问题。出现这个问题意味着你动态代理里面的操作姿势不对。

PersonImpl proxyPerson4 = (PersonImpl) Proxy.newProxyInstance(Person.class.getClassLoader(), person.getClass().getInterfaces(), new CustomInvocation(person));

Person proxyPerson5 = (PersonImpl) Proxy.newProxyInstance(Person.class.getClassLoader(), person.getClass().getInterfaces(), new CustomInvocation(person));

Person proxyPerson6 = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), Person.class.getInterfaces(), new CustomInvocation(person));

Person proxyPerson7 = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), PersonImpl.class.getInterfaces(), new CustomInvocation(person));

Person proxyPerson8 = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{Person.class}, new CustomInvocation(person));

上面所有的示例中,只有 proxyPerson7 和 proxyPerson8 是正确的。

proxyPerson4 错在我转型成了 PersonImpl,因为 JDK 动态代理构造的时候,是根据我传入的 interface 进行构造的,而不是 interface 的实现类,而 PersonImpl 是一个实现类。其实这个操作其实有点类似于把一个 interface 强制转换成一个他的实现类,这怎么可能呢?

proxyPerson5 犯得和 proxyPerson4 是一样的错误。

proxyPerson6 是错在 Proxy.newProxyInstance(xxx) 的第二个参数传入的是 Person 的 interface,问题是 Person 的 interface 是空的,Person 自己本身并没有实现哪一个接口,所以这里返回的是 null,所以构造出来的代理类肯定也是有问题的。

proxyPerson7 和 proxyPerson8 其实就是变了一下写法,两个实质上是一模一样的,

Q3 Invoke 方法中那个 Object proxy 是谁?

public Object invoke(Object proxy, Method method, Object[] args) ,据我们所知,如果要实现动态代理,我们需要实现自己的 InvocationHandler 类。在 InvocationHandler 中,我们需要重写原来的 invoke 方法。最初我有一个疑问,就是 invoke 方法中第一个参数 Object proxy 是谁,因为感觉在动态代理中根本用不到。

经过查找资料,实际上那个 Object proxy 就是代理后的类。下面给大家贴段代码:

public final void sayHello() throws {
  try {
    this.h.invoke(this, m3, null);
    return;
  } catch (RuntimeException localRuntimeException) {
    throw localRuntimeException;
  } catch (Throwable localThrowable) {
    throw new UndeclaredThrowableException(localThrowable);
  }
}

这段代码就是 JDK 动态代理生成后的类,sayHello() 就是被代理类中的一个方法,其中 this.h 就是我们自己实现的 InvocationHandler,我们看到第一个参数传递的是 this,也就是自己(通过动态代理生成后的类)。

更多具体信息可看这篇文章:https://blog.csdn.net/yhl_jxy/article/details/80586785

原创文章,作者:Smith,如若转载,请注明出处:https://www.inlighting.org/archives/jdk-dynamic-proxy

打赏 微信扫一扫 微信扫一扫
SmithSmith
上一篇 2020年1月30日 下午1:01
下一篇 2021年3月7日 下午1:37

相关推荐

  • Spring Boot2 + Spring Security + Thymeleaf 简单教程

    这篇文章以一个基于 Spring Boot 2、Spring Security 5 和 Thymeleaf 的 MVC 示例项目为主线,讲解了传统服务端页面场景下的权限控制实现。正文先介绍 Spring Security 的过滤器链和项目中的权限模型,再逐步配置表单登录、登出、remember-me、异常页面和方法级注解鉴权,同时补充了自定义权限注解、Thymeleaf 安全标签的使用方式,以及在 Controller 中获取当前登录用户信息的几种常见写法。

    2019年8月2日
    2.1K0
  • 有限状态机( Finite State Machine )JAVA 版

    这篇文章用空调开关、吹风和制冷三种状态的切换过程,介绍了有限状态机在 Java 中的基本实现方式。正文先展示了使用枚举加 if/else 判断状态流转的传统写法,再给出基于枚举常量内聚状态迁移逻辑的有限状态机实现,并通过同一组测试对比两种方案的效果,说明状态机写法在处理复杂流程控制时更清晰,也更便于维护。

    2020年1月30日
    2.3K0
  • Shiro + JWT + Spring Boot Restful 简易教程

    这篇文章给出了一个基于 Spring Boot、Shiro 和 JWT 的 Restful 鉴权教程,目标是在前后端分离场景下放弃 Session 和 Cookie,改用无状态的 Token 认证。正文从整体登录流程讲起,说明 token 的生成与校验方式,再逐步实现模拟用户数据源、统一返回结构、自定义异常、JWT 工具类、Shiro 的 Realm 与 Filter,以及注解鉴权和跨域支持配置,最后也点出了这套实现仍可继续改进的地方。

    2019年8月1日
    2.2K0

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注