Skip to content

Spring AOP高级特性

在前面的文章中,我们已经介绍了Spring AOP的基本概念、基于XML的配置和基于注解的配置。本文将深入探讨Spring AOP的一些高级特性,这些特性可以帮助我们更好地应用AOP解决复杂的横切关注点问题。

1. 复合切点表达式

在实际应用中,我们经常需要组合多个切点表达式来精确定位连接点。Spring AOP支持使用逻辑运算符(&&、||、!)来组合切点表达式。

基于XML的复合切点

xml
<aop:config>
    <aop:pointcut id="serviceMethodPointcut" 
                 expression="execution(* com.example.service.*.*(..))"/>
    
    <aop:pointcut id="transactionalMethodPointcut" 
                 expression="@annotation(org.springframework.transaction.annotation.Transactional)"/>
    
    <aop:pointcut id="compositePointcut" 
                 expression="serviceMethodPointcut() && transactionalMethodPointcut()"/>
    
    <aop:aspect ref="loggingAspect">
        <aop:before pointcut-ref="compositePointcut" method="logBefore"/>
    </aop:aspect>
</aop:config>

基于注解的复合切点

java
@Aspect
@Component
public class LoggingAspect {
    
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethod() {}
    
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void transactionalMethod() {}
    
    @Pointcut("serviceMethod() && transactionalMethod()")
    public void serviceTransactionalMethod() {}
    
    @Before("serviceTransactionalMethod()")
    public void logBefore(JoinPoint joinPoint) {
        // 实现...
    }
}

这些示例展示了如何组合切点表达式来匹配同时满足多个条件的连接点,例如"所有服务类中带有@Transactional注解的方法"。

2. 切点参数绑定

Spring AOP允许从切点表达式中捕获参数并将其传递给通知方法。这在需要访问方法参数或其他上下文信息时非常有用。

参数绑定示例

java
@Aspect
@Component
public class SecurityAspect {
    
    @Pointcut("execution(* com.example.service.UserService.*(..)) && args(username,..)")
    public void userServiceMethods(String username) {}
    
    @Before("userServiceMethods(username)")
    public void checkAccess(JoinPoint joinPoint, String username) {
        System.out.println("Checking access for user: " + username);
        
        // 权限检查逻辑
        if ("admin".equals(username)) {
            System.out.println("Admin access granted");
        } else {
            System.out.println("Regular user access");
        }
    }
}

在上面的例子中,我们捕获了UserService方法的第一个参数(假设它是一个String类型的用户名),并将其传递给前置通知方法。

注解参数绑定

也可以绑定方法或类上的注解:

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Secured {
    String[] roles();
}

@Aspect
@Component
public class SecurityAspect {
    
    @Before("execution(* com.example.service.*.*(..)) && @annotation(secured)")
    public void checkSecurity(JoinPoint joinPoint, Secured secured) {
        String[] roles = secured.roles();
        
        // 检查当前用户是否拥有所需角色
        for (String role : roles) {
            // 实现角色检查逻辑
            System.out.println("Checking for role: " + role);
        }
    }
}

使用示例:

java
@Service
public class UserService {
    
    @Secured(roles = {"ADMIN", "USER_MANAGER"})
    public void deleteUser(String username) {
        // 实现...
    }
}

3. 多切面协同工作

在实际应用中,我们可能需要多个切面在同一个连接点上协同工作。例如,可能需要事务管理、日志记录和安全检查等多个切面应用于同一个方法。

切面顺序控制

当多个切面应用于同一个连接点时,可以使用@Order注解或实现Ordered接口来控制它们的执行顺序:

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

@Aspect
@Component
@Order(2) // 第二优先级
public class TransactionAspect {
    // 实现...
}

@Aspect
@Component
@Order(3) // 最低优先级
public class LoggingAspect {
    // 实现...
}

在XML配置中,可以使用order属性:

xml
<aop:config>
    <aop:aspect id="securityAspect" ref="securityAspect" order="1">
        <!-- 实现... -->
    </aop:aspect>
    
    <aop:aspect id="transactionAspect" ref="transactionAspect" order="2">
        <!-- 实现... -->
    </aop:aspect>
    
    <aop:aspect id="loggingAspect" ref="loggingAspect" order="3">
        <!-- 实现... -->
    </aop:aspect>
</aop:config>

切面执行顺序

对于多个切面,执行顺序如下:

  1. 优先级高的切面的前置通知先执行
  2. 优先级低的切面的后置、返回或异常通知先执行
  3. 环绕通知遵循前置和后置通知的规则

示例执行顺序:

SecurityAspect - @Before
TransactionAspect - @Before
LoggingAspect - @Before
目标方法执行
LoggingAspect - @After/@AfterReturning
TransactionAspect - @After/@AfterReturning
SecurityAspect - @After/@AfterReturning

