背景: Web项目一般都会做日志审计,一般在接口层面控制,无论是通过AOP方式,还是接口内部,我们都可以控制日志的输出。SpringBoot中,也提供了接口日志记录的扩展点,下面将列出相关配置及源码。
一、关联配置项
@ConfigurationProperties(
prefix = "spring.mvc"
)
public class WebMvcProperties {
private boolean publishRequestHandledEvents = true;
}
二、配置项生效位置
@Conditional({DefaultDispatcherServletCondition.class})
@ConditionalOnClass({ServletRegistration.class}) @EnableConfigurationProperties({WebMvcProperties.class})
protected static class DispatcherServletConfiguration {
@Bean(
name = {"dispatcherServlet"}
)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
// 在此读取配置,并设置到Servlet中
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
}
可以看到SpringBoot日志开关默认是开启的,但是我们访问接口的时候并没有看到相关日志,因为还需要我们自行监听日志发布事件。
三、自定义监听事件
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.ServletRequestHandledEvent;
import org.springframework.web.servlet.DispatcherServlet;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@Component
public class RequestHandledEventListener {
// 重点:借助Request,我们可自定义更多逻辑,如设置参数内容
@Autowired
private HttpServletRequest request;
@EventListener(ServletRequestHandledEvent.class)
public void requestHandled(ServletRequestHandledEvent event) {
DispatcherServlet source = (DispatcherServlet) event.getSource();
}
}
通过监听ServletRequestHandledEvent事件,根据Event所包含的信息,我们可以知道本次请求很多相关的信息,如:
private final String requestUrl; // 请求URI,如/doLogin
private final String clientAddress; // 客户端IP
private final String method; // 请求方法,POST或GET等
private final Object source; // DispatcherServlet实例
private final String servletName; // SpringBoot为dispatcherServlet
private final int statusCode; // 状态码
private String sessionId; // 会话ID
private String userName; // 用户相关
private final long processingTimeMillis; // 耗时【毫秒|MS】
private Throwable failureCause; // 失败堆栈
通过事件的getDescription方法,我们可以看到事件格式化后的描述,但此方法判断请求是否成功是通过判断failureCause!=null,而不是statusCode,因为现在都是框架封装全局异常,此方法输出的status=[ok]会存在歧义。
日志示例:
description:url=[/doLogin]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[86ms]; status=[failed: java.lang.IllegalArgumentException: 非法参数异常]
五、事件触发位置
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = this.buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
this.initContextHolders(request, localeContext, requestAttributes);
try {
// 接口请求处理方法,由DispatcherServlet实现
this.doService(request, response);
} catch (IOException | ServletException var16) {
failureCause = var16;
throw var16;
} catch (Throwable var17) {
failureCause = var17;
throw new NestedServletException("Request processing failed", var17);
} finally {
this.resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
this.logResult(request, response, (Throwable)failureCause, asyncManager);
this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
}
}
private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response, long startTime, @Nullable Throwable failureCause) {
// 此处判断状态,并通过上下文发布日志事件
if (this.publishEvents && this.webApplicationContext != null) {
long processingTime = System.currentTimeMillis() - startTime;
this.webApplicationContext.publishEvent(new ServletRequestHandledEvent(this, request.getRequestURI(), request.getRemoteAddr(), request.getMethod(), this.getServletConfig().getServletName(), WebUtils.getSessionId(request), this.getUsernameForRequest(request), processingTime, failureCause, response.getStatus()));
}
}
}
总结:
1、借助SpringBoot框架,可以省去我们管理日志的一般需求,所有接口都可记录
2、只有响应日志,无请求接收日志,一般只有响应日志具备价值,请求的核心在于安全和解析处理
3、一般业务日志记录,会涉及请求部分参数内容,默认不支持,可通过全局注入HttpServletRequest对象,借助请求属性功能间接获取,即接口属性设值再在此处获取。关于全局获取Request对象,参见这里。
4、如果需要知道请求对应接口方法相关的信息,用来特殊信息登记,如获取安全注解内容,可参考这篇文章。(这样就不用借助AOP + 注解方式来实现全局代理处理)
5、关于接口相关的全局封装,可参考这篇文章。