Spring Boot中的Aspect是用于实现面向切面编程(Aspect-Oriented Programming,AOP)的一种机制。AOP是一种编程范式,通过将横切关注点(如日志记录、性能统计、事务管理等)从业务逻辑中分离出来,以模块化的方式进行处理。
在Spring Boot中,Aspect使用注解方式实现。它通过定义切点(Pointcut)来选择横切关注点所在的连接点(Join Point),并在特定的连接点上织入(Weave)切面逻辑。切面逻辑可以在连接点之前(Before)、之后(After)、异常抛出时(AfterThrowing)或返回结果后(AfterReturning)执行。
使用Spring Boot的Aspect可以在不修改原始代码的情况下,对系统进行功能增强,例如添加日志、进行性能监控、实现事务管理等。通过将这些横切关注点从各个业务模块中抽离出来,可以提高代码的可维护性和可重用性
我们使用切面编程实现无侵入记录接口日志信息。
首先定义一个切面类:
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地址、接口名称、接口类名和接口方法名等信息,并输出到日志里。然后,执行目标方法,并获取返回值。如果执行过程中发生了异常,则捕获异常并输出异常信息,最后假装记录了接口执行完成的数据。最后,返回执行目标方法的结果。
我把我定义的注解代码展示一下:
// 设置注解的使用范围(类和方法) @Target({ElementType.METHOD, ElementType.TYPE}) // 设置注解的生命周期(运行时) @Retention(RetentionPolicy.RUNTIME) public @interface SaveLog { /** * 接口名称(必填) */ String name(); /** * 是否打印日志 */ boolean isPrintLog() default true; /** * 是否保存传入参数(默认true) */ boolean isSaveParam() default true; }
然后我们定义下控制器的接口信息:
@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 输出信息如下:
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 : 接口执行完成,假装我自己记录完成了
切面编程的魅力到此体验结束,完全无侵入,太棒了。
public class test { public static void main(String[] args) { &...
public class test { public static void main(String[] args) { &...
java限制1个方法同一时间只能被一个线程访问public synchronized void setOrderPay(){ }加上synchronized 修饰符即可...
Java5 引入了一种主要用于数组的增强型 for 循环,类似js中的for inpublic class Member { public static void main(String[]&...
(1).java睡眠函数Thread.sleep(时间); //单位为毫秒(2).java睡眠函数例子Date dNow = new Date(); SimpleDateFormat ft = new&nbs...
System.out.println("当前时间戳(秒): " + System.currentTimeMillis()/1000); System.out.println("当前时间戳(毫秒): " +&nb...