我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!
1 背景
平时工作中,我们后端程序员干得最多的就是写接口,然后实现接口。当前后端联调的时候,API的输入输出参数是判断问题的关键。然而每次手动输出日志的话又很麻烦,所以我们在系统中增加了一个注解用来输出API接口的各种参数。给每个接口添加这个注解,则可以输出请求相关的log。
输出如图所示,是不是很清晰明了,这不是棒棒的。下边我来解释下是如何做到的。
2 代码
2.1 导入相关的依赖
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.43</version>
</dependency>
2.2 首先是一个注解类
将来这个注解就可以加在API的方法上,用来输出log。它有一个属性printResult,用来定义是否开启输出。定义这个参数的原因是我们本来还有两个参数用来定义日志是否存库,我写文章的时候给砍掉了,存库的也比较简单,加一个参数用来开启关闭,然后将日志拼接存库就行。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebLog {
/**
* 是否开启打印响应结果
*
*/
boolean printResult() default false;
}
2.3 切面类
@Component
@Slf4j
@Aspect
@Order(1)
public class WebLogAspect {
private final ThreadLocal<Stopwatch> stopWatchThreadLocal = new ThreadLocal<>();
private final ThreadLocal<Boolean> printResultThreadLocal = new ThreadLocal<>();
@Pointcut("@annotation(com.luke.demo.webLog.annotation.WebLog)")
public void webLog() {
}
@Before("webLog()")
public void before(JoinPoint joinPoint) {
stopWatchThreadLocal.set(Stopwatch.createStarted());
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String requestURI = request.getRequestURI();
String requestMethod = request.getMethod();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String declaringTypeName = signature.getDeclaringTypeName();
String name = signature.getName();
Object[] args = joinPoint.getArgs();
Method method = signature.getMethod();
log.info("-------------------------------请求开始-------------------------------");
log.info("请求IP: {}", IpUtils.getIpAddr(request));
log.info("User-Agent: {}", request.getHeader("User-Agent"));
log.info("请求URI: {}", requestURI);
log.info("请求方式: {}", requestMethod);
log.info("请求方法: {}", declaringTypeName.concat(".").concat(name));
args = Stream.of(args).filter(parm -> !(parm instanceof ServletRequest) && !(parm instanceof MultipartFile) && !(parm instanceof ServletResponse)).toArray();
String params = Arrays.toString(args);
log.info("方法入参: {}", params);
WebLog webLog = method.getAnnotation(WebLog.class);
printResultThreadLocal.set(webLog.printResult());
}
@AfterReturning(returning = "object", pointcut = "webLog()")
public void afterReturning(Object object) {
Boolean print = printResultThreadLocal.get();
if (print) {
log.info("请求响应:{}", JSON.toJSONString(object));
}
printAfterReturning();
}
@AfterThrowing(pointcut = "webLog()")
public void afterThrowing() {
log.info("接口执行异常");
printAfterReturning();
}
private void printAfterReturning() {
Stopwatch stopwatch = stopWatchThreadLocal.get();
stopwatch.stop();
log.info("请求耗时: {} ms", stopwatch.elapsed(TimeUnit.MILLISECONDS));
log.info("-------------------------------请求结束-------------------------------");
stopWatchThreadLocal.remove();
printResultThreadLocal.remove();
}
}
这里的Stopwatch是用来记录请求消耗的时间。ThreadLocal则将这些参数规定在自己线程内,不与其他线程共享。
2.4 API
我们用这个接口模拟一个API请求,请求后边缀了一个参数,模拟入参效果。请求的地址为http://localhost:8080/test/test/123456会返回一个字符串test123456;
@Slf4j
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping(value = "test/{testValue}")
@WebLog(printResult = true)
public String logTest(@PathVariable String testValue) {
return "test"+testValue;
}
}
2.4 输出
调用之后,输出就如下所示。记录了请求的IP,请求的URL,请求的方式,请求的方法,入参和回参,以及请求耗时。可以满足我们日常开发以及前后端联调所需。
-------------------------------请求开始-------------------------------
请求IP: 0:0:0:0:0:0:0:1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
请求URI: /test/test/123456
请求方式: GET
请求方法: com.luke.demo.webLog.controller.TestController.logTest
方法入参: [123456]
请求响应:"test123456"
请求耗时: 4 ms
-------------------------------请求结束-------------------------------