obj instanceof UserService 为 true,obj.getClass() == UserServiceImpl.class 为 false,而 obj 明明就是那个 UserServiceImpl——只要它被 Spring 用 JDK 动态代理包过一层。同一段代码,换个代理方式结论又变了。
这三个机制在源码层面各写各的,在字节码与运行时层面却互相咬合:instanceof 是一条指令、反射是一次访问检查 + 转发调用、代理是一次运行期类生成。把路径拆开,很多"玄学"就落地了。
1. instanceof 是一条指令,不是一次方法调用
instanceof 有两条硬语义:左边为 null 时恒为 false;比较的是"运行期类型是否可赋值给目标类型",包含继承与接口实现关系。它在字节码里就是一条 instanceof 指令,后跟常量池里的类型引用。
Object o = "abc";
System.out.println(o instanceof String); // true
System.out.println(null instanceof String); // false,编译期就折叠成常量
// 编译期还会做"不可能成立"的检查
String s = "x";
// System.out.println(s instanceof Integer); // 编译错误:inconvertible types
$ javap -c Demo.class
aload_1
instanceof java/lang/String # 单条指令:查常量池类型,沿父类/接口链判定
ifeq ...
与 getClass() == 的区别是这类 bug 的高发区:
| 写法 | 判定依据 | 子类实例 | 代理对象 | null |
|---|---|---|---|---|
x instanceof Base |
可赋值性(含继承、接口) | true |
看代理实现的接口 | false |
x.getClass() == Base.class |
精确类型相等 | false |
false |
NPE |
Base.class.isInstance(x) |
与 instanceof 等价 |
true |
同左 | false |
Base.class.isAssignableFrom(x.getClass()) |
类型关系 | true |
同左 | NPE |
getClass() == 适合"必须精确类型"的场景(比如判断某个标记类),但只要链路上出现过代理、字节码增强、record 包装,它就会悄悄给 false。
JDK 16 起模式匹配 instanceof 正式可用(JEP 394),JDK 17 是它的第一个 LTS:
if (o instanceof Point p && p.x() > 0) { // 绑定变量的作用域由流程决定
System.out.println(p.y());
}
if (!(o instanceof Point p)) return; // 取反时没有绑定变量,需提前返回
System.out.println(p.x());
模式匹配只是省掉了 (Point) o 这次强制转换——字节码里依旧是 instanceof + checkcast(JIT 通常能把后者消掉),语义没变。
2. 反射:三条入口,三种开销来源
Class<?> clazz = Class.forName("com.example.OrderService"); // 会触发静态初始化
Method m = clazz.getDeclaredMethod("query", long.class); // 只找本类声明
Object result = m.invoke(instance, 1001L); // 参数装箱 + 访问检查
| 入口 | 是否初始化类 | 是否查父类 | 典型用途 |
|---|---|---|---|
Class.forName(name) |
是(initialize=true) |
— | 按配置加载驱动、插件 |
ClassLoader.loadClass(name) |
否 | — | 只判断类型是否存在 |
getDeclaredMethod |
— | 否,只本类 | 拿私有方法(需 setAccessible) |
getMethod |
— | 是,只找 public |
走接口/父类的公开方法 |
Method.invoke |
— | — | 每次都有装箱与访问检查 |
setAccessible(true) 在 JDK 9+ 不再是万能的,它要过模块这一关:目标包是否 opens 给调用方。JDK 16 起强封装默认开启、JDK 17 移除了 --illegal-access 开关,反射 java.base 内部包直接抛 InaccessibleObjectException:
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
// Unable to make field private final byte[] java.lang.String.value accessible
// 需启动参数:--add-opens java.base/java.lang=ALL-UNNAMED
JDK 18 的 JEP 416 把核心反射换到方法句柄上:Method.invoke 不再运行期生成 MethodAccessor 字节码,首次调用开销与预热曲线都更平。但**--illegal-access 的宽容模式在 JDK 17 就已移除**,升级后靠它续命的反射代码会直接失败,这是版本迁移最常见的炸点。
3. 代理:JDK 动态代理与 CGLIB 的两条路
JDK 动态代理基于接口,运行期生成 $Proxy0 类,所有方法调用转发给 InvocationHandler:
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class<?>[]{UserService.class},
(p, method, args) -> {
System.out.println("before " + method.getName());
return method.invoke(target, args); // 转发给目标
});
System.out.println(proxy.getClass().getName()); // 典型输出:jdk.proxy1.$Proxy0
System.out.println(proxy instanceof UserService); // true,实现了接口
System.out.println(proxy instanceof UserServiceImpl); // false,不继承实现类
CGLIB / ByteBuddy 走继承:生成目标类的子类并重写方法,所以代理不了 final 类与 final 方法,private 方法也拦不到。
| 维度 | JDK 动态代理 | CGLIB / ByteBuddy |
|---|---|---|
| 前提 | 目标实现接口 | 目标可被继承(非 final) |
| 生成物 | $Proxy0 implements 接口 |
Target$$EnhancerByCGLIB extends Target |
| 拦截范围 | 接口上声明的方法 | 可重写的实例方法 |
final 类/方法 |
不受影响(代理的是接口) | 无法代理 |
| 构造器 | 不调用目标构造器 | 需实例化子类(常用 Objenesis 绕过) |
| self-invocation | 不经过代理 | 同样不经过代理 |
Spring AOP 的选择依据是有没有接口,以及 proxyTargetClass:
| 条件 | 行为 |
|---|---|
| 目标实现至少一个接口 | JDK 代理(Spring Framework 默认),只能按接口注入 |
| 无接口 | CGLIB,可按实现类注入 |
proxyTargetClass = true |
强制 CGLIB |
| Spring Boot 2.x 起 | 默认 proxy-target-class=true,即 CGLIB |
final 类需代理 |
只能接口化或去 final |
getClass() 踩空就在这里:注入的是 $Proxy0 或 UserServiceImpl$$EnhancerBySpringCGLIB,getClass() == UserServiceImpl.class 必然为 false。要拿真实类型用 AopUtils.getTargetClass(bean) 或 AopProxyUtils.ultimateTargetClass(bean),别自己 getClass()。
同理,instanceof 也只在"代理实现了该接口"时成立:JDK 代理对接口成立、对实现类不成立;CGLIB 代理对实现类成立(子类),对接口也成立。
4. 反射为什么慢,以及方法句柄怎么救
Method.invoke 的开销集中在三处,与"反射是解释执行"这个说法并不完全一致:
| 开销来源 | 具体动作 | 能否消除 |
|---|---|---|
| 参数装箱 | invoke(Object, Object...) 要求把 int 装箱、把变参打成数组 |
用 MethodHandle 的精确签名可消除 |
| 访问检查 | 每次调用校验模块导出、成员可见性 | 检查结果会被缓存,但仍有分支 |
| 无法内联 | 调用目标对 JIT 不透明,热点路径难优化 | MethodHandle 可被内联 |
| 结果装箱 | 返回值统一为 Object,出参也要拆箱 |
同上 |
旧实现的"通胀"机制(调用超过阈值后生成 MethodAccessor 字节码)本身也是一次类生成成本;JEP 416 之后这条路径改由方法句柄承担,首次调用与稳态表现都更接近直接调用,但仍不等于直接调用——尤其参数装箱那部分,反射 API 的签名决定了它消除不掉。
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(OrderService.class, "query",
MethodType.methodType(Order.class, long.class));
Order o = (Order) mh.invokeExact(service, 1001L); // 签名精确,无装箱
invokeExact 要求调用点静态类型与 MethodType 完全一致,JIT 能把它编成近似直接调用。LambdaMetafactory 更进一步,把方法句柄"烧"成函数式接口实例:
MethodHandle mh = lookup.findVirtual(OrderService.class, "query",
MethodType.methodType(Order.class, long.class));
Function<Long, Order> fn = (Function<Long, Order>) LambdaMetafactory.metafactory(
lookup, "apply", MethodType.methodType(Function.class),
mh.type(), mh, mh.type()).getTarget().invokeExact();
Order o = fn.apply(1001L); // 稳态接近直接调用
这就是框架"启动时反射、运行期用句柄"的原因:一次性成本留在启动阶段,稳态走可内联路径。
5. 排错:五个反射与代理的现场
| 现象 | 根因 | 处理 |
|---|---|---|
InaccessibleObjectException |
目标包未 opens 给调用方 |
加 --add-opens m/p=ALL-UNNAMED,或改调公开 API |
IllegalArgumentException: object is not an instance of declaring class |
invoke 接收者类型不对 |
核对声明类与实例类型,静态方法传 null |
NoSuchMethodException |
用 getMethod 找私有方法 |
改 getDeclaredMethod 并逐层向上找 |
getClass() == X.class 为 false |
拿到的是代理对象 | 用 AopUtils.getTargetClass 取真实类型 |
CGLIB 代理下 final 方法不生效 |
无法重写 | 去 final,或改接口 + JDK 代理 |
还有一条容易漏:Class.newInstance() 在 JDK 9 已废弃,且会吞掉构造器抛出的受检异常;要拿私有构造器必须 getDeclaredConstructor() + setAccessible(true),否则抛 IllegalAccessException。
6. 边界:这些机制不该被用在哪
| 场景 | 更合适的做法 | 理由 |
|---|---|---|
| 业务代码里按类型分支 | 多态或 switch 模式匹配 |
反射分支无法被编译器检查 |
| 高频调用的插件入口 | 启动时绑定 MethodHandle / 函数式接口 |
每次 invoke 的装箱成本白付 |
| 需要拦截同类内部调用 | 重构拆分或自注入代理 | self-invocation 不走代理 |
代理 final 类 |
接口化 | 继承路线走不通 |
| 反射 JDK 内部字段 | 换公开 API | 强封装下必然失败 |
一条实践结论:instanceof 判接口是稳的,判实现类不稳——实现类随时会被代理替换,而接口是代理必然实现的契约。
7. 自测
null instanceof X的结果是什么?x.getClass() == X.class在x为null时又怎样?instanceof与Class.isInstance的判定依据是否一致?与getClass() ==差在哪?- JDK 16 的模式匹配
instanceof改变了语义吗?字节码里还会出现checkcast吗? Class.forName与ClassLoader.loadClass的关键差别是什么?各自适合什么场景?setAccessible(true)在 JDK 17 上为什么会失败?两种解决路径分别是什么?- JDK 动态代理与 CGLIB 的生成物分别是什么?为什么前者对实现类做
instanceof会失败?