反射
问题
什么是 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: 反射的应用场景有哪些?
答案:
- Spring IoC:通过反射创建 Bean 实例、注入依赖
- Spring AOP:通过动态代理实现切面编程
- ORM 框架:MyBatis/Hibernate 将数据库结果映射到实体类字段
- JSON 序列化:Jackson/Gson 通过反射读写对象字段
- JUnit/TestNG:通过反射调用测试方法
- 注解处理:运行时读取注解信息
Q2: 反射的性能问题如何优化?
答案:
反射调用比直接调用慢约 5-10 倍(取决于 JVM 优化),优化手段:
- 缓存 Method/Field 对象:避免重复查找
- setAccessible(true):跳过访问检查,提升约 4 倍
- MethodHandle(JDK 7+):接近直接调用的性能
- 代码生成:如 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 方法时事务不会生效——因为没有经过代理对象。