一图搞懂AOP、动态代理和反射之间的关系
前言
在 Java 基础学习中,很多初学者容易混淆 AOP、代理模式和反射之间的关系。它们经常一起出现,但实际上属于不同层次的概念。
在讨论之前,需要先明确:
AOP ≠ Spring AOP ≠ JDK 动态代理。
- AOP 是一种编程思想,用于将日志、事务、权限校验等横切逻辑与业务逻辑分离。
- Spring AOP 是 Spring 对 AOP 思想的一种实现。
- 动态代理 是 Spring AOP 在运行期实现方法增强的重要手段。
- 反射 是 Java 提供的一种运行时能力,可以被动态代理用于调用目标方法。
下面以 Spring AOP 为例,梳理它们之间的关系。
该图片较全面地总结了 AOP、代理和反射之间的关系(如有错误,欢迎指出):

概念
AOP(面向切面编程)
AOP 的全称是 Aspect-Oriented Programming,即面向切面编程。
传统的面向对象编程通常按照类和对象组织业务代码,而 AOP 关注的是散布在多个类中的公共逻辑,例如:
- 日志记录
- 事务管理
- 权限校验
- 性能统计
- 异常处理
- 接口限流
这些逻辑往往会跨越多个业务模块,因此也被称为横切关注点。
解决的问题
假设系统中有转账、下单和退款三个业务方法,它们都需要记录日志并进行权限校验:
public void transfer() {
checkPermission();
writeLog();
// 转账业务
}
public void createOrder() {
checkPermission();
writeLog();
// 下单业务
}
public void refund() {
checkPermission();
writeLog();
// 退款业务
}
如果直接在每个方法中编写这些公共逻辑,会出现以下问题:
- 公共代码散落在不同业务类中。
- 业务逻辑与非业务逻辑耦合。
- 修改公共逻辑时,需要同时修改多个位置。
- 容易产生重复代码或遗漏。
AOP 的核心目标就是:
将横切关注点从业务代码中抽离,并在指定位置统一织入。
经过分离以后,业务方法只需要关注自身职责:
public void transfer() {
// 转账业务
}
日志、事务等逻辑则由 AOP 在方法执行前后统一处理。
常见的实现方式
Java 生态中常见的 AOP 实现包括 Spring AOP 和 AspectJ。
Spring AOP
Spring AOP 主要通过运行时动态代理实现。
Spring 在容器中创建 Bean 时,会判断该 Bean 是否需要被增强。如果需要,Spring 会为其创建一个代理对象。调用者实际拿到的通常是代理对象,而不是未经增强的原始对象。
调用过程可以简化为:
调用者
↓
代理对象
↓
执行切面逻辑
↓
目标对象
例如:
调用转账方法
↓
开启事务
↓
执行转账业务
↓
提交事务
因此,Spring AOP 通常属于运行时织入。
AspectJ
AspectJ 是一套更加完整的 AOP 实现。它可以直接修改或增强类的字节码,不局限于通过代理对象拦截方法调用。
AspectJ 支持的织入方式包括:
- 编译期织入
- 编译后织入
- 类加载期织入
与 Spring AOP 相比,AspectJ 能够支持更丰富的连接点,例如构造方法调用、字段访问等,但配置和使用成本也相对更高。
需要注意的是:
Spring AOP 和 AspectJ 不是同一个概念。Spring 可以集成 AspectJ,但 Spring AOP 本身主要基于代理实现。
Spring AOP 的核心概念
切面(Aspect)
切面是对横切逻辑的封装,例如日志切面、事务切面和权限校验切面。
@Aspect
@Component
public class LogAspect {
}
连接点(Join Point)
连接点表示程序执行过程中可以被增强的位置。在 Spring AOP 中,连接点主要是方法执行。
切入点(Pointcut)
切入点用于筛选需要被增强的连接点。例如,下面的表达式表示匹配 service 包中的所有方法:
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {
}
可以简单理解为:
- 连接点:所有可能被增强的方法。
- 切入点:从中选出实际需要增强的方法。
通知(Advice)
通知是切面在特定时间执行的具体逻辑。Spring AOP 常见的通知类型包括:
@Before:目标方法执行前@After:目标方法结束后@AfterReturning:目标方法正常返回后@AfterThrowing:目标方法抛出异常后@Around:环绕目标方法执行
例如:
@Before("serviceMethods()")
public void before() {
System.out.println("方法执行前记录日志");
}
目标对象(Target)
目标对象是实际包含业务逻辑的对象,也就是被代理、被增强的对象。
代理对象(Proxy)
代理对象由 Spring 创建。调用者通过代理对象间接访问目标对象,代理对象负责执行切面逻辑和目标方法。
织入(Weaving)
织入是将切面逻辑应用到目标对象的过程。Spring AOP 主要在运行时通过创建代理对象完成织入。
引介(Introduction)
引介允许在不修改原类代码的情况下,为目标对象增加新的接口及其实现。这个功能在普通业务开发中使用得相对较少,但也是 Spring AOP 概念体系的一部分。
代理模式
代理模式是一种结构型设计模式。在代理模式中,调用者不会直接访问目标对象,而是通过代理对象间接访问目标对象。
调用者 → 代理对象 → 目标对象
代理对象可以在调用目标对象之前或之后加入额外逻辑。
解决的问题
代理模式主要解决的是:
在不修改目标类核心代码的情况下,控制或增强对目标对象的访问。
常见应用包括:
- 方法调用前后记录日志
- 权限校验
- 事务控制
- 远程调用
- 延迟加载
- 缓存
- 性能监控
代理模式本身不等于 AOP。AOP 是一种编程思想,而代理模式是一种设计模式。只是代理模式提供的“调用前后增强”能力,非常适合用来实现 AOP。
静态代理
静态代理是指代理类由开发者提前编写,在编译时代理关系就已经确定。
首先定义接口:
public interface UserService {
void saveUser();
}
目标类实现接口:
public class UserServiceImpl implements UserService {
@Override
public void saveUser() {
System.out.println("保存用户");
}
}
编写代理类:
public class UserServiceProxy implements UserService {
private final UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void saveUser() {
System.out.println("记录开始日志");
target.saveUser();
System.out.println("记录结束日志");
}
}
使用代理对象:
public class Main {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.saveUser();
}
}
运行结果:
记录开始日志
保存用户
记录结束日志
静态代理的优点是结构直观、容易理解,而且不依赖复杂的运行时机制。但它也存在明显缺点:
- 每个目标类型通常都需要编写对应的代理类。
- 接口中的方法发生变化时,代理类也要跟着修改。
- 目标类数量较多时,会产生大量重复代码。
动态代理
动态代理不需要开发者为每个目标类手动编写代理类,而是在程序运行过程中动态生成代理对象。
Java 中常见的动态代理方式包括:
- JDK 动态代理
- CGLIB 动态代理
JDK 动态代理
JDK 动态代理面向接口创建代理对象,因此目标类通常需要实现接口。
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxyObject, method, args) -> {
System.out.println("记录开始日志");
Object result = method.invoke(target, args);
System.out.println("记录结束日志");
return result;
}
);
proxy.saveUser();
Proxy.newProxyInstance() 需要三个主要参数:
Proxy.newProxyInstance(
ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler handler
)
它们分别表示:
loader:用于加载代理类的类加载器。interfaces:代理对象需要实现的接口。handler:代理对象调用方法时执行的处理逻辑。
当调用 proxy.saveUser() 时,代理对象会将调用转交给 InvocationHandler 的 invoke() 方法。invoke() 可以在目标方法执行前后增加额外逻辑,并通过下面的代码调用目标方法:
method.invoke(target, args);
这里的 Method.invoke() 就属于反射调用。
需要注意避免写成:
method.invoke(proxyObject, args);
因为这相当于再次调用代理对象,可能不断进入 InvocationHandler,最终导致递归调用或栈溢出。
CGLIB 动态代理
CGLIB 主要通过继承目标类、生成目标类子类的方式创建代理对象,因此目标类不一定需要实现接口。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback((MethodInterceptor) (
proxyObject,
method,
args,
methodProxy
) -> {
System.out.println("记录开始日志");
Object result = methodProxy.invokeSuper(proxyObject, args);
System.out.println("记录结束日志");
return result;
});
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
proxy.saveUser();
CGLIB 代理的核心思路可以简化为:
class UserServiceProxy extends UserServiceImpl {
@Override
public void saveUser() {
// 增强逻辑
super.saveUser();
// 增强逻辑
}
}
实际的代理类由框架在运行时动态生成,不需要开发者手动编写。
由于 CGLIB 依赖继承实现代理,因此存在以下限制:
final类不能被继承,所以不能通过这种方式创建代理子类。final方法不能被重写,所以无法被代理对象增强。private方法对子类不可见,也无法通过重写进行增强。
两种动态代理的适用场景
| 对比项 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 主要实现方式 | 实现目标接口 | 继承目标类 |
| 是否要求接口 | 通常需要 | 不需要 |
| 代理对象类型 | 接口的实现类 | 目标类的子类 |
| 主要限制 | 主要代理接口方法 | 不能重写 final、private 方法 |
| 方法调用 | 常见方式是反射调用 | 常见方式是生成字节码并调用父类方法 |
对于 Spring AOP,可以先作如下理解:
- 适合使用接口代理时,可以采用 JDK 动态代理。
- 需要基于具体类代理时,可以采用 CGLIB。
- 具体使用哪一种方式,还会受到 Spring 配置、目标类结构及框架版本的影响。
还需要注意,虽然 Spring AOP 使用动态代理,但并不意味着 Spring AOP 的所有方法调用都会被增强。
例如,同一个类中通过 this 调用另一个方法:
@Service
public class OrderService {
public void createOrder() {
this.pay();
}
@Transactional
public void pay() {
// 支付逻辑
}
}
createOrder() 内部直接调用 this.pay(),没有经过 Spring 创建的代理对象,因此 pay() 上的事务增强可能不会生效。这也是 Spring AOP 基于代理实现所带来的典型限制。
反射
Java 反射是指程序在运行过程中动态获取类的信息,并操作类、对象、方法和字段的能力。
反射相关的核心类型包括:
ClassMethodFieldConstructor
解决的问题
正常调用一个方法时,通常需要在编译阶段明确对象类型和方法名称:
UserService userService = new UserServiceImpl();
userService.saveUser();
但框架往往无法提前知道用户会定义哪些类、哪些方法以及哪些注解。
反射可以让程序在运行时完成这些操作:
- 获取类的信息
- 获取类中的方法和字段
- 检查类、方法或字段上的注解
- 动态创建对象
- 动态读取或修改字段
- 动态调用方法
例如,运行时获取类的信息:
Class<?> clazz = UserServiceImpl.class;
System.out.println(clazz.getName());
for (Method method : clazz.getDeclaredMethods()) {
System.out.println(method.getName());
}
通过反射创建对象:
Class<?> clazz = UserServiceImpl.class;
Object object = clazz
.getDeclaredConstructor()
.newInstance();
通过反射调用方法:
Method method = clazz.getDeclaredMethod("saveUser");
method.invoke(object);
反射的主要价值在于:
让程序具有运行时检查和操作未知类型的能力,从而支撑框架的通用化设计。
Spring 可以利用反射完成许多工作,例如:
- 扫描和识别注解
- 分析 Bean 的类型信息
- 查找构造方法
- 完成依赖注入
- 读取 AOP 切点相关信息
- 调用目标方法
但需要注意:
动态代理不等于反射,反射也不等于动态代理。
JDK 动态代理的 InvocationHandler 经常通过 Method.invoke() 调用目标对象,因此会直接使用反射;CGLIB 的核心则是运行时字节码生成和方法拦截,不应简单概括为“完全依靠反射”。
三者之间的关系
AOP、代理模式和反射可以从三个不同层次理解:
| 概念 | 所属层次 | 主要作用 |
|---|---|---|
| AOP | 编程思想 | 分离横切关注点与业务逻辑 |
| 代理模式 | 设计模式 | 通过代理对象控制或增强目标对象 |
| 反射 | Java 运行时机制 | 动态获取类型信息并操作对象或方法 |
以 Spring AOP 使用 JDK 动态代理为例,三者之间的关系如下:
AOP
│
│ 需要实现横切逻辑的统一织入
↓
Spring AOP
│
│ 使用代理对象拦截方法调用
↓
JDK 动态代理
│
│ InvocationHandler 接收方法调用
↓
反射调用 Method.invoke()
│
↓
目标对象的业务方法
一次完整的方法调用过程大致如下:
调用者调用代理对象的方法
↓
代理对象将调用交给 InvocationHandler
↓
执行日志、事务、权限校验等增强逻辑
↓
通过反射调用目标对象的方法
↓
获得目标方法的返回值
↓
执行方法调用后的增强逻辑
↓
将结果返回给调用者
如果 Spring AOP 使用 CGLIB,调用链则可以简化为:
调用者
↓
CGLIB 生成的代理子类
↓
方法拦截器
↓
切面逻辑
↓
目标方法
因此,“AOP 的底层就是反射”这种说法并不严谨。
更准确的说法是:
Spring AOP 是 AOP 思想的一种实现,它主要借助动态代理拦截方法调用;JDK 动态代理的目标方法调用通常会使用反射,而 CGLIB 主要依赖运行时字节码生成和方法拦截。
总结
AOP、代理模式和反射虽然经常一起出现,但它们并不是同一个概念。
- AOP 是一种编程思想,关注横切逻辑与业务逻辑的分离。
- Spring AOP 是 AOP 思想的一种实现,主要通过运行时动态代理完成方法增强。
- 代理模式是一种设计模式,通过代理对象间接访问并增强目标对象。
- JDK 动态代理主要面向接口,通常通过
InvocationHandler和反射调用目标方法。 - CGLIB 动态代理主要面向具体类,通过生成目标类的子类实现方法拦截。
- 反射是 Java 的运行时能力,可以动态获取类型信息、创建对象和调用方法。
- AspectJ 可以直接进行字节码织入,不依赖 Spring AOP 的代理调用模型。
最终可以用一句话概括三者的关系:
AOP 提出了“为什么增强”,代理模式解决“如何拦截并增强”,反射则可以为运行时识别和调用方法提供底层能力。

浙公网安备 33010602011771号