4. 自定义注解驱动的切面

创建自定义注解是实现AOP的一种强大方式,它让我们可以声明式地标记需要应用切面的方法或类。

自定义日志注解

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

实现切面

java
@Aspect
@Component
public class ExecutionTimeAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(ExecutionTimeAspect.class);
    
    @Around("@annotation(logExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint, LogExecutionTime logExecutionTime) throws Throwable {
        long startTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().toShortString();
        LogLevel level = logExecutionTime.value();
        
        try {
            return joinPoint.proceed();
        } finally {
            long executionTime = System.currentTimeMillis() - startTime;
            
            switch (level) {
                case DEBUG:
                    logger.debug("{} executed in {} ms", methodName, executionTime);
                    break;
                case INFO:
                    logger.info("{} executed in {} ms", methodName, executionTime);
                    break;
                case WARN:
                    logger.warn("{} executed in {} ms", methodName, executionTime);
                    break;
                case ERROR:
                    logger.error("{} executed in {} ms", methodName, executionTime);
                    break;
            }
        }
    }
}

使用自定义注解

java
@Service
public class UserService {
    
    @LogExecutionTime(LogLevel.DEBUG)
    public User findById(Long id) {
        // 实现...
    }
    
    @LogExecutionTime
    public List<User> findAll() {
        // 实现...
    }
}

5. 动态切点

动态切点是在运行时根据条件决定是否匹配连接点的切点。Spring AOP允许我们实现org.springframework.aop.Pointcut接口来创建自定义的动态切点。

实现动态切点

java
public class TimeBasedPointcut implements Pointcut {
    
    private final MethodMatcher methodMatcher = new TimeBasedMethodMatcher();
    
    @Override
    public ClassFilter getClassFilter() {
        return ClassFilter.TRUE; // 匹配所有类
    }
    
    @Override
    public MethodMatcher getMethodMatcher() {
        return methodMatcher;
    }
    
    private static class TimeBasedMethodMatcher implements MethodMatcher {
        
        @Override
        public boolean matches(Method method, Class<?> targetClass) {
            // 静态部分匹配,例如检查方法名或注解
            return method.getName().startsWith("find");
        }
        
        @Override
        public boolean isRuntime() {
            return true; // 表示需要运行时匹配
        }
        
        @Override
        public boolean matches(Method method, Class<?> targetClass, Object... args) {
            // 运行时匹配,例如基于当前时间或系统负载
            Calendar cal = Calendar.getInstance();
            int hour = cal.get(Calendar.HOUR_OF_DAY);
            
            // 在工作时间(8:00-18:00)执行
            return hour >= 8 && hour < 18;
        }
    }
}

注册动态切点

可以在Java配置中注册动态切点:

java
@Configuration
public class AopConfig {
    
    @Bean
    public Advisor timeBasedAdvisor() {
        TimeBasedPointcut pointcut = new TimeBasedPointcut();
        return new DefaultPointcutAdvisor(pointcut, new TimeBasedAdvice());
    }
    
    @Bean
    public TimeBasedAdvice timeBasedAdvice() {
        return new TimeBasedAdvice();
    }
}

public class TimeBasedAdvice implements MethodInterceptor {
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Executing " + invocation.getMethod().getName() + " during working hours");
        return invocation.proceed();
    }
}

6. 编程式AOP

虽然声明式AOP(XML或注解)是Spring AOP的主要使用方式,但Spring也支持编程式AOP,允许在代码中创建和应用切面。

使用ProxyFactory

java
@Component
public class ProgrammaticAopExample {
    
    public Object createProxy(Object target) {
        ProxyFactory factory = new ProxyFactory(target);
        
        // 添加通知
        factory.addAdvice(new PerformanceInterceptor());
        factory.addAdvice(new SecurityInterceptor());
        
        // 设置代理目标类,而不是接口
        factory.setProxyTargetClass(true);
        
        // 创建代理
        return factory.getProxy();
    }
}

class PerformanceInterceptor implements MethodInterceptor {
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        
        try {
            return invocation.proceed();
        } finally {
            long end = System.currentTimeMillis();
            System.out.println(invocation.getMethod().getName() + " took " + (end - start) + " ms");
        }
    }
}

class SecurityInterceptor implements MethodInterceptor {
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Security check before " + invocation.getMethod().getName());
        return invocation.proceed();
    }
}

在Spring Bean中使用

java
@Service
public class UserServiceManager {
    
    private final ProgrammaticAopExample aopExample;
    private final UserService userService;
    
    public UserServiceManager(ProgrammaticAopExample aopExample, UserService userService) {
        this.aopExample = aopExample;
        this.userService = userService;
    }
    
