请求日志组件,没有AOP,所以更优雅

如果没记错的话,距离第一次说的请求日志相关的内容应该也过了有两篇了,最近工作上也开始忙起来了,因此隔了也有好长一段时间了,久久没有到来,让各位大佬久等了。

耽误了各位大佬装X,我承认是我的问题。好了,先把刀放下,我马上开始。

请求日志组件,没有AOP,所以更优雅

开始之前,需要先给各位大佬普及一下即将用到的新的知识点。

新朋友ControllerAdvice

类终成一bean,而有些类则需要一点小小的帮助.

对于@ControllerAdvice这个注解,用过全局异常的朋友可能都不陌生了,但是它不仅仅能做全局异常,我们今天的新朋友也需要它的帮助。

上一篇自定义的返回参数处理器,还有很多限制,今天给大家介绍的新朋友,就更强大了!你可以用它来做很多事情!

新朋友RequestBodyAdvice

拿过请求body数据,或者说在某种情况下需要提前获取body内容的朋友,有没有想起一个印象很深刻的事情,就是request中的inputstream只能读取一次,所以每当我们想先读取流的时候,都会重新去包装一个新的request。

spring考虑到了这点,因此spring为了不让我们做繁琐的操作,spring给我们提供了一个扩展点,就是我们可以在流的读取前后做一些操作。

我们请求日志的实现,必然涉及到我们提前对body的读取,因此我们刚好可以利用这个扩展点,但是我们不会再去读取流,也不用去解析流了,我们可以直接拿到spring给我们封装好的数据

我们来看下接口。

public interface RequestBodyAdvice {

boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType)
;

HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;

Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}

类如其名,这个类的作用一看就知道是跟请求体(body)相关的。我们继续仔细看看接口方法,方法名也很明显,看到supports方法是不是又想到了万能的策略模式。哈哈哈!根据其他三个方法的名称也很容易知道方法的作用,以及在哪个环节执行。

supports: 是否支持处理当前请求
beforeBodyRead: 请求体在被read前执行
afterBodyRead: 请求体在被read后执行(这个时候body已经绑定到了实体上了)
handleEmptyBody: 当body为空时执行

看了《Spring源码解读(第九弹)-请求参数封装,返回值处理》的朋友,很容易就可以想到我们目前讨论的这个接口跟请求参数封装的接口有关系,因为body也是需要封装成实体对象的,只不过我们上次聊的是@RequestParam的内容罢了,这个扩展点的源代码是处理body参数的部分,还记得RequestResponseBodyMethodProcessor吧!调用是在超类AbstractMessageConverterMethodProcessor。喜欢的朋友可以自己去debug哦!

新朋友ResponseBodyAdvice

public interface ResponseBodyAdvice<T> {

boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType, selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response)
;
}

介绍完了请求body的内容了,返回body的想必就更简单了。

supports: 能否处理当前返回值
beforeBodyWrite: 在数据写入到response流之前执行

这个扩展点的源代码是处理body返回值的部分,依然是RequestResponseBodyMethodProcessor!调用也是在超类AbstractMessageConverterMethodProcessor,喜欢的朋友也可以去debug哦!

beforeBodyWrite()方法中你一样可以像返回值处理器一样对返回body进行处理

请求日志

代码清单

请求日志组件,没有AOP,所以更优雅

app_log_code_list.png
AppLog: 用来记录当前请求的信息,包括请求路径,请求参数,返回体等
AppLogContextHolder: 线程上线文日志对象持有类,持有当前线程上下文log对象。
AppLogInterceptor: HandlerInterceptor拦截器,用来获取请求的param,path等信息,和打印请求信息
AppLogConfiguration: 配置类
AppLogRequestBodyAdvice: 用来获取请求body
AppLogResponseBodyAdvice: 用来获取返回body

代码明细

AppLog

public class AppLog {
/** 请求路径 */
private String path;
/** 请求param */
private Map<String, String[]> parameters;
/** 请求体 */
private Object reqBody;
/** 返回体 */
private Object respBody;
}

日志对象实体,用来保存当前请求相关的信息,如请求路径path,请求参数parameters,请求体,返回体等信息,按需求增减即可。

AppLogContextHolder

public class AppLogContextHolder {

private static final ThreadLocal<AppLog> TL = ThreadLocal.withInitial(AppLog::new);

public static AppLog get() {
// 获取线程上下文的日志对象
return TL.get();
}

public static void remove() {
// 移除线程上下文的日志对象
TL.remove();
}

}

线程上下文对象持有类,持有当前线程上下文日志对象。

AppLogInterceptor

