Java 动态代理原理入门与面试
目录
五、CGLIB 动态代理(Spring AOP 底层用的就是这个)
八、MyBatis + Spring AOP 的动态代理对比
Q5:MyBatis 的 Mapper 为什么用 JDK 动态代理?
一、什么是代理
代理 = 中介 / 帮你办事的人
你不想亲自做一件事,找个人帮你做,但你还是能控制他做什么、怎么做。
你(目标对象)→ 代理(中介)→ 实际办事 ↑ 你可以在这里加额外操作(记录日志、权限校验、事务管理)
二、静态代理 vs 动态代理
2.1 静态代理(编译时就确定代理谁)
// 1. 定义接口
public interface UserService {
void saveUser(String name);
void deleteUser(Long id);
}
// 2. 真正的实现类
public class UserServiceImpl implements UserService {
@Override
public void saveUser(String name) {
System.out.println("保存用户:" + name);
}
@Override
public void deleteUser(Long id) {
System.out.println("删除用户:" + id);
}
}
// 3. 代理类(需要手动写,代理谁就写谁的代理)
public class UserServiceProxy implements UserService {
private UserService target; // 持有真正的实现类
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void saveUser(String name) {
System.out.println("【代理】开始执行");
long start = System.currentTimeMillis();
target.saveUser(name); // 调用真正的实现
long cost = System.currentTimeMillis() - start;
System.out.println("【代理】执行完成,耗时 " + cost + "ms");
}
@Override
public void deleteUser(Long id) {
System.out.println("【代理】开始执行");
target.deleteUser(id);
System.out.println("【代理】执行完成");
}
}
// 4. 使用
public class Main {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.saveUser("张三");
}
}
静态代理的问题:
有 10 个接口 → 要写 10 个代理类 有 100 个接口 → 要写 100 个代理类 太累了
动态代理解决这个问题:不需要手写代理类,运行时自动生成。
三、动态代理的两种实现
| JDK 动态代理 | CGLIB 动态代理 | |
|---|---|---|
| 要求 | 目标类必须实现接口 | 不需要接口,只要不是 final 类 |
| 原理 | 基于 java.lang.reflect.Proxy |
基于字节码生成子类(ASM) |
| 性能 | 反射调用,稍慢 | 字节码生成,稍快 |
| Spring 默认 | 目标类有接口时用 JDK 代理 | 目标类没接口时用 CGLIB |
| 限制 | 只能代理接口方法 | 不能代理 final 类/方法 |
Spring Boot 2.x 默认全部用 CGLIB(即使有接口也用 CGLIB,因为性能更好)。
四、JDK 动态代理(MyBatis 用的就是这个)
4.1 核心类
java.lang.reflect.Proxy(创建代理对象) java.lang.reflect.InvocationHandler(拦截调用的处理器)
4.2 手写一个 JDK 动态代理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkProxyDemo {
public static void main(String[] args) {
// 1. 创建目标对象
UserService target = new UserServiceImpl();
// 2. 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
target.getClass().getInterfaces(), // 要代理的接口
new MyInvocationHandler(target) // 调用处理器
);
// 3. 调用代理对象的方法
proxy.saveUser("张三");
proxy.deleteUser(1L);
}
}
// 3. 定义调用处理器(核心!拦截所有方法调用)
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 {
// --- 代理逻辑开始 ---
String methodName = method.getName();
System.out.println("【代理】方法开始:" + methodName);
long start = System.currentTimeMillis();
// --- 代理逻辑结束 ---
// 调用真正的目标方法
Object result = method.invoke(target, args);
// --- 代理逻辑开始 ---
long cost = System.currentTimeMillis() - start;
System.out.println("【代理】方法结束:" + methodName + ",耗时 " + cost + "ms");
// --- 代理逻辑结束 ---
return result;
}
}
运行结果:
【代理】方法开始:saveUser 保存用户:张三 【代理】方法结束:saveUser,耗时 0ms 【代理】方法开始:deleteUser 删除用户:1 【代理】方法结束:deleteUser,耗时 0ms
4.3 每一步发生了什么
Proxy.newProxyInstance() 做了什么?
│
▼
① 根据 ClassLoader + Interface[] 动态生成一个新类
│ 这个类实现了 UserService 接口
│ 但它的方法实现全部委托给 InvocationHandler
│
▼
② 生成的类大致长这样(你不需要写,JVM 自动帮你生成):
│
│ class $Proxy0 implements UserService {
│ InvocationHandler h;
│
│ public void saveUser(String name) {
│ // 通过反射拿到方法对象
│ Method m = UserService.class.getMethod("saveUser", String.class);
│ // 交给 InvocationHandler 处理
│ h.invoke(this, m, new Object[]{name});
│ }
│
│ public void deleteUser(Long id) {
│ Method m = UserService.class.getMethod("deleteUser", Long.class);
│ h.invoke(this, m, new Object[]{id});
│ }
│ }
│
▼
③ new $Proxy0() → 返回代理对象
4.4 为什么 MyBatis 用的是 JDK 动态代理
// MyBatis 的 Mapper 是接口
public interface UserMapper {
User selectById(Long id);
}
// JDK 动态代理要求:目标必须实现接口 ✅
// Mapper 恰好是接口 → 天然适合 JDK 动态代理
// MyBatis 内部大致是这样的
Object mapperProxy = Proxy.newProxyInstance(
UserMapper.class.getClassLoader(),
new Class[]{UserMapper.class},
new MapperProxy(sqlSession) // 拦截器
);
// 拿到的 mapperProxy 就是你可以直接用的 "Mapper 实现"
五、CGLIB 动态代理(Spring AOP 底层用的就是这个)
5.1 核心原理
不需要接口,通过 ASM 字节码框架生成目标类的子类,重写父类方法来实现拦截。
目标类:UserServiceImpl(普通类,没实现接口)
│
▼
CGLIB 生成子类:UserServiceImpl$$EnhancerBySpringCGLIB
│ 这个子类继承了 UserServiceImpl
│ 重写了所有非 final 的 public 方法
│
▼
调用子类的方法时 → 拦截 → 转发给 MethodInterceptor
5.2 手写一个 CGLIB 代理
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxyDemo {
public static void main(String[] args) {
// 1. 创建 CGLIB 代理
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class); // 父类是谁
enhancer.setCallback(new MyMethodInterceptor()); // 拦截器
// 2. 创建代理对象(子类)
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
// 3. 调用
proxy.saveUser("张三");
}
}
// 拦截器(类似 JDK 的 InvocationHandler)
class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
System.out.println("【CGLIB 代理】方法开始:" + method.getName());
long start = System.currentTimeMillis();
// 调用父类的方法(不是 method.invoke)
Object result = methodProxy.invokeSuper(obj, args);
long cost = System.currentTimeMillis() - start;
System.out.println("【CGLIB 代理】方法结束:" + method.getName() + ",耗时 " + cost + "ms");
return result;
}
}
注意:CGLIB 调用父类方法用 methodProxy.invokeSuper(obj, args),而不是 method.invoke()(那会死循环调自己)。
六、JDK 代理 vs CGLIB 代理的代码对比
// ========== JDK 动态代理 ==========
// 1. 目标必须有接口
public interface UserService { void save(String name); }
public class UserServiceImpl implements UserService { ... }
// 2. 用 Proxy.newProxyInstance
Object proxy = Proxy.newProxyInstance(
classLoader,
new Class[]{UserService.class}, // 接口
new InvocationHandler() {
public Object invoke(Object p, Method m, Object[] args) {
// 拦截逻辑
return m.invoke(target, args); // 调目标方法
}
}
);
// ========== CGLIB 动态代理 ==========
// 1. 目标可以是普通类(不需要接口)
public class UserService { public void save(String name) { ... } }
// 2. 用 Enhancer
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class); // 父类
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method m, Object[] args, MethodProxy mp) {
// 拦截逻辑
return mp.invokeSuper(obj, args); // 调父类方法
}
});
Object proxy = enhancer.create();
七、Spring AOP 和动态代理的关系
Spring AOP 的底层就是动态代理。
@Service
public class UserService {
@Transactional // Spring AOP 拦截这个方法
public void saveUser(String name) {
// 你的业务代码
userMapper.insert(name);
}
}
Spring 怎么拦截的?
Spring 启动
│
▼
发现 UserService 有 @Transactional
│
▼
用动态代理包装 UserService
│ 有接口 → JDK 动态代理(或 CGLIB,看配置)
│ 没接口 → CGLIB 动态代理
│
▼
你注入的 UserService 是代理对象
│
▼
调用 saveUser() 时
│ 代理拦截 → 开启事务 → 执行你的方法 → 提交/回滚事务
// 你写的代码(看起来像直接调)
userService.saveUser("张三");
// 实际执行的(代理拦截后)
@Transactional 开启
→ userService.saveUser("张三") // 真正的业务方法
@Transactional 提交
所以 Spring AOP = 动态代理 + 切面逻辑。
八、MyBatis + Spring AOP 的动态代理对比
| MyBatis Mapper | Spring AOP | |
|---|---|---|
| 代理谁 | Mapper 接口 | Service/Controller 等 Bean |
| 代理方式 | JDK 动态代理(Mapper 是接口) | 有接口用 JDK,没接口用 CGLIB |
| 拦截器 | MapperProxy(找 SQL、执行) | TransactionInterceptor(事务管理)等 |
| 用途 | 把接口调用转成 SQL 执行 | 日志、事务、权限、缓存等切面功能 |
| 创建时机 | Spring 启动时 | Spring 启动时 |
| 拦截时机 | 每次调用 Mapper 方法时 | 每次调用 Bean 方法时 |
九、面试话术汇总
Q1:什么是动态代理?和静态代理的区别?
话术:动态代理不需要手写代理类,在运行时通过反射或字节码技术自动生成。静态代理每个目标类都要写一个代理类,10 个接口写 10 个,维护成本高。动态代理只需要一个处理器,所有方法调用都走同一个处理器,通用性强。JDK 动态代理基于反射,CGLIB 基于字节码生成子类。
Q2:JDK 动态代理的原理?
话术:JDK 动态代理通过 Proxy.newProxyInstance() 方法,传入目标类的接口和一个 InvocationHandler 处理器,JVM 在运行时动态生成一个实现了目标接口的代理类。调用代理对象的方法时,所有调用都会被路由到 InvocationHandler 的 invoke() 方法,在这里可以插入额外逻辑,再通过反射调用目标对象的真正方法。
Q3:CGLIB 动态代理的原理?
话术:CGLIB 通过 ASM 字节码框架,运行时生成目标类的子类,重写所有非 final 的 public 方法。调用子类方法时,拦截交给 MethodInterceptor,再调用父类的原始方法。因为是生成子类,所以不需要接口,但不能代理 final 类和 final 方法。Spring Boot 2.x 默认全部用 CGLIB,即使有接口也用,因为性能比 JDK 反射调用更好。
Q4:Spring 默认用哪种动态代理?
话术:Spring Boot 2.x 之前,默认规则是目标类有接口用 JDK 动态代理,没有接口用 CGLIB。Spring Boot 2.x 之后,默认全部用 CGLIB,因为 CGLIB 性能更好。可以通过 spring.aop.proxy-target-class=false 强制用 JDK 代理。
Q5:MyBatis 的 Mapper 为什么用 JDK 动态代理?
话术:因为 Mapper 是接口,JDK 动态代理要求目标必须有接口,正好匹配。MyBatis 用 Proxy.newProxyInstance() 创建代理对象,InvocationHandler 是 MapperProxy,里面做了三件事:根据方法名找 SQL、通过 SqlSession 执行、结果映射成 Java 对象。
Q6:动态代理有什么局限性?
话术:两个局限。JDK 动态代理只能代理接口方法,不能代理非接口方法;CGLIB 不能代理 final 类和 final 方法(因为是生成子类,final 不能被重写)。性能上,JDK 代理通过反射调用,CGLIB 通过字节码生成方法直接调用,CGLIB 首次生成稍慢但后续调用更快。另外动态代理会增加调试难度,因为堆栈里多了一层代理类。
十、一张图看完整个知识体系
动态代理
├── 静态代理(手写代理类,不灵活)
│
├── JDK 动态代理(基于接口)
│ ├── Proxy.newProxyInstance()
│ ├── InvocationHandler.invoke()
│ └── 应用:MyBatis Mapper、Spring AOP(有接口时)
│
└── CGLIB 动态代理(基于子类)
├── Enhancer + MethodInterceptor
├── ASM 字节码生成子类
└── 应用:Spring AOP(无接口时 / Spring Boot 2.x 全部用)
十一、一句话速记
代理 = 中介,帮你干活还能加额外操作 JDK 代理 = 基于接口,Proxy + InvocationHandler,反射调用 CGLIB 代理 = 基于子类,Enhancer + MethodInterceptor,字节码生成 MyBatis 用 JDK 代理(Mapper 是接口) Spring Boot 2.x 默认全用 CGLIB(性能更好) 动态代理 = Spring AOP 的底层原理
更多推荐
所有评论(0)