概念说明
Spring Boot中的AOP(面向切面编程)是一种编程范式,它允许开发者定义跨多个对象的横切关注点。横切关注点是与业务逻辑无关的功能,如日志记录、安全检查、事务管理等。AOP的主要目的是将横切关注点与业务逻辑分离,提高代码的模块化和可维护性。
AOP的核心概念包括:
- 切面(Aspect):横切关注点的模块化。切面通常表示为一个Java类,用于封装横切关注点的代码。
- 连接点(Joinpoint):程序执行过程中的某个特定点,如方法的执行或异常的抛出。在Spring AOP中,连接点总是方法的执行。
- 通知(Advice):切面在特定连接点上执行的动作。Spring AOP支持五种类型的通知:
- 前置通知(Before):在连接点之前执行。
- 后置通知(After):在连接点之后执行。
- 返回通知(After Returning):在连接点正常完成后执行。
- 异常通知(After Throwing):如果连接点抛出异常,则执行。
- 环绕通知(Around):包围一个连接点的通知,可以在方法执行前后执行自定义行为。
- 切点(Pointcut):用于匹配连接点的断言。切点用于确定哪些连接点应该被通知所拦截。
- 引入(Introduction):允许声明额外的方法或字段。
- 目标对象(Target Object):被一个或多个切面所通知的对象。
- AOP代理(AOP Proxy):框架创建的对象,用于实现切面的契约。
- 织入(Weaving):将切面与其他应用程序类型或对象链接在一起的过程。在Spring AOP中,织入通常在运行时通过代理对象完成。
AOP在代码中的应用
以下几个例子是AOP在实际代码中的应用,通过它们可以更好的理解AOP:
- 日志切面:用于记录方法的进入和退出,以及方法的执行时间。
@Aspect @Component public class LoggingAspect { @Before("execution(* com.example..*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("进入方法: " + joinPoint.getSignature().getName()); } @AfterReturning(pointcut = "execution(* com.example..*(..))", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println("退出方法: " + joinPoint.getSignature().getName() + ",结果: " + result); } @AfterThrowing(pointcut = "execution(* com.example..*(..))", throwing = "e") public void logAfterThrowing(JoinPoint joinPoint, Throwable e) { System.out.println("方法: " + joinPoint.getSignature().getName() + " 发生异常: " + e); } }
- 性能监控切面:用于测量方法执行的时间。
@Aspect @Component public class PerformanceAspect { @Around("execution(* com.example..*(..))") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object proceed = joinPoint.proceed(); long executionTime = System.currentTimeMillis() - start; System.out.println(joinPoint.getSignature() + " 执行耗时 " + executionTime + "ms"); return proceed; } }
- 安全检查切面:用于在方法执行前检查用户权限。
@Aspect @Component public class SecurityAspect { @Before("execution(* com.example..*(..))") public void checkSecurity(JoinPoint joinPoint) { // 模拟安全检查逻辑 System.out.println("对方法: " + joinPoint.getSignature().getName() + " 进行安全检查"); // 如果没有权限,可以抛出异常或返回特定结果 } }
- 事务管理切面:用于管理数据库事务。
@Aspect @Component public class TransactionAspect { @Before("execution(* com.example.service..*(..))") public void startTransaction() { // 开始数据库事务 System.out.println("开始事务"); } @AfterReturning("execution(* com.example.service..*(..))") public void commitTransaction() { // 提交数据库事务 System.out.println("提交事务"); } @AfterThrowing("execution(* com.example.service..*(..))") public void rollbackTransaction() { // 回滚数据库事务 System.out.println("回滚事务"); } }
- 缓存切面:用于缓存方法的返回结果,以减少数据库查询次数。
@Aspect @Component public class CachingAspect { private Map<String, Object> cache = new HashMap<>(); @Around("execution(* com.example.service..*(..))") public Object cacheResult(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); List<Object> params = Arrays.asList(joinPoint.getArgs()); String cacheKey = methodName + "_" + params.hashCode(); if (cache.containsKey(cacheKey)) { System.out.println("返回缓存结果: " + cacheKey); return cache.get(cacheKey); } Object result = joinPoint.proceed(); cache.put(cacheKey, result); return result; } }
在这些示例中,@Aspect
注解表示该类是一个切面,@Component
注解将切面注册为 Spring 容器的一个 Bean,使其能够被自动发现并织入到应用程序中。切点(Pointcut)用于定义通知(Advice)应该应用到哪些连接点(Joinpoint,通常是方法执行)。通过使用不同的通知类型(如@Before、@AfterReturning、@Around等),可以控制横切关注点在方法执行的哪个阶段被织入。
结合自定义注解使用AOP
下面我们自定义一个注释@LogExecutionTime
,添加该注释的方法会在执行结束时在日志中输出该方法的执行时间:
步骤 1: 定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 注解应用的地方(方法)
@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期(运行时)
public @interface LogExecutionTime {
// 可以定义一些属性,例如是否启用日志
boolean enabled() default true;
}
步骤 2: 创建切面
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogExecutionTimeAspect {
private static final Logger logger = LoggerFactory.getLogger(LogExecutionTimeAspect.class);
@Pointcut("@annotation(logExecutionTime)")
public void logExecutionTimePointcut(LogExecutionTime logExecutionTime) {
// 定义切点
}
@AfterReturning(pointcut = "logExecutionTimePointcut(logExecutionTime)", returning = "result")
public void logExecutionTime(JoinPoint joinPoint, LogExecutionTime logExecutionTime, Object result) {
// 检查是否启用了日志
if (logExecutionTime.enabled()) {
long startTime = System.currentTimeMillis();
// 方法执行结束,计算执行时间
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 输出日志
logger.info(joinPoint.getSignature() + " executed in " + executionTime + "ms");
}
}
}
步骤 3: 应用注解
在你的服务或控制器方法上使用@LogExecutionTime
注解:
import org.springframework.stereotype.Service;
@Service
public class MyService {
@LogExecutionTime(enabled = true)
public void methodToLog() {
// 需要记录执行时间的方法逻辑
}
}
在这个例子中,@LogExecutionTime
注解被用于标记methodToLog
方法,表示这个方法的执行时间会被记录到日志中。LogExecutionTimeAspect
切面会拦截使用了@LogExecutionTime
注解的方法,并在方法执行结束后记录执行时间。如果enabled
属性为true
,则会输出日志。