Skip to content

基于注解的AOP配置

Spring AOP提供了两种配置方式:基于XML的配置和基于注解的配置。本文将介绍如何使用注解配置方式实现AOP功能,相比XML配置,注解配置更加简洁、直观,且与代码紧密结合。

启用注解AOP支持

要在Spring中使用基于注解的AOP,首先需要启用AspectJ自动代理:

在XML配置中启用

xml
<?xml version="1.0" encoding="UTF-8"?>
<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">
                          
    <!-- 启用AspectJ自动代理 -->
    <aop:aspectj-autoproxy/>
    
    <!-- Bean定义 -->
    
</beans>

在Java配置中启用

java
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    // 配置内容
}

必要的依赖

在Maven项目中,需要添加以下依赖:

xml
<dependencies>
    <!-- Spring Core & Context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.20</version>
    </dependency>
    
    <!-- Spring AOP -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>5.3.20</version>
    </dependency>
    
    <!-- AspectJ runtime, required for AOP -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.9.9.1</version>
    </dependency>
    
    <!-- AspectJ weaver -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.9.1</version>
    </dependency>
</dependencies>

创建切面

使用注解配置AOP时,切面是一个带有@Aspect注解的普通Java类:

java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component  // 确保切面类被Spring容器管理
public class LoggingAspect {
    
    // 切面内容...
    
}

定义切点

切点使用@Pointcut注解定义,可以在切面类内部定义多个切点:

java
@Aspect
@Component
public class LoggingAspect {
    
    // 定义一个切点,匹配service包中所有类的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}
    
    // 定义一个切点,匹配带有@Loggable注解的方法
    @Pointcut("@annotation(com.example.annotation.Loggable)")
    public void loggableMethods() {}
    
    // 定义一个切点,匹配controller包中所有类的所有方法
    @Pointcut("within(com.example.controller..*)")
    public void controllerMethods() {}
    
    // 组合切点
    @Pointcut("serviceMethods() || controllerMethods()")
    public void serviceOrControllerMethods() {}
}

定义通知

Spring AOP支持五种类型的通知,使用不同的注解来定义:

前置通知(Before Advice)

java
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Before executing: " + methodName);
}

后置通知(After Advice)

java
@After("serviceMethods()")
public void logAfter(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("After executing: " + methodName);
}

返回通知(After Returning Advice)

java
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Method " + methodName + " returned: " + result);
}

异常通知(After Throwing Advice)

java
@AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Method " + methodName + " threw exception: " + ex.getMessage());
}

环绕通知(Around Advice)

java
@Around("serviceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    String methodName = joinPoint.getSignature().getName();
    
    System.out.println("Around before method: " + methodName);
    long startTime = System.currentTimeMillis();
    
    try {
        // 执行目标方法
        Object result = joinPoint.proceed();
        
        long endTime = System.currentTimeMillis();
        System.out.println("Method " + methodName + " executed in " + (endTime - startTime) + "ms");
        System.out.println("Around after method: " + methodName);
        
        return result;
    } catch (Exception e) {
        System.out.println("Around after throwing: " + e.getMessage());
        throw e;
    }
}

完整的切面示例

以下是一个完整的日志切面示例,包含所有类型的通知:

java
package com.example.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    
    // 定义切点
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}
    
    // 前置通知
    @Before("serviceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Before executing: " + methodName);
    }
    
    // 后置通知
    @After("serviceMethods()")
    public void logAfter(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("After executing: " + methodName);
    }
    
    // 返回通知
    @AfterReturning(pointcut = "serviceMethods()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Method " + methodName + " returned: " + result);
    }
    
    // 异常通知
    @AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
    public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Method " + methodName + " threw exception: " + ex.getMessage());
    }
    
    // 环绕通知
    @Around("serviceMethods()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        
        System.out.println("Around before method: " + methodName);
        long startTime = System.currentTimeMillis();
        
        try {
            // 执行目标方法
            Object result = joinPoint.proceed();
            
            long endTime = System.currentTimeMillis();
            System.out.println("Method " + methodName + " executed in " + (endTime - startTime) + "ms");
            System.out.println("Around after method: " + methodName);
            
            return result;
        } catch (Exception e) {
            System.out.println("Around after throwing: " + e.getMessage());
            throw e;
        }
    }
}

目标类和服务类

为了测试AOP功能,我们需要一些目标服务类:

java
package com.example.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
    
    public void addUser(String username) {
        System.out.println("Adding user: " + username);
        // 业务逻辑...
    }
    
    public void updateUser(String username) {
        System.out.println("Updating user: " + username);
        // 业务逻辑...
    }
    
    public String getUser(String userId) {
        System.out.println("Getting user with ID: " + userId);
        // 业务逻辑...
        return "User:" + userId;
    }
    
    public void deleteUser(String userId) {
        if (userId == null) {
            throw new IllegalArgumentException("User ID cannot be null");
        }
        System.out.println("Deleting user with ID: " + userId);
        // 业务逻辑...
    }
}

高级特性

1. 自定义注解切点

可以创建自定义注解并使用它来定义切点:

java
package com.example.annotation;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Loggable {
}

然后使用该注解标记需要日志记录的方法:

java
@Service
public class UserService {
    
    @Loggable
    public void addUser(String username) {
        System.out.println("Adding user: " + username);
    }
    
    // 其他方法...
}

在切面中定义针对该注解的切点和通知:

java
@Aspect
@Component
public class AnnotationLoggingAspect {
    
    @Pointcut("@annotation(com.example.annotation.Loggable)")
    public void loggableMethods() {}
    
    @Around("loggableMethods()")
    public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 环绕通知逻辑...
    }
}

2. 获取注解属性

如果注解带有属性,可以在通知中获取这些属性值:

java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Loggable {
    LogLevel value() default LogLevel.INFO;
    
    enum LogLevel {
        INFO, DEBUG, WARN, ERROR
    }
}

在通知中获取注解属性:

java
@Around("@annotation(loggable)")
public Object logWithLevel(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
    LogLevel level = loggable.value();
    
    // 根据级别记录日志
    switch (level) {
        case DEBUG:
            System.out.println("DEBUG: Before executing " + joinPoint.getSignature().getName());
            break;
        case INFO:
            System.out.println("INFO: Before executing " + joinPoint.getSignature().getName());
            break;
        // 其他级别...
    }
    
    try {
        Object result = joinPoint.proceed();
        return result;
    } finally {
        // 记录方法执行后日志...
    }
}

3. 切面优先级

当多个切面都需要在同一个连接点执行通知时,可以使用@Order注解指定切面的优先级:

java
@Aspect
@Component
@Order(1)  // 优先级高的切面
public class SecurityAspect {
    // 实现...
}

@Aspect
@Component
@Order(2)  // 优先级低的切面
public class LoggingAspect {
    // 实现...
}

数值越小,优先级越高。在上面的例子中,SecurityAspect的前置通知会在LoggingAspect的前置通知之前执行,而LoggingAspect的后置通知会在SecurityAspect的后置通知之前执行。

4. 引入(Introduction)

引入允许向现有的类添加新方法或属性,使其实现额外的接口:

java
package com.example.monitoring;

public interface Monitorable {
    void startMonitoring();
    void stopMonitoring();
}

package com.example.monitoring;

public class DefaultMonitorable implements Monitorable {
    @Override
    public void startMonitoring() {
        System.out.println("Starting monitoring...");
    }
    
    @Override
    public void stopMonitoring() {
        System.out.println("Stopping monitoring...");
    }
}

在切面中使用@DeclareParents注解实现引入:

java
@Aspect
@Component
public class MonitoringAspect {
    
    @DeclareParents(value = "com.example.service.*+", defaultImpl = DefaultMonitorable.class)
    public static Monitorable monitorable;
    
    @Before("execution(* com.example.service.*.*(..)) && this(monitorable)")
    public void recordUsage(Monitorable monitorable) {
        monitorable.startMonitoring();
    }
    
    @After("execution(* com.example.service.*.*(..)) && this(monitorable)")
    public void endMonitoring(Monitorable monitorable) {
        monitorable.stopMonitoring();
    }
}

5. 代理模式配置

Spring AOP默认会根据目标类是否实现接口自动选择使用JDK动态代理还是CGLIB代理。可以在启用AspectJ自动代理时强制使用CGLIB代理:

java
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)  // 强制使用CGLIB代理
public class AppConfig {
    // 配置内容
}

或在XML中:

xml
<aop:aspectj-autoproxy proxy-target-class="true"/>

Spring Boot中的AOP配置

在Spring Boot应用中配置AOP更加简单,只需添加依赖并使用注解:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Spring Boot会自动启用AspectJ自动代理,无需显式配置。然后可以直接创建带有@Aspect注解的切面类:

java
@Aspect
@Component
public class LoggingAspect {
    // 切面实现...
}

实际应用示例

1. 性能监控切面

