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 : 接口执行完成,假装我自己记录完成了
切面编程的魅力到此体验结束,完全无侵入,太棒了。
字节(Byte)是计量单位,表示数据量多少,是计算机信息技术用于计量存储容量的一种计量单位,通常情况下一字节等于八位。字符(Character)计算机中使用的字母、数字、字和符号,比如'A'、'B'、'$'、'&'等。一般在英文...
java限制1个方法同一时间只能被一个线程访问public synchronized void setOrderPay(){ }加上synchronized 修饰符即可...
java判断字符是否是一个字母System.out.println(Character.isLetter('a'));java判断字符是否是一个数字System.out.println(Character.isDigit('0'));java判断字符是否是一个空白Sy...
java stringBuffer(1).stringBuffer和stringBuilder区别stringBuffer是线程安全的,stringBuilder速度更快(2).简单的stringBuffer例子StringBuffer sBuffer = new&nb...
(1).创建数组double[] myList = new double[size]; //推荐创建方式 double myList[] = new double[size];  ...
System.out.println("当前时间戳(秒): " + System.currentTimeMillis()/1000); System.out.println("当前时间戳(毫秒): " +&nb...