在本篇文章中不会详细介绍日志如何配置、如果切换另外一种日志工具之类的内容,只用于记录作者本人在工作过程中对日志的几种处理方式。

1. Debug 日志管理

在开发的过程中,总会遇到各种莫名其妙的问题,而这些问题的定位一般会使用到两种方式,第一种是通过手工 Debug 代码,第二种则是直接查看日志输出。Debug 代码这种方式只能在 IDE 下使用,一旦程序移交部署,就只能通过日志来跟踪定位了。

在测试环境下,我们无法使用 Debug 代码来定位问题,所以这时候需要记录所有请求的参数及对应的响应报文。而在 数据交互篇 中,我们将请求及响应的格式都定义成了Json,而且传输的数据还是存放在请求体里面。而请求体对应在 HttpServletRequest 里面又只是一个输入流,这样的话,就无法在过滤器或者拦截器里面去做日志记录了,而必须要等待输入流转换成请求模型后(响应对象转换成输出流前)做数据日志输出。

有目标那就好办了,只需要找到转换发生的地方就可以植入我们的日志了。通过源码的阅读,终于在 AbstractMessageConverterMethodArgumentResolver 个类中发现了我们的期望的那个地方,对于请求模型的转换,实现代码如下:

@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { MediaType contentType;
boolean noContentType = false;
try {
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
} Class<?> contextClass = (parameter != null ? parameter.getContainingClass() : null);
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = (parameter != null ?
ResolvableType.forMethodParameter(parameter) : ResolvableType.forType(targetType));
targetClass = (Class<T>) resolvableType.resolve();
} HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod();
Object body = NO_VALUE; try {
inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage); for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = genericConverter.read(targetType, contextClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
else if (targetClass != null) {
if (converter.canRead(targetClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex);
} if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && inputMessage.getBody() == null)) {
return null;
}
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
} return body;
}

  

上面的代码中有一处非常重要的地方,那就在在数据转换前后都存在 Advice 相关的方法调用,显然,只需要在 Advice 里面完成日志记录就可以了,下面开始实现自定义 Advice。

首先,请求体日志切面 LogRequestBodyAdvice 实现如下:

@ControllerAdvice
public class LogRequestBodyAdvice implements RequestBodyAdvice { private Logger logger = LoggerFactory.getLogger(LogRequestBodyAdvice.class); @Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
} @Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return body;
} @Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
} @Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
Method method = parameter.getMethod();
String classMappingUri = getClassMappingUri(method.getDeclaringClass());
String methodMappingUri = getMethodMappingUri(method);
if (!methodMappingUri.startsWith("/")) {
methodMappingUri = "/" + methodMappingUri;
}
logger.debug("uri={} | requestBody={}", classMappingUri + methodMappingUri, JSON.toJSONString(body));
return body;
} private String getMethodMappingUri(Method method) {
RequestMapping methodDeclaredAnnotation = method.getDeclaredAnnotation(RequestMapping.class);
return methodDeclaredAnnotation == null ? "" : getMaxLength(methodDeclaredAnnotation.value());
} private String getClassMappingUri(Class<?> declaringClass) {
RequestMapping classDeclaredAnnotation = declaringClass.getDeclaredAnnotation(RequestMapping.class);
return classDeclaredAnnotation == null ? "" : getMaxLength(classDeclaredAnnotation.value());
} private String getMaxLength(String[] strings) {
String methodMappingUri = "";
for (String string : strings) {
if (string.length() > methodMappingUri.length()) {
methodMappingUri = string;
}
}
return methodMappingUri;
}
}

  

得到日志记录如下:

2017-05-02 22:48:15.435 DEBUG 888 --- [nio-8080-exec-1] c.q.funda.advice.LogRequestBodyAdvice    : uri=/sys/user/login | 
requestBody={"password":"123","username":"123"}

对应的,响应体日志切面 LogResponseBodyAdvice 实现如下:

@ControllerAdvice
public class LogResponseBodyAdvice implements ResponseBodyAdvice { private Logger logger = LoggerFactory.getLogger(LogResponseBodyAdvice.class); @Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
} @Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
logger.debug("uri={} | responseBody={}", request.getURI().getPath(), JSON.toJSONString(body));
return body;
}
}

  

