Spring AOP 基于 XML 和注解(`@Aspect`, `@Before`, `@After`, `@Around`)的实现

Spring AOP:XML与注解的华山论剑,谁能笑傲江湖?

各位看官,今天咱们聊聊Spring AOP这玩意儿。AOP,全称Aspect-Oriented Programming,面向切面编程。听起来高大上,其实说白了,就是想让你在不修改原有代码的情况下,还能给它加点料,比如加个日志,做个权限校验啥的。就好比你想给你的烤鸭添点佐料,但又不想把烤鸭大卸八块重新烤一遍。

Spring AOP 提供了两种实现方式:一种是古色古香的XML配置,一种是时尚前卫的注解(@Aspect, @Before, @After, @Around)驱动。这两种方式就像武林中的两个门派,一个内功深厚,稳扎稳打;一个剑走偏锋,灵活多变。那么问题来了,到底哪个更好呢?今天咱们就来一场华山论剑,好好比划比划。

一、XML配置:老骥伏枥,志在千里

XML配置就像一位经验丰富的老前辈,虽然看起来有点笨重,但却拥有着强大的内功。它把所有的切面、切点、通知都定义在XML文件中,结构清晰,一目了然。

1. XML配置的基本概念

在XML配置中,主要涉及以下几个概念:

  • Aspect(切面): 包含了通知(Advice)和切点(Pointcut)的类,它定义了横切逻辑。
  • Pointcut(切点): 定义了在哪些连接点(Join Point)上应用通知。连接点指的是程序执行过程中的一些点,比如方法调用、方法执行、异常抛出等。切点可以使用表达式来匹配这些连接点。
  • Advice(通知): 定义了在切点上执行的动作。Spring AOP 支持以下几种通知类型:
    • Before(前置通知): 在目标方法执行前执行。
    • After(后置通知): 在目标方法执行后执行,无论方法是否抛出异常。
    • AfterReturning(返回后通知): 在目标方法成功执行后执行,可以访问目标方法的返回值。
    • AfterThrowing(异常后通知): 在目标方法抛出异常后执行,可以访问抛出的异常。
    • Around(环绕通知): 包围目标方法的执行,可以控制目标方法的执行时机,甚至可以修改目标方法的返回值。

2. XML配置示例

咱们先来个简单的例子,假设有一个UserService类,我们想在它的createUser方法执行前后分别打印日志。

// UserService.java
public class UserService {
    public void createUser(String username) {
        System.out.println("Creating user: " + username);
    }
}

接下来,我们需要创建一个切面类,定义通知和切点。

// LoggingAspect.java
public class LoggingAspect {
    public void beforeCreateUser(String username) {
        System.out.println("[Before] Creating user: " + username);
    }

    public void afterCreateUser() {
        System.out.println("[After] User created successfully.");
    }
}

最后,在Spring的XML配置文件中配置切面:

<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 定义 UserService Bean -->
    <bean id="userService" class="com.example.UserService"/>

    <!-- 定义 LoggingAspect Bean -->
    <bean id="loggingAspect" class="com.example.LoggingAspect"/>

    <!-- 配置 AOP -->
    <aop:config>
        <aop:aspect id="logging" ref="loggingAspect">
            <!-- 定义切点:execution(* com.example.UserService.createUser(..)) 表示 UserService 类的 createUser 方法 -->
            <aop:pointcut id="createUserPointcut" expression="execution(* com.example.UserService.createUser(..))"/>

            <!-- 定义前置通知 -->
            <aop:before method="beforeCreateUser" pointcut-ref="createUserPointcut" arg-names="username"/>

            <!-- 定义后置通知 -->
            <aop:after method="afterCreateUser" pointcut-ref="createUserPointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

