Spring Boot 自定义注解,AOP 切面统一打印出入参请求日志

1. 问题提出

  • 在访问接口时,需要查看入参和出参信息,并且将这些访问信息存储到数据库中,实现接口问题排查

文章目录

  • 1. 问题提出
  • 2. 整体方案
    • 2.1 添加AOP Maven依赖
    • 2.2 自定义日志注解
    • 2.3 配置AOP切面
    • 2.4 添加切点的开关注解
  • 完整代码:
    • com.example.aspect.DemoApplication.java
    • com.example.aspect.controller.demoController.java
    • com.example.aspect.log.annotation.EnableAOPLog.java
    • com.example.aspect.log.annotation.LogAnnotation.java
    • com.example.aspect.log.aop.LogImportSelector.java
    • com.example.aspect.log.aop.WebLogAspect.java
    • com.example.aspect.log.params.SysLogParam.java
  • 相关问题
    • 问题1: com.example.aspect.log.aop.WebLogAspect.java中将logAnnotation放在输入参数中
    • 问题2:输入输出参数信息需要脱敏
    • 问题3: 输入参数需要存放在mongodb中

2. 整体方案

  • 自定义日志注解:在注解处添加切面,每次调用方法时,就会触发AOP
  • 配置AOP切面
  • 配置AOP开关
    Spring Boot 自定义注解,AOP 切面统一打印出入参请求日志_第1张图片
    这里采用自定义注解和AOP切面方式,实现入参和出参的日志信息打印与存储

2.1 添加AOP Maven依赖

pom.xml文件中添加依赖,这里不用依赖也行,需要的时候加上:


2.2 自定义日志注解

package com.example.aspect.log.annotation;
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)   // 在运行时使用该注解
@Target({ElementType.METHOD})        // 注解作用于方法上
@Documented							// 注解将包含在JavaDoc上
public @interface LogAnnotation {   // 注解名为LogAnnotation

    // 模块名称
    String module();

    // 是否从当前的上下文中获取获取 url、ip
    boolean request() default true;

    // 记录和打印请求参数
    boolean recordParam() default true;

    // 记录和打印响应参数
    boolean recordResponse() default true;

    // 记录到数据库
    boolean recordToDB() default false;
}

2.3 配置AOP切面

在配置AOP切面之前,我们需要了解下aspectj相关注解的作用

  • @Aspect: 声名该类为一个注解类
  • @Pointcut: 定义一个切点,后面跟随一个表达式,表达式可以定义为切某个注解,也可以切某个package下的方法

切点定义好后,就是围绕这个切点做文章:

  • @Before: 在切点之前,织入相关代码
  • @After: 在切点之前后,织入相关代码
  • @AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景;
  • @AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;
  • @Around: 环绕,可以在切入点前后织入代码,并且可以自由的控制何时执行切点;
    Spring Boot 自定义注解,AOP 切面统一打印出入参请求日志_第2张图片
    正文:
    定义一个LogAnnotaion.java的切面类,声明一个切点:
// 以自定义@LogAnnotaion注解为切点,指向刚才注解的位置
@Pointcut"@annotation(com.example.aspect.log.annotation)"public void webLog(){}

定义@Around环绕,用户何时执行切点:

@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint, LogAnnotation logAnnotation) throws Throwable {
	long startTime = System.currentTimeMillis();  
	Object result = joinPoint.proceed();  // 执行切点
	// 打印出参
	logger.info("Response Args  : {}", new Gson().toJson(result));
	// 执行耗时
	logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
	// 返回接口返参结果,这里必须要有返回
	return result;
}

2.4 添加切点的开关注解

log.aop.LogImportSelector.java

package com.example.aspect.log.aop;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class LogImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        return new String[]{WebLogAspect.class.getName()};
    }
}

ImportSelector接口是至spring中导入外部配置的核心接口,在SpringBoot的自动化配置和@EnableXXX(功能性注解)都有它的存在,关于SpringBoot的分析可以参考:深入理解SpringBoot的自动装配。

log.annotation.EnableAOPLog.java

package com.example.aspect.log.annotation;

import com.example.aspect.log.aop.LogImportSelector;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(LogImportSelector.class)
public @interface EnableAOPLog {
}

完整代码:

Spring Boot 自定义注解,AOP 切面统一打印出入参请求日志_第3张图片

com.example.aspect.DemoApplication.java

package com.example.aspect;

import com.example.aspect.log.annotation.EnableAOPLog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@EnableAOPLog   // 打开日志切面开关
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

com.example.aspect.controller.demoController.java

package com.example.aspect.controller;

import com.example.aspect.log.annotation.LogAnnotation;
import com.example.aspect.log.params.LoginParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping(value = "/v1")
public class demoController {

    @PostMapping(value = "/login")
    @LogAnnotation(module = "serviceA")  // 添加日志注解
    public String login(@RequestBody LoginParam loginParam){
        log.info("login方法开始执行");
        return loginParam.getUsername();
    }
}

com.example.aspect.log.annotation.EnableAOPLog.java

package com.example.aspect.log.annotation;

