Appearance
基于注解的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");
}
}
}
最佳实践
- 保持切面简单:每个切面应该只关注单一的横切关注点,遵循单一职责原则
- 合理使用切点:创建可重用的切点定义,避免在每个通知中重复切点表达式
- 注意切面顺序:使用
@Order
注解明确指定切面的优先级,特别是当多个切面相互依赖时 - 避免在通知中抛出异常:除非是故意中断执行流程,否则应该捕获并处理通知中的异常
- 记录通知执行信息:在通知中使用适当的日志级别记录信息,便于调试和监控
- 注意性能影响:AOP虽然方便,但会带来额外的性能开销,不要过度使用
- 选择合适的通知类型:根据需求选择最合适的通知类型,不要默认使用环绕通知
- 使用自定义注解:通过自定义注解来标记需要应用切面的方法,提高代码的可读性和可维护性
常见问题及解决方案
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可以显著提高代码的模块化程度,使应用更加健壮和易于维护。