代码解释:

  • xmlns:aop="http://www.springframework.org/schema/aop":引入 AOP 命名空间。
  • <aop:config>:声明 AOP 配置。
  • <aop:aspect id="logging" ref="loggingAspect">:定义一个切面,id是切面的唯一标识,ref指向切面类的 Bean。
  • <aop:pointcut id="createUserPointcut" expression="execution(* com.example.UserService.createUser(..))"/>:定义一个切点,id是切点的唯一标识,expression是切点表达式。execution(* com.example.UserService.createUser(..)) 表示匹配 com.example.UserService 类的 createUser 方法,* 表示返回任意类型,.. 表示任意参数。
  • <aop:before method="beforeCreateUser" pointcut-ref="createUserPointcut" arg-names="username"/>:定义一个前置通知,method指向切面类中的方法,pointcut-ref指向切点,arg-names指定方法参数名。
  • <aop:after method="afterCreateUser" pointcut-ref="createUserPointcut"/>:定义一个后置通知,method指向切面类中的方法,pointcut-ref指向切点。

3. XML配置的优点和缺点

  • 优点:
    • 配置集中化: 所有 AOP 配置都集中在 XML 文件中,方便管理和维护。
    • 可读性强: XML 结构清晰,易于理解。
    • 解耦性好: 切面和目标对象完全解耦,不会侵入目标对象的代码。
  • 缺点:
    • 配置繁琐: 需要编写大量的 XML 代码,容易出错。
    • 可维护性差: 当切面较多时,XML 文件会变得非常庞大,难以维护。
    • 不灵活: 修改切面配置需要修改 XML 文件,重新部署应用。

二、注解驱动:后生可畏,青出于蓝

注解驱动就像一位年轻有为的侠客,身手敏捷,招式灵活。它使用注解来定义切面、切点、通知,代码简洁,易于理解。

1. 注解驱动的基本概念

在注解驱动中,主要使用以下几个注解:

  • @Aspect:声明一个类为切面。
  • @Pointcut:定义切点表达式。
  • @Before:定义前置通知。
  • @After:定义后置通知。
  • @AfterReturning:定义返回后通知。
  • @AfterThrowing:定义异常后通知。
  • @Around:定义环绕通知。

2. 注解驱动示例

还是上面的例子,我们用注解来实现。

// UserService.java (不变)
public class UserService {
    public void createUser(String username) {
        System.out.println("Creating user: " + username);
    }
}
// LoggingAspect.java
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Pointcut("execution(* com.example.UserService.createUser(String)) && args(username)")
    public void createUserPointcut(String username) {}

    @Before("createUserPointcut(username)")
    public void beforeCreateUser(String username) {
        System.out.println("[Before] Creating user: " + username);
    }

    @After("createUserPointcut(username)")
    public void afterCreateUser(String username) {
        System.out.println("[After] User created successfully.");
    }
}
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 开启自动代理 -->
    <aop:aspectj-autoproxy/>

    <!-- 扫描组件 -->
    <context:component-scan base-package="com.example"/>

</beans>

代码解释:

  • @Aspect:将 LoggingAspect 类声明为一个切面。
  • @Component:将 LoggingAspect 类声明为一个 Spring Bean,方便 Spring 容器管理。
  • @Pointcut("execution(* com.example.UserService.createUser(String)) && args(username)"):定义一个切点,execution(* com.example.UserService.createUser(String)) 表示匹配 com.example.UserService 类的 createUser 方法,参数类型为 Stringargs(username) 表示将 createUser 方法的 username 参数绑定到切点方法的 username 参数上。
  • @Before("createUserPointcut(username)"):定义一个前置通知,createUserPointcut(username) 表示使用 createUserPointcut 切点,并将 username 参数传递给 beforeCreateUser 方法。
  • @After("createUserPointcut(username)"):定义一个后置通知,createUserPointcut(username) 表示使用 createUserPointcut 切点,并将 username 参数传递给 afterCreateUser 方法。
  • <aop:aspectj-autoproxy/>:开启自动代理,让 Spring 自动扫描带有 @Aspect 注解的类,并创建代理对象。
  • <context:component-scan base-package="com.example"/>:扫描指定包下的所有类,并将带有 @Component 注解的类注册为 Spring Bean。

