一文讲透 Java 代理模式:从静态代理、JDK 动态代理到 Spring AOP

一文讲透 Java 代理模式:从静态代理、JDK 动态代理到 Spring AOP

代理模式是 Java 中非常重要的设计模式,也是理解 Spring AOP、Spring 事务、MyBatis Mapper 代理、RPC 远程调用等技术的基础。

很多人第一次看代理模式时,会觉得它很简单:不就是在真实对象外面包了一层吗?

这个理解没有错,但还不够深入。

代理模式真正重要的地方在于:它让我们可以在不修改原始业务代码的情况下,对方法调用过程进行增强。

比如原来的业务方法只负责保存用户:

1
2
3
public void save(User user) {
System.out.println("保存用户:" + user);
}

现在我们想在保存用户之前打印日志,在保存用户之后统计耗时。如果直接修改业务代码,当然可以实现,但这样日志、耗时统计、事务、权限等非核心逻辑就会和业务代码混在一起。

代理模式解决的就是这个问题。

它的核心调用链是:

1
调用方 -> 代理对象 -> 真实对象

调用方并不直接访问真实对象,而是先访问代理对象。代理对象可以在调用真实对象之前、之后,甚至异常时,插入额外逻辑。


代理模式到底解决了什么问题?

先看一个最普通的业务接口:

1
2
3
public interface UserService {
void save(User user);
}

真实业务类:

1
2
3
4
5
6
7
public class UserServiceImpl implements UserService {

@Override
public void save(User user) {
System.out.println("保存用户:" + user);
}
}

如果没有代理,调用方式很直接:

1
2
UserService userService = new UserServiceImpl();
userService.save(new User());

调用链就是:

1
调用方 -> UserServiceImpl.save()

这时候如果需要加日志,最直接的做法是改业务类:

1
2
3
4
5
6
7
8
9
10
11
public class UserServiceImpl implements UserService {

@Override
public void save(User user) {
System.out.println("开始调用 save");

System.out.println("保存用户:" + user);

System.out.println("调用结束");
}
}

这段代码虽然能跑,但问题很明显。

UserServiceImpl 的核心职责应该是保存用户,现在却混入了日志逻辑。以后如果还要加事务、权限校验、接口限流、耗时统计,业务方法会越来越臃肿。

更麻烦的是,如果 OrderServiceProductServicePaymentService 也都要加日志,那么每个业务类都要手动改一遍。

这就违反了一个很重要的原则:

1
核心业务逻辑和通用增强逻辑不应该强耦合在一起。

代理模式的价值就在这里。

它不直接修改真实对象,而是在真实对象外面包一层代理对象。

真实对象只负责核心业务。

代理对象负责日志、事务、权限、缓存等增强逻辑。


静态代理:最容易理解的代理模式

静态代理就是我们自己手动写一个代理类。

代理类和真实类实现同一个接口。

真实对象:

1
2
3
4
5
6
7
public class UserServiceImpl implements UserService {

@Override
public void save(User user) {
System.out.println("保存用户:" + user);
}
}

代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UserServiceProxy implements UserService {

private final UserService target;

public UserServiceProxy(UserService target) {
this.target = target;
}

@Override
public void save(User user) {
long start = System.currentTimeMillis();

System.out.println("开始调用 save");

target.save(user);

long end = System.currentTimeMillis();

System.out.println("调用结束,耗时:" + (end - start) + "ms");
}
}

使用方式:

1
2
3
4
5
UserService target = new UserServiceImpl();

UserService proxy = new UserServiceProxy(target);

proxy.save(new User());

这段代码里面最重要的是:

1
private final UserService target;

target 就是真实对象。

构造方法:

1
2
3
public UserServiceProxy(UserService target) {
this.target = target;
}

表示创建代理对象时,把真实对象传进来。

所以:

1
target.save(user);

真正调用的是:

1
UserServiceImpl.save(user);

执行流程是:

1
2
3
4
5
6
1. 调用 proxy.save(user)
2. 进入 UserServiceProxy 的 save 方法
3. 代理对象打印开始日志
4. 代理对象调用 target.save(user)
5. 真实对象 UserServiceImpl 执行业务逻辑
6. 代理对象打印结束日志和耗时

也就是:

1
调用方 -> UserServiceProxy -> UserServiceImpl

这就是静态代理。

静态代理的优点是简单、直观,非常适合理解代理模式的思想。

但是它的问题也很明显:每一个接口、每一个业务类,都可能需要写一个对应的代理类。

比如现在只有一个 UserService,还好处理。

如果系统里有很多服务:

1
2
3
4
5
UserService
OrderService
ProductService
PaymentService
CouponService

那可能就要写很多代理类:

1
2
3
4
5
UserServiceProxy
OrderServiceProxy
ProductServiceProxy
PaymentServiceProxy
CouponServiceProxy