    public UserService getSecureUserService() {
        return (UserService) aopExample.createProxy(userService);
    }
}

7. 引入(Introduction)高级用法

引入允许我们向现有类添加新的方法或接口,而不修改这些类的源代码。

自定义引入接口

java
public interface Monitorable {
    boolean isMonitored();
    void setMonitored(boolean monitored);
}

public class DefaultMonitorable implements Monitorable {
    private boolean monitored = false;
    
    @Override
    public boolean isMonitored() {
        return monitored;
    }
    
    @Override
    public void setMonitored(boolean monitored) {
        this.monitored = monitored;
    }
}

基于注解的引入

java
@Aspect
@Component
public class MonitoringAspect {
    
    @DeclareParents(value = "com.example.service.*+", defaultImpl = DefaultMonitorable.class)
    public static Monitorable monitorable;
}

编程式引入

java
@Component
public class ProgrammaticIntroductionExample {
    
    public Object addMonitoringSupport(Object target) {
        ProxyFactory factory = new ProxyFactory(target);
        
        // 添加引入
        factory.addAdvice(new MonitoringIntroductionInterceptor());
        factory.setProxyTargetClass(true);
        
        return factory.getProxy();
    }
}

class MonitoringIntroductionInterceptor extends DelegatingIntroductionInterceptor implements Monitorable {
    private boolean monitored = false;
    
    @Override
    public boolean isMonitored() {
        return monitored;
    }
    
    @Override
    public void setMonitored(boolean monitored) {
        this.monitored = monitored;
    }
}

高级用法示例

结合监控和性能跟踪:

java
@Aspect
@Component
public class MonitoringAspect {
    
    @DeclareParents(value = "com.example.service.*+", defaultImpl = DefaultMonitorable.class)
    public static Monitorable monitorable;
    
    @Around("execution(* com.example.service.*.*(..)) && this(monitorable)")
    public Object monitor(ProceedingJoinPoint joinPoint, Monitorable monitorable) throws Throwable {
        if (!monitorable.isMonitored()) {
            return joinPoint.proceed();
        }
        
        String methodName = joinPoint.getSignature().toShortString();
        System.out.println("Monitoring " + methodName + " execution");
        
        long start = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            long end = System.currentTimeMillis();
            
            System.out.println(methodName + " completed in " + (end - start) + " ms");
            return result;
        } catch (Exception e) {
            System.out.println(methodName + " failed with: " + e.getMessage());
            throw e;
        }
    }
}

使用示例:

java
@Service
public class ServiceClient {
    
    private final UserService userService;
    
    public ServiceClient(UserService userService) {
        this.userService = userService;
    }
    
    public void process() {
        // 启用监控
        ((Monitorable) userService).setMonitored(true);
        
        // 调用服务方法,将触发监控
        userService.findAll();
        
        // 禁用监控
        ((Monitorable) userService).setMonitored(false);
        
        // 不会触发监控
        userService.findById(1L);
    }
}

8. 与Spring集成的AspectJ支持

虽然Spring AOP功能强大,但它有一些限制,如只能代理Spring管理的bean,只支持方法连接点等。Spring提供了与AspectJ的集成,以支持更强大的AOP功能。

启用AspectJ加载时织入

spring-aspects依赖中,Spring提供了与AspectJ的集成支持。要启用AspectJ的加载时织入(LTW),需要在配置中添加:

xml
<context:load-time-weaver/>

或在Java配置中:

java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
    // 配置...
}

还需要配置META-INF/aop.xml文件:

xml
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <weaver>
        <include within="com.example..*"/>
    </weaver>
    
    <aspects>
        <aspect name="com.example.aspect.PerformanceAspect"/>
        <aspect name="com.example.aspect.SecurityAspect"/>
    </aspects>
</aspectj>

使用@Configurable

AspectJ的一个重要用法是使用@Configurable注解来为非Spring管理的对象提供依赖注入:

java
@Configurable
public class User {
    
    @Autowired
    private transient UserService userService;
    
    private Long id;
    private String username;
    
    public User(Long id, String username) {
        this.id = id;
        this.username = username;
    }
    
    public void process() {
        // 可以使用注入的服务
        userService.process(this);
    }
}

启用@Configurable支持:

java
@Configuration
@EnableSpringConfigured
public class AppConfig {
    // 配置...
}

这样,即使User对象是用new User()创建的,而不是由Spring容器管理,它仍然可以使用依赖注入。

9. AOP代理的限制和解决方案

使用Spring AOP时,需要了解一些限制和解决方案:

限制1: 自调用问题

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

解决方案:

  1. 使用AspectJ代替Spring AOP
  2. 注入自身代理:
java
@Service
public class UserService {
    
    @Autowired
    @Lazy
    private UserService self;
    
