【记录版】SpringBoot中Servlet接口请求日志记录

发布时间:2023年12月22日

SpringBoot + ServletRequestHandledEvent

背景: 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、关于接口相关的全局封装,可参考这篇文章

文章来源:https://blog.csdn.net/src1025/article/details/135099322
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。