而且这些代理类里面的增强逻辑可能还差不多,都是打印日志、统计耗时、开启事务、提交事务。

这样会产生大量重复代码。

所以静态代理适合学习代理模式,但在大型项目中,如果全靠手写代理类,维护成本会很高。

这时候就需要动态代理。


动态代理:代理类不再手写,而是在运行时生成

动态代理的核心思想是:

1
不用我们手写代理类,而是在程序运行时自动生成代理对象。

静态代理中,我们需要自己写:

1
2
3
public class UserServiceProxy implements UserService {
// 代理逻辑
}

动态代理中,这个代理类不需要我们手写,而是由程序在运行时生成。

Java 中常见的动态代理主要有两种:

1
2
1. JDK 动态代理
2. CGLIB 动态代理

这两种方式都可以生成代理对象,但它们的实现原理不同。


JDK 动态代理:基于接口生成代理对象

JDK 动态代理是 Java 原生提供的动态代理机制。

它有一个重要特点:

1
目标对象必须实现接口。

比如我们有一个接口:

1
2
3
public interface UserService {
void save(User user);
}

真实对象实现这个接口:

1
2
3
4
5
6
7
public class UserServiceImpl implements UserService {

@Override
public void save(User user) {
System.out.println("保存用户:" + user);
}
}

然后可以使用 JDK 动态代理创建代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JdkProxyDemo {

public static void main(String[] args) {
UserService target = new UserServiceImpl();

UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();

System.out.println("开始调用:" + method.getName());

Object result = method.invoke(target, args);

long end = System.currentTimeMillis();

System.out.println("调用结束,耗时:" + (end - start) + "ms");

return result;
}
}
);

proxy.save(new User());
}
}

这段代码里最核心的是:

1
Proxy.newProxyInstance(...)

它会在运行时生成一个代理对象。

这个代理对象也实现了 UserService 接口,所以可以强转成:

1
UserService proxy

也就是说,调用方看到的依然是:

1
UserService

但实际上它拿到的是 JDK 运行时生成的代理对象。

当执行:

1
proxy.save(new User());

不会直接进入 UserServiceImpl.save(),而是先进入:

1
InvocationHandler.invoke()

然后在 invoke() 方法里面,再通过反射调用真实对象:

1
method.invoke(target, args);

所以 JDK 动态代理的调用链是:

1
2
3
4
5
6
7
8
9
调用方

JDK 生成的代理对象

InvocationHandler.invoke()

method.invoke(target, args)

真实对象 UserServiceImpl

这里有几个参数需要理解一下。

第一个参数:

1
target.getClass().getClassLoader()

表示使用哪个类加载器来加载代理类。

第二个参数:

1
target.getClass().getInterfaces()

表示代理对象需要实现哪些接口。

第三个参数:

1
new InvocationHandler() { ... }

表示代理对象的方法调用逻辑。

你可以把 InvocationHandler 理解成代理对象的统一拦截器。

所有通过代理对象调用的方法,都会进入 invoke() 方法。

这就是为什么动态代理可以统一处理日志、事务、权限等逻辑。


JDK 动态代理为什么必须有接口?

这是因为 JDK 动态代理生成的代理类,本质上是实现了目标对象的接口。

比如目标类实现了:

1
public class UserServiceImpl implements UserService

JDK 动态代理生成的代理类,大概可以理解成这样:

1
2
3
4
5
6
7
8
9
public class $Proxy0 implements UserService {

private InvocationHandler handler;

@Override
public void save(User user) {
handler.invoke(this, saveMethod, new Object[]{user});
}
}

当然真实生成的字节码比这个复杂,但思想可以这样理解。

代理对象和真实对象都实现了同一个接口:

1
2
UserServiceImpl implements UserService
$Proxy0 implements UserService

所以调用方可以这样写:

1
UserService userService = proxy;

但是如果目标类没有实现接口,JDK 动态代理就不知道代理对象应该实现什么接口。

所以 JDK 动态代理要求目标对象必须实现接口。


CGLIB 动态代理:基于继承生成代理对象

CGLIB 和 JDK 动态代理不同。

JDK 动态代理是基于接口。

CGLIB 动态代理是基于继承。

也就是说,即使目标类没有实现接口,CGLIB 也可以生成代理对象。

比如这个类没有实现任何接口:

1
2
3
4
5
6
public class UserService {

public void save(User user) {
System.out.println("保存用户:" + user);
}
}

CGLIB 可以在运行时生成它的子类代理对象。

可以理解成生成了一个类似这样的类:

1
2
3
4
5
6
7
8
9
10
11
public class UserService$$EnhancerByCGLIB extends UserService {

@Override
public void save(User user) {
System.out.println("开始调用 save");

super.save(user);

System.out.println("调用结束");
}
}

也就是说,CGLIB 的代理对象是目标类的子类。

