跳到主要内容

反射

问题

什么是 Java 反射?有什么应用场景?JDK 动态代理和 CGLIB 代理有什么区别?

答案

反射基础

反射(Reflection)允许在运行时检查和操作类的结构(字段、方法、构造器等),是 Java 框架(Spring、MyBatis、Jackson)的基石。

获取 Class 对象

ReflectionBasic.java
// 方式一:类名.class(编译期确定)
Class<String> clazz1 = String.class;

// 方式二:对象.getClass()(运行时获取)
String s = "hello";
Class<?> clazz2 = s.getClass();

// 方式三:Class.forName()(动态加载,最常用于框架)
Class<?> clazz3 = Class.forName("java.lang.String");

// 三者返回的是同一个 Class 对象
System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true

常用反射操作

ReflectionOps.java
Class<?> clazz = User.class;

// 获取并调用构造器
Constructor<?> ctor = clazz.getDeclaredConstructor(String.class, int.class);
ctor.setAccessible(true); // 访问私有构造器
Object user = ctor.newInstance("Alice", 25);

// 获取并访问字段
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); // 访问私有字段
String name = (String) nameField.get(user);
nameField.set(user, "Bob"); // 修改字段值

// 获取并调用方法
Method method = clazz.getDeclaredMethod("sayHello", String.class);
method.setAccessible(true);
Object result = method.invoke(user, "World");
方法说明
getFields()获取所有 public 字段(含继承)
getDeclaredFields()获取本类声明的所有字段(含 private,不含继承)
getMethods()获取所有 public 方法(含继承)
getDeclaredMethods()获取本类声明的所有方法(含 private,不含继承)

动态代理

JDK 动态代理

基于接口,运行时生成代理类:

JdkProxyDemo.java
// 目标接口
public interface UserService {
String findUser(Long id);
}

// 目标实现
public class UserServiceImpl implements UserService {
public String findUser(Long id) {
return "User-" + id;
}
}

// InvocationHandler:定义代理逻辑
public class LogHandler implements InvocationHandler {
private final Object target; // 被代理的目标对象

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

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before: " + method.getName());
Object result = method.invoke(target, args); // 调用目标方法
System.out.println("After: " + method.getName());
return result;
}
}

// 创建代理对象
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
target.getClass().getInterfaces(), // 代理的接口
new LogHandler(target) // 调用处理器
);
proxy.findUser(1L); // Before: findUser → User-1 → After: findUser

CGLIB 代理

基于继承(子类),不需要接口:

CglibProxyDemo.java
// 目标类(没有实现接口)
public class OrderService {
public String createOrder(String item) {
return "Order-" + item;
}
}

// MethodInterceptor:定义代理逻辑
public class LogInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("Before: " + method.getName());
// 调用父类(原始)方法,比 method.invoke() 更快
Object result = proxy.invokeSuper(obj, args);
System.out.println("After: " + method.getName());
return result;
}
}

// 创建代理对象
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderService.class);
enhancer.setCallback(new LogInterceptor());
OrderService proxy = (OrderService) enhancer.create();
proxy.createOrder("iPhone");

JDK 代理 vs CGLIB 代理

维度JDK 动态代理CGLIB 代理
原理基于接口,运行时生成实现类基于继承,运行时生成子类
要求目标类必须实现接口不需要接口,但不能代理 final 类/方法
性能(创建)较快较慢(生成子类字节码)
性能(调用)JDK 8+ 优化后接近略快(FastClass 机制)
依赖JDK 内置需要引入 cglib 库
Spring 中目标实现接口时默认使用目标没有接口时使用
Spring 的代理策略
  • Spring AOP 默认:目标实现接口 → JDK 代理;无接口 → CGLIB 代理
  • Spring Boot 2.x+ 默认:统一使用 CGLIB 代理(spring.aop.proxy-target-class=true),避免因代理类型不同导致的注入问题

常见面试问题

Q1: 反射的应用场景有哪些?

答案

  1. Spring IoC:通过反射创建 Bean 实例、注入依赖
  2. Spring AOP:通过动态代理实现切面编程
  3. ORM 框架:MyBatis/Hibernate 将数据库结果映射到实体类字段
  4. JSON 序列化:Jackson/Gson 通过反射读写对象字段
  5. JUnit/TestNG:通过反射调用测试方法
  6. 注解处理:运行时读取注解信息

Q2: 反射的性能问题如何优化?

答案

反射调用比直接调用慢约 5-10 倍(取决于 JVM 优化),优化手段:

  1. 缓存 Method/Field 对象:避免重复查找
  2. setAccessible(true):跳过访问检查,提升约 4 倍
  3. MethodHandle(JDK 7+):接近直接调用的性能
  4. 代码生成:如 CGLIB 的 FastClass,或用 ASM/Javassist 直接生成字节码

Q3: 反射能访问私有成员吗?

答案

可以。通过 setAccessible(true) 可以绕过 Java 的访问控制检查,访问 private 字段和方法。但在 JDK 9+ 的模块系统(JPMS)中,跨模块的反射访问会受到限制,需要通过 --add-opens 参数显式开放。

Q4: Class.forName() 和 ClassLoader.loadClass() 的区别?

答案

维度Class.forName(name)classLoader.loadClass(name)
类初始化默认执行静态代码块不会初始化类
使用场景JDBC 加载驱动延迟加载,只在使用时初始化
// Class.forName 会触发 static {} 块
Class.forName("com.mysql.cj.jdbc.Driver");

// 也可以控制是否初始化
Class.forName("com.example.MyClass", false, classLoader); // false = 不初始化

Q5: 为什么 Spring AOP 需要动态代理?

答案

Spring AOP 需要在不修改原始代码的情况下给方法添加额外逻辑(日志、事务、权限等)。动态代理可以在运行时拦截方法调用,在目标方法执行前后插入切面逻辑:

// @Transactional 的底层实现(简化)
// 代理对象拦截方法调用 → 开启事务 → 调用原始方法 → 提交/回滚事务

这就是为什么在同一个类中调用 @Transactional 方法时事务不会生效——因为没有经过代理对象。

相关链接