import com.example.aspect.log.aop.LogImportSelector;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(LogImportSelector.class)
public @interface EnableAOPLog {
}

com.example.aspect.log.annotation.LogAnnotation.java

package com.example.aspect.log.annotation;
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface LogAnnotation {

    // 模块
    String module();

    // 是否从当前的上下文中获取获取 url、ip
    boolean request() default true;

    // 记录和打印请求参数
    boolean recordParam() default true;

    // 记录和打印响应参数
    boolean recordResponse() default true;

    // 记录到数据库
    boolean recordToDB() default false;
}

com.example.aspect.log.aop.LogImportSelector.java

package com.example.aspect.log.aop;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class LogImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        return new String[]{WebLogAspect.class.getName()};
    }
}

com.example.aspect.log.aop.WebLogAspect.java

package com.example.aspect.log.aop;

import com.example.aspect.log.annotation.LogAnnotation;
import com.example.aspect.log.params.SysLogParam;
import com.example.aspect.utils.ParemetersDesensitive;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * https://www.exception.site/springboot/spring-boot-aop-web-request
 */

@Slf4j
@Aspect
public class WebLogAspect {

    @Around("@annotation(logAnnotation)")
    public Object doAround(ProceedingJoinPoint joinPoint, LogAnnotation logAnnotation) throws Throwable {

        SysLogParam sysLogParam = new SysLogParam();
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder
                .currentRequestAttributes()).getRequest();

        // 记录进入时间
        long startTime = System.currentTimeMillis();
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sysLogParam.setLogTime(dateFormat.format(new Date(startTime)));

        // 记录归属模块/服务
        sysLogParam.setModule(logAnnotation.module());

        // todo:记录登录用户

        // 记录客户端相关信息
        if (logAnnotation.request() && httpServletRequest != null) {
            sysLogParam.setMethod(httpServletRequest.getMethod());
            sysLogParam.setIp(null);
            sysLogParam.setUrl(httpServletRequest.getRequestURI());
        }

        // 记录请求参数
        if (logAnnotation.recordParam()) {
            ParemetersDesensitive.recordRequestParam(joinPoint, sysLogParam);
        } else {
            sysLogParam.setParams("[-]");
        }
        log.info("开始请求:{} {}, req:{} ", sysLogParam.getMethod(), sysLogParam.getUrl(), sysLogParam.getParams());

        Object result = null;
        try {
            // 执行方法
            result = joinPoint.proceed();
            sysLogParam.setFlag(Boolean.TRUE);
        } catch (Exception e) {
            sysLogParam.setFlag(Boolean.FALSE);
            log.error("请求报错{}", e.getMessage());
            throw e;
        } finally {
            try {
                if (logAnnotation.recordResponse()) {
                    log.info("结束请求:{} {}, time:{}, req:{}, resp:{}", sysLogParam.getMethod(), sysLogParam.getUrl(), (System.currentTimeMillis() - startTime), sysLogParam.getParams(), ParemetersDesensitive.resultStr(result));
                } else {
                    log.info("结束请求:{} {}, time:{}, req:{}", sysLogParam.getMethod(), sysLogParam.getUrl(), (System.currentTimeMillis() - startTime), sysLogParam.getParams());
                }
                if (logAnnotation.recordToDB()) {
                    recordToDb(sysLogParam);
                }
                log.trace("退出日志切面");
            } catch (Exception e) {
                log.warn("记录日志时,出现异常");
            }
        }
        return result;
    }

    // 记录执行参数到数据库
    private void recordToDb(SysLogParam sysLog) {
//        if (logService == null) {
//            return;
//        }
//        CompletableFuture.runAsync(() -> {
//            try {
//                log.trace("日志落库开始:{}", sysLog);
//                logService.save(sysLog);
//                log.trace("开始落库结束:{}", sysLog);
//            } catch (Exception e) {
//                log.error("落库失败:{}", e.getMessage());
//            }
//        }, taskExecutor);
    }

}

com.example.aspect.log.params.SysLogParam.java

package com.example.aspect.log.params;

import lombok.Data;

import java.io.Serializable;

@Data
public class SysLogParam implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 对应mongodb的id
     */
    private String id;
    /**
     * 客户端IP地址
     */
    private String ip;
    /**
     * 登录用户信息:账号
     */
    private String loginName;
    /**
     * 登录用户信息:用户Id
     */
    private String userId;
    /**
     * 登录用户信息:用户名
     */
    private String userName;
    /**
     * 归属服务ID
     */
    private String module;
    /**
     * 访问的方法:GET/PUT/POST/DELETE
     */
    private String method;
    /**
     * 访问的url地址
     */
    private String url;
    /**
     * 传递的参数信息
     */
    private String params;
    /**
     * 是否执行成功
     */
    private Boolean flag;
    /**
     * 访问时间
     */
    private String logTime;
    /**
     * 接口耗时
     */
    private String durationTime;
}

相关问题

问题1: com.example.aspect.log.aop.WebLogAspect.java中将logAnnotation放在输入参数中

问题2:输入输出参数信息需要脱敏

问题3: 输入参数需要存放在mongodb中

你可能感兴趣的:(Java开发,spring,boot,java,后端)