java
@Aspect
@Component
public class PerformanceMonitoringAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringAspect.class);
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        try {
            return joinPoint.proceed();
        } finally {
            long executionTime = System.currentTimeMillis() - start;
            String methodName = joinPoint.getSignature().toShortString();
            
            logger.info("{} executed in {} ms", methodName, executionTime);
            
            // 如果执行时间超过阈值,记录警告
            if (executionTime > 1000) {
                logger.warn("{} execution time exceeded threshold: {} ms", methodName, executionTime);
            }
        }
    }
}

2. 审计日志切面

java
@Aspect
@Component
public class AuditLogAspect {
    
    private final AuditLogService auditLogService;
    
    public AuditLogAspect(AuditLogService auditLogService) {
        this.auditLogService = auditLogService;
    }
    
    @Pointcut("@annotation(com.example.annotation.Auditable)")
    public void auditableMethods() {}
    
    @AfterReturning(pointcut = "auditableMethods() && @annotation(auditable)", returning = "result")
    public void logAuditEvent(JoinPoint joinPoint, Auditable auditable, Object result) {
        String principal = SecurityContextHolder.getContext().getAuthentication().getName();
        String action = auditable.action();
        String resource = auditable.resource();
        
        auditLogService.recordAudit(
            principal,
            action,
            resource,
            joinPoint.getSignature().toShortString(),
            Arrays.toString(joinPoint.getArgs()),
            result != null ? result.toString() : null
        );
    }
}

对应的注解定义:

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Auditable {
    String action();
    String resource();
}

3. 安全切面

java
@Aspect
@Component
@Order(1)  // 确保安全检查在其他切面之前执行
public class SecurityAspect {
    
    @Before("execution(* com.example.service.*.*(..)) && @annotation(secured)")
    public void checkSecurity(JoinPoint joinPoint, Secured secured) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication == null) {
            throw new AccessDeniedException("No authentication found");
        }
        
        boolean hasAnyRole = false;
        for (String role : secured.value()) {
            if (authentication.getAuthorities().stream()
                    .anyMatch(a -> a.getAuthority().equals("ROLE_" + role))) {
                hasAnyRole = true;
                break;
            }
        }
        
        if (!hasAnyRole) {
            throw new AccessDeniedException("Access denied");
        }
    }
}

最佳实践

  1. 保持切面简单:每个切面应该只关注单一的横切关注点,遵循单一职责原则
  2. 合理使用切点:创建可重用的切点定义,避免在每个通知中重复切点表达式
  3. 注意切面顺序:使用@Order注解明确指定切面的优先级,特别是当多个切面相互依赖时
  4. 避免在通知中抛出异常:除非是故意中断执行流程,否则应该捕获并处理通知中的异常
  5. 记录通知执行信息:在通知中使用适当的日志级别记录信息,便于调试和监控
  6. 注意性能影响:AOP虽然方便,但会带来额外的性能开销,不要过度使用
  7. 选择合适的通知类型:根据需求选择最合适的通知类型,不要默认使用环绕通知
  8. 使用自定义注解:通过自定义注解来标记需要应用切面的方法,提高代码的可读性和可维护性

常见问题及解决方案

1. 代理类型问题

问题:使用this指示器时,如果目标类没有实现接口并且使用JDK动态代理,可能会出现类型转换错误。

解决方案:使用@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用CGLIB代理。

2. 同一类内部方法调用问题

问题:当在同一个类中一个方法调用另一个方法时,AOP通知不会应用于被调用的方法。

解决方案

  • 将方法移到另一个类中
  • 获取当前代理对象并通过代理调用方法:
java
@Service
public class UserService {
    
    @Autowired
    private ApplicationContext context;
    
    public void process() {
        // 自调用会触发AOP
        UserService proxy = context.getBean(UserService.class);
        proxy.save();
    }
    
    @Transactional
    public void save() {
        // 业务逻辑...
    }
}

3. 切面循环依赖问题

问题:当切面和目标对象之间存在循环依赖时,可能会导致初始化错误。

解决方案

  • 重构代码,消除循环依赖
  • 使用延迟注入(@Lazy
  • 使用字段或方法注入代替构造函数注入

总结

基于注解的AOP配置提供了一种简洁、直观的方式来定义切面、切点和通知。相比XML配置,注解配置与代码更紧密结合,更易于维护和理解。

通过本文的介绍,我们了解了如何使用@Aspect@Pointcut@Before@After@AfterReturning@AfterThrowing@Around等注解来配置AOP功能,以及如何应用一些高级特性如引入、切面优先级和自定义注解切点等。

在实际项目中,注解配置是使用Spring AOP的主流方式,特别是在Spring Boot应用中。合理使用AOP可以显著提高代码的模块化程度,使应用更加健壮和易于维护。