得到日志记录如下:

2017-05-02 22:48:15.520 DEBUG 888 --- [nio-8080-exec-1] c.q.funda.advice.LogResponseBodyAdvice   : uri=/sys/user/login | 
responseBody={"code":10101,"msg":"手机号格式不合法"}

  

2. 异常日志管理

Debug 日志只适用于开发及测试阶段,一般应用部署生产,鉴于日志里面的敏感信息过多,往往只会在程序出现异常时输出明细的日志信息,在 ExceptionHandler 标注的方法里面输入异常日志无疑是最好的,但摆在面前的一个问题是,如何将 @RequestBody 绑定的 Model 传递给异常处理方法?我想到的是通过 ThreadLocal 这个线程本地变量来存储每一次请求的 Model,这样就可以贯穿整个请求处理流程,下面使用 ThreadLocal 来协助完成异常日志的记录。

在绑定时,将绑定 Model 有存放到 ThreadLocal:

@RestController
@RequestMapping("/sys/user")
public class UserController { public static final ThreadLocal<Object> MODEL_HOLDER = new ThreadLocal<>(); @InitBinder
public void initBinder(WebDataBinder webDataBinder) {
MODEL_HOLDER.set(webDataBinder.getTarget());
} }

  

异常处理时,从 ThreadLocal 中取出变量,并做相应的日志输出:

@ControllerAdvice
@ResponseBody
public class ExceptionHandlerAdvice { private Logger logger = LoggerFactory.getLogger(ExceptionHandlerAdvice.class); @ExceptionHandler(Exception.class)
public Result handleException(Exception e, HttpServletRequest request) {
logger.error("uri={} | requestBody={}", request.getRequestURI(),
JSON.toJSONString(UserController.MODEL_HOLDER.get()));
return new Result(ResultCode.WEAK_NET_WORK);
} }

  

当异常产生时,输出日志如下:

2017-05-03 21:46:07.177 ERROR 633 --- [nio-8080-exec-1] c.q.funda.advice.ExceptionHandlerAdvice  : uri=/sys/user/login | 
requestBody={"password":"123","username":"13632672222"}

  

注意:当 Mapping 方法中带有多个参数时,需要将 @RequestBody 绑定的变量当作方法的最后一个参数,否则 ThreadLocal 中的值将会被其它值所替换。如果需要输出 Mapping 方法中所有参数,可以在 ThreadLocal 里面存放一个 Map 集合。

项目的 github 地址:https://github.com/qchery/funda


原文地址:http://blog.csdn.net/chinrui/article/details/71056847

SpringBoot实战 之 接口日志篇的更多相关文章

  1. Spring Boot 2.x(十一):AOP实战--打印接口日志

    接口日志有啥用 在我们日常的开发过程中,我们可以通过接口日志去查看这个接口的一些详细信息.比如客户端的IP,客户端的类型,响应的时间,请求的类型,请求的接口方法等等,我们可以对这些数据进行统计分析,提 ...

  2. SpringBoot实战 之 异常处理篇

    在互联网时代,我们所开发的应用大多是直面用户的,程序中的任何一点小疏忽都可能导致用户的流失,而程序出现异常往往又是不可避免的,那该如何减少程序异常对用户体验的影响呢?其实方法很简单,对异常进行捕获,然 ...

  3. SpringBoot实战之异常处理篇

    在互联网时代,我们所开发的应用大多是直面用户的,程序中的任何一点小疏忽都可能导致用户的流失,而程序出现异常往往又是不可避免的,那该如何减少程序异常对用户体验的影响呢?其实方法很简单,对异常进行捕获,然 ...

  4. SpringSecurity权限管理系统实战—二、日志、接口文档等实现

    系列目录 SpringSecurity权限管理系统实战-一.项目简介和开发环境准备 SpringSecurity权限管理系统实战-二.日志.接口文档等实现 SpringSecurity权限管理系统实战 ...

  5. JAVAEE——SpringBoot日志篇:日志框架SLF4j、日志配置、日志使用、切换日志框架

    Spring Boot 日志篇 1.日志框架(故事引入) 小张:开发一个大型系统: ​ 1.System.out.println(""):将关键数据打印在控制台:去掉?写在一个文件 ...

  6. SpringBoot实战(四)获取接口请求中的参数(@PathVariable,@RequestParam,@RequestBody)

    上一篇SpringBoot实战(二)Restful风格API接口中写了一个控制器,获取了前端请求的参数,现在我们就参数的获取与校验做一个介绍: 一:获取参数 SpringBoot提供的获取参数注解包括 ...

  7. SpringBoot实战(二)Restful风格API接口

    在上一篇SpringBoot实战(一)HelloWorld的基础上,编写一个Restful风格的API接口: 1.根据MVC原则,创建一个简单的目录结构,包括controller和entity,分别创 ...

  8. springboot实战开发全套教程,让开发像搭积木一样简单!Github星标已上10W+!

    前言 先说一下,这份教程在github上面星标已上10W,下面我会一一给大家举例出来全部内容,原链接后面我会发出来!首先我讲一下接下来我们会讲到的知识和技术,对比讲解了多种同类技术的使用手日区别,大家 ...

  9. apollo客户端springboot实战(四)

    1. apollo客户端springboot实战(四) 1.1. 前言   经过前几张入门学习,基本已经完成了apollo环境的搭建和简单客户端例子,但我们现在流行的通常是springboot的客户端 ...