public class AppLogInterceptor implements HandlerInterceptor {

private final Logger log = LoggerFactory.getLogger(getClass());

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获得一个线程上下文log对象
AppLog appLog = AppLogContextHolder.get();

// 把请求路径也放入到线程上下文中
appLog.setPath(request.getServletPath());

// 把请求参数放入到线程上下文对象中
appLog.setParameters(request.getParameterMap());

// 这里也可以把header信息也放进去,我这里就不放了哈
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
AppLog appLog = AppLogContextHolder.get();
// 打印本次请求的相关日志
log.info(JSON.toJSONString(appLog));
// 请求结束后清空掉当前线程上下文的内容,防止内存泄漏
AppLogContextHolder.remove();
}

}

拦截器,用来拦截本次请求,获取请求路径,请求参数等信息。

在请求结束之后,打印请求相关的日志,并移除掉当前线程上下文的内容,防止内存泄漏。

AppLogConfiguration

@Configuration
public class AppLogConfiguration {

@Bean
public WebMvcConfigurer appLogWebMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加拦截器
registry.addInterceptor(new AppLogInterceptor());
}
};
}

}

配置类,配置我们获取请求param,path等信息的拦截器。

AppLogRequestBodyAdvice

@ControllerAdvice
public class AppLogRequestBodyAdvice extends RequestBodyAdviceAdapter {

@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 返回true,所有请求都拦截
return true;
}

@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 把请求体数据放入线程上下文
AppLogContextHolder.get().setReqBody(body);
return body;
}
}

RequestBodyAdviceAdapter这个类是RequestBodyAdvice的空实现,我们继承这类,可以只重写我们需要的方法就好。

AppLogResponseBodyAdvice

@ControllerAdvice
public class AppLogResponseBodyAdvice implements ResponseBodyAdvice<Object> {

@Value("${app-log.response.body.print:false}")
private boolean printBody;

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 由于返回体一般情况下都比较大,因此通过配置来确定是否需要打印返回体
return printBody;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 将返回体设置到log对象中
AppLogContextHolder.get().setRespBody(body);
return body;
}

}

返回体通常情况下返回体都会比较大,都不需要打印,因此加了一个配置开关,用来在合适的时候可以开启。如果你的项目使用了配置中心,那么当线上出现bug需要排查的时候,你就可以开启这个开关,协助你排查问题,而不是一开始就打印返回体。

测试类

@Controller
@RequestMapping("/appLog")
public class AppLogTestController {

@PostMapping("/test")
@ResponseBody
public B test(@RequestParam("name") String name, @RequestBody A a) {
return new B(name, a.age);
}

public static class A {
private Integer age;
}

public static class B {
private String name;
private Integer age;
}
}

测试接口中,包含一个name的parameter,包含一个类型为A的请求体,然后将请求的namea.age封装到实体B上返回。

记得有个打印请求体的配置要打开哦,或者你直接给他搞成true也行(我测试的时候也这样搞,哈哈哈!),总之你开心就好!

app-log.response.body.print=true

见证奇迹的时候

POST http://localhost:8080/appLog/test?name=monkeySun
Content-Type: application/json
Accept: */*

{"age": 500}

###

发起请求,我们直来看结果,日志内容如下。

请求日志组件,没有AOP,所以更优雅

app_log_result.png

人工图片文本智能识别(笑哭)开始…1s,2s,3s…..

识别成功,上图关键信息识别结果如下:

2023-03-11 11:58:25.014  INFO 7744 --- [nio-8080-exec-1] c.w.s.d.c.c.applog.AppLogInterceptor     : {"parameters":{"name":["monkeySun"]},"path":"/appLog/test","reqBody":{"age":500},"respBody":{"age":500,"name":"monkeySun"}}

是不是感觉要比AOP要舒服多了!

好了,今天就到这儿吧,毕竟识别图片消耗太大了….

无名道友:说好的三合一呢?你演我是吧?

请求日志组件,没有AOP,所以更优雅

小编:额。。。强哥,你听我狡辩…解释..

请求日志组件,没有AOP,所以更优雅

小编:内容确实又超出预期了,各位强哥再给小编我缓两天,真的,就缓两天,这次真不演了,接下来的几篇,一定先把之前说好的那些自定义组件给各位大佬讲完,各位强哥稍安勿躁。

毕竟一口吃不成一个大胖子,咱慢慢来!


通过智能分析,得出各位可能喜欢测试代码,请在这里自提哦!https://gitee.com/wt123/learn/tree/master/springboot-learn


在下诚心恭祝各位强哥越来越牛逼!


Good Luck…


原文始发于微信公众号(心猿易码):请求日志组件,没有AOP,所以更优雅

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/133274.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!