努力成为面试高手02:Spring AOP

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

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;
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值