随机推荐

  1. 详解spl_autoload_register()  函数(转)

    原文地址:http://blog.csdn.net/panpan639944806/article/details/23192267 在了解这个函数之前先来看另一个函数:__autoload. 一._ ...

  2. MySQL刷新事务日志级别设置

    标签(linux): mysql 笔者Q:972581034 交流群:605799367.有任何疑问可与笔者或加群交流 # if set to 1 , InnoDB will flush (fsync ...

  3. Mysql了解及安装

    1.数据库由两部分来构成的 打开一个连接工具,用工具给MySQL发送命令,实际上是给数据库当中的服务下的命令,在服务当中解析命令,最终将命令转化成对物理库上文件IO的操作. 所以数据库的安装位置有两个 ...

  4. 2. getline()和get()

    1.面向行输入:getline() ---其实还可以接受第三个参数. getline()函数读取整行,调用该方法 使用cin.getline().该函数有两个参数, 第一个参数是是用来存储输入行的数组 ...

  5. 在线生成PDF的网站-HTML 转 PDF 在线

    http://pdf.df5d.com/   (服务器问题,演示暂停了,但是 下面介绍的组件还是可以使用的) 将前面用到的wkhtmltopdf用一个服务器程序集成在一起,接受一个URL参数,在生成一 ...

  6. ABP官方文档翻译 3.8 数据过滤器

    数据过滤器 介绍 预定义过滤器 ISoftDelete 何时使用? IMustHaveTenant 何时使用? IMayHaveTenant 何时使用 禁用过滤器 关于using语句 关于多租户 全局 ...

  7. ABP官方文档翻译 2.3 缓存

    缓存 介绍 ICacheManager 警告:GetCache方法 ICache ITypedCache 配置 实体缓存 实体缓存如何工作 Redis缓存集成 介绍 ABP为缓存提供了一个抽象接口,它 ...

  8. CF 570D. Tree Requests [dsu on tree]

    传送门 题意: 一棵树,询问某棵子树指定深度的点能否构成回文 当然不用dsu on tree也可以做 dsu on tree的话,维护当前每一个深度每种字母出现次数和字母数,我直接用了二进制.... ...

  9. (转载)Java:按值传递与按引用传递

    原链接:传送门 前天在做系统的时候被Java中参数传递问题卡了一下,回头查阅了相关的资料,对参数传递问题有了新的了解和掌握,但是有个问题感觉还是很模糊,就是Java中到底是否只存在值传递,因为在查阅资 ...

  10. Centos启动默认打开网络

    Centos打开网络 测试的时候发现网络没有打开,得到图像界面点击网络打开.比较麻烦去搜索了解决方法在此记录下来. 通过 /etc/sysconfig/network-script/, 编辑ifcfg ...