# 前言 {#前言}
上一篇文章介绍了如何使用MyBatis的Plugin,来实现SQL的日志打印,这篇文章介绍一下如何将SpringBoot的请求日志,优雅地打印到日志中。 实现效果是这样的,只需要在需要打印的接口上加上一个注解,或者增加一项配置项,一个很详细的请求出入参等信息就被打印出来了。
# 准备工作 {#准备工作}
因为这个功能采用了AOP切面的功能,因此需要先引入AOP的依赖,版本按实际填写即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
# 基于注解的实现 {#基于注解的实现}
这个功能的原理很简单,就是一个基于AOP实现的小功能,但是在查问题时还挺有用的,本次实现两个注解,只打印请求入参的注解和入参出参都打印的注解。 第一个是只打印请求的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 记录请求日志
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface BeforeLog {
}
第二个是同时打印出入参的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 记录请求和响应日志
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AroundLog {
}
接着定义一个类叫做LogAspect,这是打日志功能的切面类,在实现上只需要先定义切入点,然后通过@Before和@Around来实现日志的打印,注释全都写在代码上了:
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.support.MultipartFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author by: 神秘的鱼仔
* @ClassName: LogAspect
* @Description: 日志切面类
* @Date: 2024/6/18 22:41
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* Before切入点
*/
@Pointcut("@annotation(com.mybatisflex.test.annotation.BeforeLog)")
public void beforePointcut() {
}
/**
* Around切入点
*/
@Pointcut("@annotation(com.mybatisflex.test.annotation.AroundLog)")
public void aroundPointcut() {
}
/**
* 记录请求日志的切面
* @param joinPoint
*/
@Before("beforePointcut()")
public void doBefore(JoinPoint joinPoint) {
try {
addLog(joinPoint,"",0);
}catch (Exception e){
log.error("doBefore日志记录异常,异常信息为:",e);
}
}
/**
* 记录请求和响应日志的切面
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("aroundPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
Object result = null;
try {
long startTime = System.currentTimeMillis();
result = joinPoint.proceed(args);
long endTime = System.currentTimeMillis();
long time = endTime - startTime;
addLog(joinPoint,JSONUtil.toJsonStr(result),time);
}catch (Exception e){
log.error("doAround日志记录异常,异常信息为:",e);
throw e;
}
return result;
}
/**
* 日志记录入库操作
*/
public void addLog(JoinPoint joinPoint, String outParams, long time) {
HttpServletRequest request = ((ServletRequestAttributes)
Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
log.info("\n\r=======================================\n\r" +
"请求地址:{} \n\r" +
"请求方式:{} \n\r" +
"请求类方法:{} \n\r" +
"请求方法参数:{} \n\r" +
"返回报文:{} \n\r" +
"处理耗时:{} ms \n\r" +
"=======================================\n\r",
request.getRequestURI(),
request.getMethod(),
joinPoint.getSignature(),
JSONUtil.toJsonStr(filterArgs(joinPoint.getArgs())),
outParams,
String.valueOf(time)
);
}
/**
* 过滤参数
* @param args
* @return
*/
private List<Object> filterArgs(Object[] args) {
return Arrays.stream(args).filter(object -> !(object instanceof MultipartFilter)
&& !(object instanceof HttpServletRequest)
&& !(object instanceof HttpServletResponse)
).collect(Collectors.toList());
}
}
接下来只需要在想要打印日志的接口上增加对应的注解,这个功能就实现了
@PostMapping("/testLog")
@AroundLog
public Result<Person> testLog(@RequestBody TestRequest request){
Person person = new Person(1,"鱼仔",27,"浙江","测试");
return Result.success(person);
}
效果可以见文章开头。
# 基于配置文件的实现 {#基于配置文件的实现}
如果接口特别多,一个个写注解的方式总是觉得太麻烦,这个时候就可以换个思路,采用配置文件的方式来实现。 先定义用来读取配置文件的类:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author by: 神秘的鱼仔
* @ClassName: LoggingProperties
* @Description:
* @Date: 2024/6/19 20:09
*/
@Component
@ConfigurationProperties(prefix = "logging")
public class LoggingProperties {
private List<String> includePaths;
public List<String> getIncludePaths() {
return includePaths;
}
public void setIncludePaths(List<String> includePaths) {
this.includePaths = includePaths;
}
}
yml文件是这样写的
logging:
includePaths:
- /api/**
- /api/v1/**
- /test/**
配置打日志要实现的效果是,只有白名单配置包含的路径才需要打印日志。 接着编写一个叫做WhiteListLogAspect的类,原理和上面的类似
import cn.hutool.json.JSONUtil;
import com.mybatisflex.test.properties.LoggingProperties;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.support.MultipartFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author by: 神秘的鱼仔
* @ClassName: WhiteListLogAspect
* @Description:
* @Date: 2024/6/19 19:50
*/
@Slf4j
@Aspect
@Component
public class WhiteListLogAspect {
@Autowired
private LoggingProperties loggingProperties;
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Before("execution(* com.mybatisflex.test.controller..*(..))")
public void doBefore(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String requestURI = request.getRequestURI();
if (shouldLog(requestURI)) {
addLog(joinPoint,"",0);
}
}
/**
* 日志记录入库操作
*/
public void addLog(JoinPoint joinPoint, String outParams, long time) {
HttpServletRequest request = ((ServletRequestAttributes)
Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
log.info("\n\r=======================================\n\r" +
"请求地址:{} \n\r" +
"请求方式:{} \n\r" +
"请求类方法:{} \n\r" +
"请求方法参数:{} \n\r" +
"返回报文:{} \n\r" +
"处理耗时:{} ms \n\r" +
"=======================================\n\r",
request.getRequestURI(),
request.getMethod(),
joinPoint.getSignature(),
JSONUtil.toJsonStr(filterArgs(joinPoint.getArgs())),
outParams,
String.valueOf(time)
);
}
/**
* 过滤参数
* @param args
* @return
*/
private List<Object> filterArgs(Object[] args) {
return Arrays.stream(args).filter(object -> !(object instanceof MultipartFilter)
&& !(object instanceof HttpServletRequest)
&& !(object instanceof HttpServletResponse)
).collect(Collectors.toList());
}
private boolean shouldLog(String requestURI) {
return loggingProperties.getIncludePaths().stream().anyMatch(pattern -> pathMatcher.match(pattern, requestURI));
}
}
# 总结 {#总结}
打印请求日志的最大作用就是当出现问题时,基于出入参能比较容易地排查问题,不过响应日志一般不会打在生产日志上,因为返回的数据会过于庞大。具体还是看项目的实际情况了。