Spring Boot中的Aspect是用于实现面向切面编程(Aspect-Oriented Programming,AOP)的一种机制。AOP是一种编程范式,通过将横切关注点(如日志记录、性能统计、事务管理等)从业务逻辑中分离出来,以模块化的方式进行处理。
在Spring Boot中,Aspect使用注解方式实现。它通过定义切点(Pointcut)来选择横切关注点所在的连接点(Join Point),并在特定的连接点上织入(Weave)切面逻辑。切面逻辑可以在连接点之前(Before)、之后(After)、异常抛出时(AfterThrowing)或返回结果后(AfterReturning)执行。 使用Spring Boot的Aspect可以在不修改原始代码的情况下,对系统进行功能增强,例如添加日志、进行性能监控、实现事务管理等。通过将这些横切关注点从各个业务模块中抽离出来,可以提高代码的可维护性和可重用性
我们使用切面编程实现无侵入记录接口日志信息。
首先定义一个切面类:
Java
代码语言:javascript复制package com.learn.aspect;
@Aspect
@Component()
public class LogAnnotationAspect {
/**
* 日志类
*/
private static final Logger logger = LoggerFactory.getLogger(LogAnnotationAspect.class);
/**
* 定义切点:(只要带有@SaveLog注解的方法都需要记录日志)
*/
@Pointcut("@annotation(com.learn.annotation.SaveLog)")
public void pointCut() {
}
/**
* 定义环绕通知:(在目标方法的前后都植入额外的逻辑)
*
* @param joinPoint
* @return Object
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 返回信息
Object response = null;
// 获取当前连接点处的方法签名。方法签名包括方法的访问修饰符、返回类型、方法名称以及方法参数类型等信息。
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 获取注解配置信息
SaveLog saveLog = methodSignature.getMethod().getDeclaredAnnotation(SaveLog.class);
// 情况一:未设置日志注解,直接调用目标方法并返回
if (saveLog == null) {
response = joinPoint.proceed();
return response;
}
// 获取接口名称
String apiName = saveLog.name();
// 是否打印日志
boolean isPrintLog = saveLog.isPrintLog();
// 获取参数值
Object[] argValues = joinPoint.getArgs();
// 获取参数名
String[] argNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
Map<Object, Object> httpReqArgs = new HashMap<>();
if (argValues != null) {
for (int i = 0; i < argValues.length; i ) {
httpReqArgs.put(argNames[i], argValues[i]);
}
}
// 执行方法 输出日志
try {
// 开启打印日志
if (isPrintLog) {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest httpServletRequest = null;
if (sra != null) {
httpServletRequest = sra.getRequest();
}
if (httpServletRequest != null) {
logger.info("IP地址: {}", httpServletRequest.getRemoteAddr());
logger.info("请求地址: {}", httpServletRequest.getRequestURL().toString());
//logger.info("请求参数:{}", JSON.toJSONString(httpReqArgs, true));
logger.info("接口名称: {}", apiName);
String className = joinPoint.getTarget().getClass().getSimpleName();
logger.info("接口类名:{}", className);
String methodName = joinPoint.getSignature().getName();
logger.info("接口方法:{}", methodName);
}
}
// 执行目标方法
response = joinPoint.proceed();
} catch (Exception e) {
// 输出异常
logger.info("接口异常:{}", e.getMessage());
// 异常继续抛出
throw e;
} finally {
// 执行完成记录数据 todo()
logger.info("接口执行完成,假装我自己记录完成了");
}
// 返回执行目标方法的结果
return response;
}
}
首先定义了一个切点pointCut,通过注解@Pointcut标记该方法作为切点,其所匹配的连接点是所有带有@SaveLog注解的方法。 接下来定义了一个环绕通知around,用于在目标方法的前后都插入额外的逻辑。在around方法中,首先获取了当前连接点处的方法签名(Method Signature),并通过访问该方法的注解信息SaveLog获取了接口名称、是否打印日志等配置参数。然后,获取请求参数的值和参数名,并将其封装成一个Map对象httpReqArgs。接着,在执行目标方法前,如果需要打印日志,会获取请求的URL、IP地址、接口名称、接口类名和接口方法名等信息,并输出到日志里。然后,执行目标方法,并获取返回值。如果执行过程中发生了异常,则捕获异常并输出异常信息,最后假装记录了接口执行完成的数据。最后,返回执行目标方法的结果。
我把我定义的注解代码展示一下:
Java
代码语言:javascript复制// 设置注解的使用范围(类和方法)
@Target({ElementType.METHOD, ElementType.TYPE})
// 设置注解的生命周期(运行时)
@Retention(RetentionPolicy.RUNTIME)
public @interface SaveLog {
/**
* 接口名称(必填)
*/
String name();
/**
* 是否打印日志
*/
boolean isPrintLog() default true;
/**
* 是否保存传入参数(默认true)
*/
boolean isSaveParam() default true;
}
然后我们定义下控制器的接口信息:
Java
代码语言:javascript复制@RestController
public class ApiController {
/**
* 首页
*/
@SaveLog(name = "首页", isPrintLog = true)
@GetMapping("/")
public String home() {
return "Welcome to our home, sit down wherever you want";
}
/**
* 关于
*/
@SaveLog(name = "关于", isPrintLog = true)
@GetMapping("/about")
public String about() {
return "Can you come and hear our story?";
}
}
访问 http://127.0.0.1:8080/ 和 http://127.0.0.1:8080/about 输出信息如下:
Java
代码语言:javascript复制2023-09-27 17:38:07.626 INFO 23864 --- [0.1-8080-exec-4] com.learn.aspect.LogAnnotationAspect : IP地址: 127.0.0.1
2023-09-27 17:38:07.626 INFO 23864 --- [0.1-8080-exec-4] com.learn.aspect.LogAnnotationAspect : 请求地址: http://127.0.0.1:8080/
2023-09-27 17:38:07.626 INFO 23864 --- [0.1-8080-exec-4] com.learn.aspect.LogAnnotationAspect : 接口名称: 首页
2023-09-27 17:38:07.626 INFO 23864 --- [0.1-8080-exec-4] com.learn.aspect.LogAnnotationAspect : 接口类名:ApiController
2023-09-27 17:38:07.627 INFO 23864 --- [0.1-8080-exec-4] com.learn.aspect.LogAnnotationAspect : 接口方法:home
2023-09-27 17:38:07.627 INFO 23864 --- [0.1-8080-exec-4] com.learn.aspect.LogAnnotationAspect : 接口执行完成,假装我自己记录完成了
2023-09-27 17:39:34.926 INFO 23864 --- [0.1-8080-exec-7] com.learn.aspect.LogAnnotationAspect : IP地址: 127.0.0.1
2023-09-27 17:39:34.927 INFO 23864 --- [0.1-8080-exec-7] com.learn.aspect.LogAnnotationAspect : 请求地址: http://127.0.0.1:8080/about
2023-09-27 17:39:34.927 INFO 23864 --- [0.1-8080-exec-7] com.learn.aspect.LogAnnotationAspect : 接口名称: 关于
2023-09-27 17:39:34.927 INFO 23864 --- [0.1-8080-exec-7] com.learn.aspect.LogAnnotationAspect : 接口类名:ApiController
2023-09-27 17:39:34.927 INFO 23864 --- [0.1-8080-exec-7] com.learn.aspect.LogAnnotationAspect : 接口方法:about
2023-09-27 17:39:34.927 INFO 23864 --- [0.1-8080-exec-7] com.learn.aspect.LogAnnotationAspect : 接口执行完成,假装我自己记录完成了
切面编程的魅力到此体验结束,完全无侵入,太棒了。