当前位置:首页 > JAVA > 正文内容

SpringBoot Aspect 切面编程

高老师12个月前 (09-27)JAVA273

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     : 接口执行完成,假装我自己记录完成了

切面编程的魅力到此体验结束,完全无侵入,太棒了。

扫描二维码推送至手机访问。

版权声明:本文由高久峰个人博客发布,如需转载请注明出处。

本文链接:https://blog.5b1.cn/post/628.html

分享给朋友:

“SpringBoot Aspect 切面编程 ” 的相关文章

 java字节与字符的区别,字节与字的区别与联系

java字节与字符的区别,字节与字的区别与联系

字节(Byte)是计量单位,表示数据量多少,是计算机信息技术用于计量存储容量的一种计量单位,通常情况下一字节等于八位。字符(Character)计算机中使用的字母、数字、字和符号,比如'A'、'B'、'$'、'&'等。一般在英文...

Java不用编译直接执行

Java不用编译直接执行

public class test {     public static void main(String[] args) {     &...

java final 修饰符,java通过final修饰符创建常量,,java通过final修饰符声明方法不可被修改

java final 修饰符,java通过final修饰符创建常量,,java通过final修饰符声明方法不可被修改

(1).final 修饰符通常和 static 修饰符一起使用来创建类常量。(2).父类中的 final 方法可以被子类继承,但是不能被子类重写,声明 final 方法的主要目的是防止该方法的内容被修改。public class Member {   ...

java增强型for循环

java增强型for循环

Java5 引入了一种主要用于数组的增强型 for 循环,类似js中的for inpublic class Member {     public static void main(String[]&...

java Character 类,java判断字符是否是一个字母,java判断字符是否是一个数字,java判断字符是否是一个空白,java判断字符是否是小写字母,java判断字符是否是大写字母,java转换字符为大写,java转换字符为小写

java Character 类,java判断字符是否是一个字母,java判断字符是否是一个数字,java判断字符是否是一个空白,java判断字符是否是小写字母,java判断字符是否是大写字母,java转换字符为大写,java转换字符为小写

java判断字符是否是一个字母System.out.println(Character.isLetter('a'));java判断字符是否是一个数字System.out.println(Character.isDigit('0'));java判断字符是否是一个空白Sy...

java日期和时间

java日期和时间

(1).java获取当前日期时间Date date = new Date();  System.out.println(date.toString());输出Fri Jul 02 10:29:55 CST 2021(2).java获取时间戳秒/毫秒D...