调用链大概是:

1
2
3
4
5
6
7
8
9
调用方

CGLIB 生成的子类代理对象

代理对象执行增强逻辑

super.save(user)

真实业务逻辑

这就是为什么 CGLIB 不要求目标类实现接口。

因为它不是靠“实现同一个接口”来代理,而是靠“继承目标类”来代理。


CGLIB 的限制

CGLIB 是基于继承实现的,所以它有几个天然限制。

第一,目标类不能是 final

因为 final 类不能被继承。

1
2
3
4
5
public final class UserService {
public void save() {
System.out.println("保存用户");
}
}

这种类 CGLIB 无法代理。

第二,目标方法不能是 final

因为 final 方法不能被子类重写。

1
2
3
4
5
6
public class UserService {

public final void save() {
System.out.println("保存用户");
}
}

CGLIB 代理对象虽然可以继承 UserService,但不能重写 save() 方法。

而不能重写,就不能在方法前后插入增强逻辑。

第三,private 方法也不能被代理。

因为 private 方法对子类不可见,不能被重写。

所以 CGLIB 虽然不要求接口,但要求目标类和目标方法能够被继承、被重写。


JDK 动态代理和 CGLIB 的详细区别

JDK 动态代理和 CGLIB 动态代理最核心的区别是:

1
2
JDK 动态代理:基于接口
CGLIB 动态代理:基于继承

可以从几个角度来对比。

对比点 JDK 动态代理 CGLIB 动态代理
代理方式 实现接口 继承目标类
是否需要接口 需要 不需要
目标类是否可以是 final 可以,因为代理的是接口 不可以,因为需要继承
目标方法是否可以是 final 接口方法本身可被代理 不可以,因为 final 方法不能重写
私有方法能否代理 不能直接代理 不能代理
生成对象类型 接口的实现类 目标类的子类
调用方式 通过 InvocationHandler 拦截 通过 MethodInterceptor 拦截
Spring 中的使用场景 目标类有接口时可用 目标类没有接口时可用

再用更直白的话说:

JDK 动态代理生成的是:

1
一个实现了接口的代理类

类似:

1
2
class $Proxy0 implements UserService {
}

CGLIB 生成的是:

1
一个继承目标类的子类

类似:

1
2
class UserService$$EnhancerByCGLIB extends UserService {
}

所以 JDK 动态代理更适合面向接口编程的场景。

CGLIB 更适合没有接口的普通类代理场景。


Spring AOP 和代理模式的关系

Spring AOP 的底层核心就是代理模式。

我们平时写业务代码时,可能只是这样写:

1
2
3
4
5
6
7
8
@Service
public class UserServiceImpl implements UserService {

@Override
public void save(User user) {
System.out.println("保存用户:" + user);
}
}

然后写一个切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Aspect
@Component
public class LogAspect {

@Around("execution(* com.example.service.*.*(..))")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();

System.out.println("开始调用方法:" + joinPoint.getSignature().getName());

Object result = joinPoint.proceed();

long end = System.currentTimeMillis();

System.out.println("方法调用结束,耗时:" + (end - start) + "ms");

return result;
}
}

表面上看,我们并没有写代理类。

但实际上,Spring 会在容器启动过程中判断哪些 Bean 需要被 AOP 增强。

如果某个 Bean 匹配了切点表达式,Spring 就会为它创建代理对象。

原本容器里应该放的是:

1
UserServiceImpl

但开启 AOP 后,容器里最终暴露给外部使用的可能是:

1
UserServiceImpl 的代理对象

所以当 Controller 注入:

1
2
@Autowired
private UserService userService;

拿到的未必是原始的 UserServiceImpl,而可能是 Spring 生成的代理对象。

执行:

1
userService.save(user);

调用链大概是:

1
2
3
4
5
6
7
8
9
10
11
Controller

Spring 代理对象

执行切面逻辑,例如日志、事务、权限

调用真实对象 UserServiceImpl.save()

真实业务方法执行

回到代理对象,继续执行后置增强

所以 Spring AOP 可以理解为:

1
Spring 自动帮我们创建代理对象,并通过代理对象对目标方法进行增强。

Spring 到底选择 JDK 动态代理还是 CGLIB?

Spring AOP 默认会根据目标类情况选择代理方式。

通常可以这样理解:

1
2
如果目标类实现了接口,Spring 可以使用 JDK 动态代理。
如果目标类没有实现接口,Spring 使用 CGLIB 动态代理。

比如:

1
2
3
public interface UserService {
void save(User user);
}
1
2
3
4
5
6
7
@Service
public class UserServiceImpl implements UserService {
@Override
public void save(User user) {
System.out.println("保存用户:" + user);
}
}

这种情况有接口,Spring 可以使用 JDK 动态代理。

生成的代理对象实现的是 UserService 接口。