3. 注解驱动的优点和缺点

  • 优点:
    • 代码简洁: 使用注解代替 XML 配置,代码更加简洁易懂。
    • 可维护性好: 切面代码和业务代码放在一起,方便维护。
    • 灵活: 修改切面配置只需要修改代码,重新编译即可。
  • 缺点:
    • 侵入性: 需要在切面类上添加注解,有一定的侵入性。
    • 可读性稍差: 当切点表达式比较复杂时,可读性会下降。
    • 依赖编译: 修改切面代码需要重新编译。

三、环绕通知:掌控全局,翻云覆雨

无论是XML配置还是注解驱动,环绕通知都是一个非常强大的武器。它可以完全掌控目标方法的执行,包括执行时机、参数、返回值等。就好比你掌握了烤箱的控制权,可以决定什么时候开始烤,烤多久,温度是多少,甚至可以在烤的过程中给烤鸭翻个身。

1. 环绕通知的原理

环绕通知使用 ProceedingJoinPoint 接口来控制目标方法的执行。ProceedingJoinPoint 接口提供了一个 proceed() 方法,用于执行目标方法。你可以在 proceed() 方法执行前后添加自己的逻辑,甚至可以不执行 proceed() 方法,从而阻止目标方法的执行。

2. 环绕通知示例

咱们来个稍微复杂点的例子,假设我们要统计 UserService 类的 createUser 方法的执行时间。

使用注解实现:

// LoggingAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Pointcut("execution(* com.example.UserService.createUser(String))")
    public void createUserPointcut() {}

    @Around("createUserPointcut()")
    public Object timeAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 执行目标方法
        long endTime = System.currentTimeMillis();
        System.out.println("Method " + joinPoint.getSignature().getName() + " execution time: " + (endTime - startTime) + "ms");
        return result;
    }
}

使用 XML 实现:

// LoggingAspect.java
public class LoggingAspect {

    public Object timeAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 执行目标方法
        long endTime = System.currentTimeMillis();
        System.out.println("Method " + joinPoint.getSignature().getName() + " execution time: " + (endTime - startTime) + "ms");
        return result;
    }
}
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 定义 UserService Bean -->
    <bean id="userService" class="com.example.UserService"/>

    <!-- 定义 LoggingAspect Bean -->
    <bean id="loggingAspect" class="com.example.LoggingAspect"/>

    <!-- 配置 AOP -->
    <aop:config>
        <aop:aspect id="logging" ref="loggingAspect">
            <!-- 定义切点:execution(* com.example.UserService.createUser(..)) 表示 UserService 类的 createUser 方法 -->
            <aop:pointcut id="createUserPointcut" expression="execution(* com.example.UserService.createUser(String))"/>

            <!-- 定义环绕通知 -->
            <aop:around method="timeAround" pointcut-ref="createUserPointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

代码解释:

  • @Around("createUserPointcut()"):定义一个环绕通知,createUserPointcut() 表示使用 createUserPointcut 切点。
  • ProceedingJoinPoint joinPoint:环绕通知的参数,用于控制目标方法的执行。
  • joinPoint.proceed():执行目标方法,并返回目标方法的返回值。
  • joinPoint.getSignature().getName():获取目标方法的名称。

3. 环绕通知的注意事项

  • 必须调用 joinPoint.proceed() 方法,否则目标方法不会执行。
  • 可以修改目标方法的参数和返回值,但要谨慎使用,避免影响程序的正确性。
  • 环绕通知的执行顺序:如果有多个环绕通知作用于同一个切点,它们的执行顺序是不确定的。

四、切点表达式:指哪打哪,精准打击