    public void process() {
        // 调用会触发AOP
        self.save();
    }
    
    @Transactional
    public void save() {
        // 业务逻辑...
    }
}

限制2: final方法和类

Spring AOP不能代理final方法和类。

解决方案:

  1. 避免使用final
  2. 使用AspectJ代替Spring AOP

限制3: 接口与实现分离

当使用JDK动态代理时,通知只能应用于接口定义的方法,不能应用于实现类中的额外方法。

解决方案:

  1. 确保所有方法都在接口中定义
  2. 强制使用CGLIB代理:
java
@EnableAspectJAutoProxy(proxyTargetClass = true)

10. 实际案例:全面的安全和审计系统

以下是一个结合多个高级AOP特性的实际案例,实现了安全检查和审计日志:

安全注解

java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Secured {
    String[] roles();
    boolean auditable() default true;
}

审计注解

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Audited {
    String action();
    String resource();
    boolean includeReturn() default false;
    boolean includeArgs() default true;
}

安全切面

java
@Aspect
@Component
@Order(1)
public class SecurityAspect {
    
    private final SecurityService securityService;
    
    public SecurityAspect(SecurityService securityService) {
        this.securityService = securityService;
    }
    
    @Before("@annotation(secured)")
    public void checkSecurity(JoinPoint joinPoint, Secured secured) {
        String[] requiredRoles = secured.roles();
        String methodName = joinPoint.getSignature().toShortString();
        
        if (!securityService.hasAnyRole(requiredRoles)) {
            throw new AccessDeniedException("Access denied to " + methodName + 
                                          ". Required roles: " + Arrays.toString(requiredRoles));
        }
    }
}

审计切面

java
@Aspect
@Component
@Order(2)
public class AuditAspect {
    
    private final AuditService auditService;
    private final SecurityService securityService;
    
    public AuditAspect(AuditService auditService, SecurityService securityService) {
        this.auditService = auditService;
        this.securityService = securityService;
    }
    
    // 处理@Secured注解的审计
    @AfterReturning(pointcut = "@annotation(secured) && execution(* com.example..*(..))", 
                   returning = "result")
    public void auditSecured(JoinPoint joinPoint, Secured secured, Object result) {
        if (secured.auditable()) {
            String methodName = joinPoint.getSignature().toShortString();
            
            auditService.recordAudit(
                securityService.getCurrentUser(),
                "ACCESS",
                methodName,
                Arrays.toString(joinPoint.getArgs()),
                null
            );
        }
    }
    
    // 处理@Audited注解的审计
    @AfterReturning(pointcut = "@annotation(audited)", returning = "result")
    public void auditMethod(JoinPoint joinPoint, Audited audited, Object result) {
        String returnValue = audited.includeReturn() && result != null ? 
                            result.toString() : null;
                            
        String args = audited.includeArgs() ? 
                    Arrays.toString(joinPoint.getArgs()) : null;
        
        auditService.recordAudit(
            securityService.getCurrentUser(),
            audited.action(),
            audited.resource(),
            args,
            returnValue
        );
    }
    
    // 审计异常
    @AfterThrowing(pointcut = "@annotation(audited)", throwing = "ex")
    public void auditException(JoinPoint joinPoint, Audited audited, Exception ex) {
        auditService.recordAudit(
            securityService.getCurrentUser(),
            "EXCEPTION",
            audited.resource(),
            ex.getClass().getName(),
            ex.getMessage()
        );
    }
}

使用示例

java
@Service
public class UserService {
    
    @Secured(roles = {"ADMIN", "USER_MANAGER"}, auditable = true)
    @Audited(action = "DELETE", resource = "USER")
    public void deleteUser(Long userId) {
        // 实现...
    }
    
    @Secured(roles = {"ADMIN", "USER", "USER_MANAGER"})
    @Audited(action = "VIEW", resource = "USER")
    public User getUser(Long userId) {
        // 实现...
    }
    
    @Secured(roles = {"ADMIN"})
    @Audited(action = "UPDATE", resource = "SYSTEM_SETTINGS", includeArgs = true, includeReturn = true)
    public boolean updateSystemSettings(Map<String, Object> settings) {
        // 实现...
    }
}

总结

Spring AOP的高级特性为解决复杂的横切关注点提供了强大的工具。通过复合切点表达式、参数绑定、多切面协同工作、自定义注解驱动的切面、动态切点、编程式AOP、高级引入用法以及与AspectJ的集成,我们可以构建出灵活且功能强大的AOP解决方案。

在实际应用中,合理利用这些高级特性可以帮助我们构建更加模块化、可维护的应用程序,同时避免代码重复和关注点混乱。然而,需要注意的是,过度使用AOP可能会增加系统的复杂性和调试难度,因此应根据实际需求适度使用。