所以你一般应该这样注入:

1
2
@Autowired
private UserService userService;

而不是:

1
2
@Autowired
private UserServiceImpl userService;

因为 JDK 动态代理生成的代理对象是接口实现类,不一定是 UserServiceImpl 的子类。

如果目标类没有接口:

1
2
3
4
5
6
7
@Service
public class UserService {

public void save(User user) {
System.out.println("保存用户:" + user);
}
}

这种情况下,JDK 动态代理没法使用,因为没有接口。

Spring 就会使用 CGLIB 创建一个子类代理对象。

生成的代理对象可以理解成:

1
2
class UserService$$SpringCGLIB extends UserService {
}

所以没有接口时,CGLIB 也能代理。


为什么 Spring 推荐面向接口编程?

因为接口可以把调用方和具体实现类解耦。

比如 Controller 依赖的是:

1
private UserService userService;

而不是:

1
private UserServiceImpl userService;

这样以后无论底层是真实对象、JDK 代理对象、CGLIB 代理对象,调用方都不用关心。

它只知道自己需要一个 UserService

这也是代理模式非常重要的一个点:

1
代理对象和真实对象对外暴露相同的行为。

调用方不用知道自己拿到的到底是真实对象还是代理对象。

这就是“对调用方透明”。


Spring 事务为什么也依赖代理模式?

Spring 事务是代理模式最典型的应用之一。

平时我们写:

1
2
3
4
5
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
stockMapper.decrease(order.getProductId());
}

看起来只是加了一个 @Transactional 注解,事务就自动生效了。

但 Spring 并不是把事务代码直接塞进你的业务方法里面。

它的实现思想大概是:

1
2
3
4
5
6
7
8
9
10
11
调用方

事务代理对象

开启事务

调用真实业务方法

业务成功,提交事务

业务异常,回滚事务

如果用伪代码表示,事务代理大概像这样:

1
2
3
4
5
6
7
8
9
10
11
public void createOrder(Order order) {
transactionManager.begin();

try {
target.createOrder(order);
transactionManager.commit();
} catch (Exception e) {
transactionManager.rollback();
throw e;
}
}

这就是代理模式的典型应用。

真实业务方法只负责业务:

1
2
orderMapper.insert(order);
stockMapper.decrease(order.getProductId());

代理对象负责事务:

1
2
3
开启事务
提交事务
回滚事务

这也是为什么有些场景下 @Transactional 会失效。

比如同一个类内部方法调用:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class OrderService {

public void create() {
this.saveOrder();
}

@Transactional
public void saveOrder() {
// 保存订单
}
}

这里 create() 里面调用:

1
this.saveOrder();

本质上是当前对象内部调用,没有经过 Spring 代理对象。

没有经过代理对象,就不会执行事务增强逻辑。

所以事务可能不会生效。

正确的理解应该是:

1
2
@Transactional 不是方法自己变强了,
而是 Spring 代理对象在方法外面包了一层事务逻辑。

如果调用没有经过代理对象,这一层事务逻辑自然就不会执行。


代理模式的本质总结

代理模式表面上是“多包了一层对象”,本质上是“控制方法调用过程”。

普通调用是:

1
调用方 -> 真实对象

代理调用是:

1
调用方 -> 代理对象 -> 真实对象

代理对象可以在这个过程中做很多事情:

1
2
3
4
方法调用前:权限校验、参数校验、开启事务、记录开始时间
方法调用中:调用真实对象
方法调用后:提交事务、记录日志、统计耗时、处理返回值
方法异常时:回滚事务、记录异常、统一包装异常

所以代理模式适合处理这类问题:

1
2
某些逻辑和核心业务无关
但又需要在很多业务方法前后统一执行

比如:

1
2
3
4
5
6
7
8
9
日志
事务
权限
缓存
限流
监控
远程调用
延迟加载
异常处理

静态代理适合理解思想,但需要手写代理类,扩展性一般。

JDK 动态代理基于接口,要求目标类实现接口,代理对象本质上是接口的实现类。

CGLIB 动态代理基于继承,不要求接口,代理对象本质上是目标类的子类,但不能代理 final 类、final 方法和 private 方法。

Spring AOP 则是在这些动态代理技术之上,把代理对象的创建、增强逻辑的织入、目标方法的调用都封装好了。

所以我们只需要写:

1
2
3
4
@Aspect
@Component
public class LogAspect {
}

或者:

1
2
3
@Transactional
public void save() {
}

Spring 就会在底层帮我们生成代理对象,并在合适的位置执行增强逻辑。

一句话总结:

1
代理模式不是为了替代真实对象,而是为了在不修改真实对象的前提下,控制和增强对真实对象的访问。

理解了代理模式,也就更容易理解 Spring AOP、Spring 事务、MyBatis Mapper 代理、RPC 框架中的远程代理等底层思想。