切点表达式是 AOP 的核心,它决定了哪些连接点会被拦截。掌握切点表达式的编写技巧,就像掌握了狙击枪的瞄准镜,可以指哪打哪,精准打击。

1. 切点表达式的语法

Spring AOP 使用 AspectJ 切点表达式语法,它非常强大,可以匹配各种各样的连接点。常用的切点表达式包括:

  • execution(modifier-pattern? return-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?):用于匹配方法的执行。
    • modifier-pattern?:可选的修饰符模式,如 publicprivate 等。
    • return-type-pattern:返回值类型模式,如 voidintString 等,* 表示任意类型。
    • declaring-type-pattern?:可选的声明类型模式,如 com.example.UserService
    • name-pattern:方法名模式,如 createUser* 表示任意方法名。
    • param-pattern:参数模式,如 (String)(int, String)() 表示没有参数,(..) 表示任意参数。
    • throws-pattern?:可选的异常抛出模式,如 throws Exception
  • within(type-pattern):用于匹配指定类型及其子类型的所有连接点。
  • this(type):用于匹配当前执行对象是指定类型的连接点。
  • target(type):用于匹配目标对象是指定类型的连接点。
  • args(argument-pattern):用于匹配方法参数是指定类型的连接点。
  • @target(annotation):用于匹配目标对象类上带有指定注解的连接点。
  • @args(annotation):用于匹配方法参数上带有指定注解的连接点。
  • @within(annotation):用于匹配指定类型及其子类型上带有指定注解的所有连接点。
  • @annotation(annotation):用于匹配方法上带有指定注解的连接点。

2. 切点表达式的组合

可以使用 &&||! 等逻辑运算符来组合切点表达式,实现更复杂的匹配。

3. 切点表达式示例

切点表达式 描述
execution(public * com.example.UserService.createUser(String)) 匹配 com.example.UserService 类的 createUser 方法,该方法是 public 的,返回值类型任意,参数类型是 String
execution(* com.example.UserService.*(..)) 匹配 com.example.UserService 类的所有方法,返回值类型任意,参数类型任意。
within(com.example.service.*) 匹配 com.example.service 包及其子包下的所有类的所有连接点。
this(com.example.UserService) 匹配当前执行对象是 com.example.UserService 类型的连接点。
target(com.example.UserService) 匹配目标对象是 com.example.UserService 类型的连接点。
args(String) 匹配方法参数是 String 类型的连接点。
@target(org.springframework.stereotype.Service) 匹配目标对象类上带有 @Service 注解的连接点。
@args(org.springframework.web.bind.annotation.RequestParam) 匹配方法参数上带有 @RequestParam 注解的连接点。
@within(org.springframework.stereotype.Controller) 匹配指定类型及其子类型上带有 @Controller 注解的所有连接点。
@annotation(com.example.annotation.Loggable) 匹配方法上带有 @Loggable 注解的连接点。

五、总结:殊途同归,各有所长

XML配置和注解驱动都是Spring AOP 的重要实现方式,它们各有优缺点,适用于不同的场景。

特性 XML配置 注解驱动
代码简洁性 较差 较好
可维护性 较差 较好
灵活性 较差 较好
侵入性
适用场景 配置简单,变动较少的切面。 配置复杂,变动频繁的切面。
学习成本 较低 较高(需要熟悉注解和 AspectJ 表达式)

总的来说,如果你喜欢清晰的结构,不喜欢侵入式代码,那么XML配置可能更适合你。如果你追求简洁的代码,喜欢快速开发,那么注解驱动可能更适合你。

当然,在实际项目中,你也可以根据具体情况选择混合使用这两种方式。就好比练武之人,既要练内功,也要练外功,内外兼修才能达到更高的境界。

最后,记住一点:选择哪种方式并不重要,重要的是理解 AOP 的思想,并将其灵活运用到你的项目中,解决实际问题。 祝各位早日练成AOP神功,笑傲江湖!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注