通过这一个帖子,我们会涉及到以下问题:

AOP即面向切面编程,能够将与业务无关,但是被业务所共同调用的逻辑封装起来,这么做可以减少重复代码与降低模块耦合度,提高可扩展与可维护性。一图胜千言,可以用下面这张图来表示:

AOP基于动态代理,主要分为基于接口的动态代理(JDK动态代理)与基于类的代理(CGLIB动态代理)。同时AOP也可以使用 AspectJ 框架。简单场景优先使用 Spring AOP;复杂场景或高性能需求时,选择 AspectJ。AOP中涉及的概念有切面,连接点,通知等。术语表如下(这里的大白话解释一栏基于之后举的房产中介例子):
| 专业术语 | 技术含义 | 大白话解释(房产中介比喻) |
| 目标(Target) | 被通知的对象 | 房东 - 拥有核心资源(房子)并执行核心操作(签协议)的人 |
| 代理(Proxy) | 向目标对象应用通知后创建的代理对象 | 房产中介 - 站在你和房东之间处理所有事务的中间人 |
| 连接点(JoinPoint) | 目标对象中定义的所有方法 | 租房所有环节 - 看房、谈价、签合同、交钥匙等所有可能插手的环节 |
| 切入点(Pointcut) | 被切面拦截/增强的连接点 | "签署租赁协议"环节 - 特别选中要让中介介入的特定环节 |
| 通知(Advice) | 拦截后要执行的增强逻辑 | 中介的具体工作: • 前置工作:记录需求、预览条款 • 后置工作:让客户收合同副本、进行后续工作 |
| 切面(Aspect) | 切入点+通知 | 整套中介服务规范 - 明确规定在"签合同"时要完成哪些准备工作和服务 |
| 织入(Weaving) | 将通知应用到目标对象生成代理的过程 | 把服务规范应用到实际流程 - 中介公司把服务标准植入租房流程的整个过程 |
想要实现AOP就要基于动态代理技术。动态代理,简要来讲就是在运行时生成代理对象,而不是在编译时。该怎么理解这句话呢?代理对象可以看作是一个房产中介,我们想要租一套房子(执行核心业务逻辑),但不再直接联系房东(目标对象),而是通过中介(代理对象)来完成。这个“中介”的工作流程是这样的:

如果对应到写程序这件事上,上面的图就可以具体为这样:

那为什么“运行时生成”比“一次性编译”更有优势呢?这本质上就是动态代理与静态代理的选择问题。代理是一种常见的设计模式,为其他对象提供一个代理以控制对某个对象的访问,将两个关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类的方法。比如在上面的房产中介的例子里,中介就承担了代理的角色,解耦了客户与房产中介的关系。
为什么要在运行时生成而不是编译时?一次性编译好不好么?我们来举一个例子:
// 1. 定义接口
public interface UserService {
void saveUser();
}
// 2. 目标类
public class UserServiceImpl implements UserService {
public void saveUser() {
// 核心业务逻辑
}
}
// 3. 静态代理类(编译时确定)
public class UserServiceStaticProxy implements UserService {
private UserService target;
public UserServiceStaticProxy(UserService target) {
this.target = target;
}
public void saveUser() {
// 前置增强
System.out.println("开始记录日志...");
// 调用目标方法
target.saveUser();
// 后置增强
System.out.println("结束记录日志...");
}
}
这段在编译时生成的代码暴露出了一个问题:如果我还有 OrderService、ProductService... 我需要为每一个服务类都手动编写一个代理类。如果我们使用动态代理,就不会出现这种情况:
// 动态代理处理器
public class LoggingHandler implements InvocationHandler {
private Object target;
public LoggingHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强
System.out.println("开始记录日志...");
// 调用目标方法
Object result = method.invoke(target, args);
// 后置增强
System.out.println("结束记录日志...");
return result;
}
}
// 使用时动态创建
UserService userService = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class},
new LoggingHandler(userService)
);
总结一下,我们选择运行时生成的原因有:
1.动态代理是通用解决方案,而静态代理是特定解决方案。
-
动态代理:一个 LoggingHandler 可以代理任何接口的实现类。在代码运行期间,运用反射机制动态创建生成。动态代理代理的是一个接口下的多个实现类。
-
静态代理:每个需要日志的类都需要一个专门的代理类。由程序员创建或者是由特定工具创建,在代码编译时就确定了被代理的类是一个静态代理。静态代理通常只代理一个类。
// 同样的LoggingHandler可以用于不同的服务
UserService userProxy = createProxy(userService, new LoggingHandler());
OrderService orderProxy = createProxy(orderService, new LoggingHandler());
ProductService productProxy = createProxy(productService, new LoggingHandler());
2.运行时生成意味着我们可以根据配置或环境动态决定是否创建代理,以及如何创建代理。
public Object createProxyIfNeeded(Object target) {
if (shouldApplyLogging()) { // 根据配置决定
return Proxy.newProxyInstance(...);
}
return target; // 直接返回原对象
}
3.复杂的框架中(如Spring),我们无法预知用户会定义哪些Bean,需要哪些AOP增强。
// Spring框架在启动时扫描所有Bean
// 根据注解配置动态决定哪些需要代理
@Bean
public UserService userService() {
UserService rawService = new UserServiceImpl();
// 根据条件动态创建代理
if (hasTransactionalAnnotation(rawService)) {
return createTransactionalProxy(rawService);
}
if (hasLoggingAnnotation(rawService)) {
return createLoggingProxy(rawService);
}
return rawService;
}
4.可以减少代码冗余,想象一下Spring框架中有成千上万个Bean:
-
静态代理:需要编写成千上万个代理类
在这里需要强调的是,是可以使用静态代理实现 AOP 的。就如上面的代码示例所示:
// 3. 静态代理类(编译时确定)
public class UserServiceStaticProxy implements UserService {
private UserService target;
public UserServiceStaticProxy(UserService target) {
this.target = target;
}
public void saveUser() {
// 前置增强
System.out.println("开始记录日志...");
// 调用目标方法
target.saveUser();
// 后置增强
System.out.println("结束记录日志...");
}
}
可以在代码中手动写一个代理类,然后在目标方法前后加日志或者事务控制。但是这样出现代码爆炸(比如你有 100 个 Service 类需要加事务,就得写 100 个对应的静态代理类)、僵化(一旦业务接口改了个方法名,所有相关的代理类都得跟着改,而动态代理通过反射调用目标方法,就解决了这种问题)、无法动态筛选(静态代理有的情况只能写死逻辑,而AOP 可以在运行时通过切点表达式精准匹配需要增强的方法)等问题,所以平时几乎没用静态代理实现 AOP。
-
动态代理:只需要几个通用的 InvocationHandler
5.支持运行时决策,即代理行为可以根据运行时状态改变,如下面的代码所示:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 根据当前用户权限决定是否执行方法
if (!currentUser.hasPermission(method.getName())) {
throw new SecurityException("无权限");
}
// 根据系统负载决定是否记录详细日志
if (systemLoad < 80) {
logDetailedInfo(method, args);
}
return method.invoke(target, args);
}
当然也不是说一次性编译就不好,这里就不赘述了。
那么接下来就接着详细说一下动态代理,主要用于在不修改原始类的情况下对方法调用进行拦截和增强。
Spring AOP的核心思想是:在程序运行时,动态地给某些方法“加料”(比如添加日志、权限检查等功能),而不需要修改原始代码。它通过“动态代理”技术来实现这一点——动态代理就像在原始对象外面包了一层“外壳”,这个外壳能拦截方法调用,并执行额外的操作。Spring AOP主要支持两种代理方式:一种是基于JDK的(要求类实现接口),另一种是基于CGLIB的(适用于没有接口的类)。
-
基于接口的代理(JDK动态代理)
依赖于 Java 自带的 Proxy 类和 InvocationHandler 接口。举个例子,假设你有一个接口 UserService 和一个实现类 UserServiceImpl 。通过JDK动态代理,你可以这样创建一个代理对象:
// 实现InvocationHandler接口,定义“加料”逻辑
public class MyHandler implements InvocationHandler {
private Object target; // 原始对象,比如UserServiceImpl的实例
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法调用前:记录日志"); // 这里是增强的功能
Object result = method.invoke(target, args); // 调用原始方法
System.out.println("方法调用后:清理资源");
return result;
}
}
// 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new MyHandler(target)
);
这样,当你调用 proxy 的方法时,它会先执行 invoke 方法中的“加料”代码,再调用原始方法。
-
基于类的代理(CGLIB动态代理)
如果类没有实现接口,Spring 会用 CGLIB 库生成一个子类作为代理。比如,有一个类 ProductService 没有接口,CGLIB 会创建一个 ProductService$$EnhancerByCGLIB 的子类,并重写方法来实现代理。代码大致如下:
// 使用Enhancer类创建代理
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ProductService.class); // 设置父类
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("CGLIB代理:方法调用前");
Object result = proxy.invokeSuper(obj, args); // 调用父类方法
System.out.println("CGLIB代理:方法调用后");
return result;
}
});
ProductService proxy = (ProductService) enhancer.create();
这样,CGLIB 代理通过继承来“伪装”成原始类,并在方法调用前后添加额外操作。简单来说,JDK 代理是基于接口的,而 CGLIB 代理是基于类的继承。Spring 会自动选择合适的方式:如果有接口就用 JDK 代理,没有就用 CGLIB 。
最后再来介绍一下 AOP 常见的通知类型,可以用一张示意图来总结:

| 专业术语 | 技术含义 | 大白话解释(房产中介比喻) |
| Before(前置通知) | 目标对象的方法调用之前触发 | 签合同前的准备工作 中介在联系房东前必须做的固定流程:记录客户需求、准备合同条款、检查证件等 |
| After(后置通知) | 目标对象的方法调用之后触发 | 无论成败都要做的收尾工作 不管合同签没签成,中介都要做的收尾工作:整理文件、记录本次服务、清理会议室等 |
| AfterReturning(返回通知) | 目标对象的方法调用完成,在返回结果值之后触发 | 签约成功后的后续服务 合同顺利签署后,中介要做的特定工作:给客户合同副本、安排物业交接、收取中介费等 |
| AfterThrowing(异常通知) | 目标对象的方法运行中抛出/触发异常后触发 | 签约失败后的处理流程 如果签约过程中出现意外(如房东反悔、证件不全),中介要做的应急处理:安抚客户、解释原因、安排备选方案等 |
| Around(环绕通知) | 编程式控制目标对象的方法调用,可操作范围最大 | 全程掌控签约流程 中介完全掌控整个签约过程:可以决定是否联系房东、何时联系、在什么条件下签约,甚至可以直接拒绝这次签约请求 |
AOP 实现的常见注解,与 AOP 常见的通知类型也是一一对应的:
| 注解 | 说明 |
| @Aspect | 用于定义切面,标注在切面类上。 |
| @Pointcut | 定义切点,标注在方法上,用于指定连接点。 |
| @Before | 在方法执行之前执行通知。 |
| @After | 在方法执行之后执行通知。 |
| @Around | 在方法执行前后都执行通知。 |
| @AfterReturning | 在方法执行后返回结果后执行通知。 |
| @AfterThrowing | 在方法抛出异常后执行通知。 |
| @Advice | 通用的通知类型,可以替代 @Before、@After 等。 |
如果我们想要控制多个切面的执行顺序,可以使用 @Order 注解直接定义切面顺序,或者实现 Ordered 接口重写 getOrder 方法:
// 值越小优先级越高
@Order(3)
@Component
@Aspect
public class LoggingAspect implements Ordered {
@Component
@Aspect
public class LoggingAspect implements Ordered {
// ....
@Override
public int getOrder() {
// 返回值越小优先级越高
return 1;
}
}
732

被折叠的 条评论
为什么被折叠?



