当前位置: 首页 > news >正文

SpringBoot记录用户操作日志

  • 一、RequestHandlerMappingInfoCache
    • RequestHandlerMappingInfo
  • 二、拦截器Interceptor
    • HttpCallLogInfo
  • 三、HttpServletRequestWrapper
  • 四、存储调用日志
  • 五、Config
    • HttpCallLogProperties

在很多系统平台中台,特别是一些敏感的后台系统,需要对用户的操作日志进行全链路记录,所以需要后台拦截所有Http调用日志。

由于大多开发框架都整合了Swagger,所以本次操作以Swagger为基准进行扩展。

一、RequestHandlerMappingInfoCache

首先,我们需要将SpringMVC中的所有请求信息进行缓存,方便后续通过请求URL获取一些日志记录需要的信息,如:方法、方法描述等。

import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;@Slf4j
public class RequestHandlerMappingInfoCache implements CommandLineRunner, EnvironmentAware, ApplicationContextAware {private static final Map<String, RequestHandlerMappingInfo> REQUEST_HANDLER_MAPPING_INFO_MAP = new ConcurrentHashMap<>(128);private ApplicationContext applicationContext;private Environment environment;@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) {this.applicationContext = applicationContext;}@Overridepublic void run(String... args) throws Exception {log.info("requestHandlerMappingInfoCache>>>init start");RequestMappingHandlerMapping requestMappingHandlerMapping = applicationContext.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();String systemName = environment.getProperty("http.call-log.systemName", environment.getProperty("spring.application.name"));handlerMethods.forEach((requestMappingInfo, handlerMethod) -> {// 类名String className = handlerMethod.getMethod().getDeclaringClass().getName();// 方法名String methodName = handlerMethod.getMethod().getName();// 方法描述String requestMappingDesc = findRequestMappingDesc(handlerMethod.getMethod().getDeclaredAnnotations());// 获取所有urlSet<String> urls = getRequestMappingInfoAllUrl(requestMappingInfo);if (CollectionUtils.isEmpty(urls)) {return;}for (String url : urls) {RequestHandlerMappingInfo info = new RequestHandlerMappingInfo();info.setSystemName(systemName);info.setRequestUrl(url);info.setClassName(className);info.setMethodName(methodName);if (StringUtils.isBlank(requestMappingDesc)) {info.setRequestMappingDesc(url);} else {info.setRequestMappingDesc(requestMappingDesc);}REQUEST_HANDLER_MAPPING_INFO_MAP.put(url, info);}});log.info("requestHandlerMappingInfoCache>>>init stop, RequestHandlerMappingInfoMap size={}", REQUEST_HANDLER_MAPPING_INFO_MAP.size());}/*** 获取所有url* @param requestMappingInfo* @return*/private Set<String> getRequestMappingInfoAllUrl(RequestMappingInfo requestMappingInfo) {if (requestMappingInfo.getPatternsCondition() != null && !CollectionUtils.isEmpty(requestMappingInfo.getPatternsCondition().getPatterns())) {return requestMappingInfo.getPatternsCondition().getPatterns();}if (requestMappingInfo.getPathPatternsCondition() != null && !CollectionUtils.isEmpty(requestMappingInfo.getPathPatternsCondition().getPatternValues())) {return requestMappingInfo.getPathPatternsCondition().getPatternValues();}return null;}public RequestHandlerMappingInfo getRequestHandlerMappingInfo(String url) {return REQUEST_HANDLER_MAPPING_INFO_MAP.get(url);}/*** 查找接口描述信息** @param annotations* @return*/private String findRequestMappingDesc(Annotation[] annotations) {if (annotations == null) {return null;}for (Annotation annotation : annotations) {if (annotation instanceof Operation) {Operation operation = (Operation) annotation;if (StringUtils.isBlank(operation.summary())) {return operation.description();}return operation.summary();}}return null;}
}

RequestHandlerMappingInfo

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;@Data
@AllArgsConstructor
@NoArgsConstructor
public class RequestHandlerMappingInfo implements Serializable {private static final long serialVersionUID = 6719771449788171956L;/*** 系统名称*/private String systemName;/*** 请求url*/private String requestUrl;/*** 描述*/private String requestMappingDesc;/*** 类名*/private String className;/*** 方法名*/private String methodName;
}

二、拦截器Interceptor

添加拦截器,拦截请求并记录相关信息。

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.ServiceLoader;@Slf4j
public class HttpCallLogInterceptor implements HandlerInterceptor {public static final ThreadLocal<HttpCallLogInfo> HTTP_CALL_LOG_INFO_THREAD_LOCAL = new ThreadLocal<>();public static final String URL_PARAMS_KEY = "urlParams";public static final String BODY_PARAMS_KEY = "bodyParams";private final RequestHandlerMappingInfoCache requestHandlerMappingInfoCache;private final HttpCallLogProperties httpCallLogProperties;public HttpCallLogInterceptor(RequestHandlerMappingInfoCache requestHandlerMappingInfoCache, HttpCallLogProperties httpCallLogProperties) {this.requestHandlerMappingInfoCache = requestHandlerMappingInfoCache;this.httpCallLogProperties = httpCallLogProperties;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {try {String requestURI = getUrl(request);RequestHandlerMappingInfo requestHandlerMappingInfo = requestHandlerMappingInfoCache.getRequestHandlerMappingInfo(requestURI);if (httpCallLogProperties.isEnable() && requestHandlerMappingInfo != null) {HttpCallLogInfo httpCallLogInfo = new HttpCallLogInfo();httpCallLogInfo.setSystemName(requestHandlerMappingInfo.getSystemName());httpCallLogInfo.setRequestUrl(requestHandlerMappingInfo.getRequestUrl());httpCallLogInfo.setRequestMappingDesc(requestHandlerMappingInfo.getRequestMappingDesc());httpCallLogInfo.setTraceId("TraceId");httpCallLogInfo.setRequestStartTime(new Date());httpCallLogInfo.setRequestParams(this.getAllRequestParams(request));httpCallLogInfo.setOperateUser("操作用户");HTTP_CALL_LOG_INFO_THREAD_LOCAL.set(httpCallLogInfo);}} catch (Exception e) {log.error("httpCallLogInterceptor>>>exception", e);}return true;}/*** 获取url,只带一个 / 前缀* @param request* @return*/private String getUrl(HttpServletRequest request) {String url = request.getRequestURI();String prefix = "//";while (StringUtils.isNotBlank(url) && url.startsWith(prefix)) {url = url.substring(1);}return url;}/*** 获取所有请求参数* @param request* @return*/private String getAllRequestParams(HttpServletRequest request) {// 获取url所有参数Map<String, Object> urlParams = getUrlParams(request);// 获取body参数String bodyParams = getBodyParams(request);if (CollectionUtils.isEmpty(urlParams) && StringUtils.isBlank(bodyParams)) {return null;}if (StringUtils.isBlank(bodyParams)) {return JSON.toJSONString(urlParams);}if (CollectionUtils.isEmpty(urlParams)) {return bodyParams;}Map<String, Object> paramMap = new HashMap<>(2);paramMap.put(URL_PARAMS_KEY, urlParams);paramMap.put(BODY_PARAMS_KEY, bodyParams);return JSON.toJSONString(paramMap);}private Map<String, Object> getUrlParams(HttpServletRequest request) {Map<String, Object> params = new HashMap<>();Enumeration<String> parameterNames = request.getParameterNames();while (parameterNames.hasMoreElements()) {String name = parameterNames.nextElement();params.put(name, request.getParameter(name));}return params;}private String getBodyParams(HttpServletRequest request) {if (request instanceof BodyReaderHttpServletRequestWrapper) {BodyReaderHttpServletRequestWrapper requestWrapper = (BodyReaderHttpServletRequestWrapper) request;return requestWrapper.getBody();}return null;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HTTP_CALL_LOG_INFO_THREAD_LOCAL.remove();}
}

HttpCallLogInfo

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;
import java.util.Date;@Data
@AllArgsConstructor
@NoArgsConstructor
public class HttpCallLogInfo implements Serializable {private static final long serialVersionUID = -737060657123005568L;/*** 系统名称*/private String systemName;/*** 描述*/private String requestMappingDesc;/*** 请求url*/private String requestUrl;private String traceId;/*** 请求时间*/private Date requestStartTime;/*** 请求参数JSON*/private String requestParams;/*** 响应结果*/private String responseResult;/*** 执行时间(毫秒)*/private Long executeTime;/*** 操作用户*/private String operateUser;
}

三、HttpServletRequestWrapper

由于 HttpServletRequest 中的 inputStream 读取一次就会失效,所以需要进行包装,利用 HttpServletRequestWrapper 进行扩展。

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;@Getter
@Slf4j
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {private final String body;public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {super(request);body = getBodyString(request);}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));return new ServletInputStream() {@Overridepublic boolean isFinished() {return byteArrayInputStream.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener readListener) {}@Overridepublic int read() throws IOException {return byteArrayInputStream.read();}};}/*** 获取请求Body*/private String getBodyString(ServletRequest request) {StringBuilder sb = new StringBuilder();try (InputStream inputStream = request.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {String line;while ((line = reader.readLine()) != null) {sb.append(line);}} catch (Exception e) {log.error("bodyReaderHttpServletRequestWrapper>>>exception", e);}return sb.toString();}
}

四、存储调用日志

利用 ResponseBodyAdvice 进行响应结果出来,并进行Http调用日志存储。

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Queues;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;@ControllerAdvice(basePackages = "com.lzq.http")
@Slf4j
public class HttpCallLogAutoStorage implements ResponseBodyAdvice<Object> {private static final int CPUS = Runtime.getRuntime().availableProcessors() <= 0 ? 64 : Runtime.getRuntime().availableProcessors();private final ExecutorService executorService = new ThreadPoolExecutor(CPUS * 2, CPUS * 2 + 1, 2, TimeUnit.MINUTES,Queues.newLinkedBlockingDeque(),new DefaultThreadFactory("HTTP-CALL-LOG-AUTO-STORAGE"),new ThreadPoolExecutor.CallerRunsPolicy());@Overridepublic boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) {if (Objects.isNull(methodParameter) || Objects.isNull(methodParameter.getMethod())) {return false;}Class<?> returnType = methodParameter.getMethod().getReturnType();// 只拦截返回类型为 Result 的方法return Result.class.equals(returnType);}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {try {HttpCallLogInfo httpCallLogInfo = HttpCallLogInterceptor.HTTP_CALL_LOG_INFO_THREAD_LOCAL.get();if (httpCallLogInfo != null) {executorService.execute(() -> {log.info("httpCallLogAutoStorage>>>publish event");if (StringUtils.isBlank(httpCallLogInfo.getResponseResult())) {httpCallLogInfo.setResponseResult(JSON.toJSONString(body));}httpCallLogInfo.setExecuteTime(System.currentTimeMillis() - httpCallLogInfo.getRequestStartTime().getTime());// TODO 存储});}} catch (Exception e) {log.error("httpCallLogAutoStorage>>>exception", e);}return body;}
}

五、Config

将上述对象注入到Spring IoC容器中。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;@AutoConfiguration
@ConditionalOnProperty(prefix = "http.call-log", value = "enable", havingValue = "true")
@EnableConfigurationProperties(value = HttpCallLogProperties.class)
public class HttpCallLogConfiguration implements WebMvcConfigurer {@Autowiredprivate HttpCallLogInterceptor httpCallLogInterceptor;@Autowiredprivate HttpCallLogProperties httpCallLogProperties;@Overridepublic void addInterceptors(InterceptorRegistry registry) {List<String> pathPatterns = httpCallLogProperties.getPathPatterns();if (CollectionUtils.isEmpty(pathPatterns)) {pathPatterns = Collections.singletonList("/**"); // 拦截所有请求}registry.addInterceptor(httpCallLogInterceptor).addPathPatterns(pathPatterns).excludePathPatterns(httpCallLogProperties.getExcludePathPatterns());}@Beanpublic HttpCallLogAutoStorage httpCallLogAutoStorage() {return new HttpCallLogAutoStorage();}/*** Http调用日志监听器** @return*/@Beanpublic HttpCallLogInfoListener httpCallLogInfoListener(List<IHttpCallLogStorageService> httpCallLogStorageServices,HttpCallLogProperties httpCallLogProperties) {return new HttpCallLogInfoListener(httpCallLogStorageServices, httpCallLogProperties);}@Configuration@ConditionalOnProperty(prefix = "http.call-log", value = "enable", havingValue = "true")@EnableConfigurationProperties(value = HttpCallLogProperties.class)public static class HttpCallLogConfig {@Beanpublic RequestHandlerMappingInfoCache requestHandlerMappingInfoCache() {return new RequestHandlerMappingInfoCache();}/*** Http调用日志拦截器*/@Beanpublic HttpCallLogInterceptor httpCallLogInterceptor(RequestHandlerMappingInfoCache requestHandlerMappingInfoCache,HttpCallLogProperties httpCallLogProperties) {return new HttpCallLogInterceptor(requestHandlerMappingInfoCache, httpCallLogProperties);}/*** 过滤器*/@Beanpublic FilterRegistrationBean httpCallLogAccessFilter() {return new FilterRegistrationBean((req, res, chain) -> {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;if ("POST".equalsIgnoreCase(request.getMethod())) {chain.doFilter(new BodyReaderHttpServletRequestWrapper(request), response);return;}chain.doFilter(req, res);});}}
}

HttpCallLogProperties

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;import java.util.Collections;
import java.util.List;@Data
@ConfigurationProperties(prefix = "http.call-log")
public class HttpCallLogProperties {private boolean enable;private String systemName;/*** 需要拦截的url*/private List<String> pathPatterns;/*** 排除的url*/private List<String> excludePathPatterns = Collections.EMPTY_LIST;
}
http://www.xdnf.cn/news/327583.html

相关文章:

  • 紫光同创FPGA实现HSSTHP光口视频传输+图像缩放,基于Aurora 8b/10b编解码架构,提供3套PDS工程源码和技术支持
  • windows使用bat脚本激活conda环境
  • TI Code Composer Studio编译时SDK报错问题解决
  • 鸿蒙NEXT开发动画案例3
  • 写程序,统计两会政府工作报告热词频率,并生成词云
  • 2025-05-07 Unity 网络基础7——TCP异步通信
  • 卷积神经网络基础(六)
  • Python 运维脚本
  • AI系列:智能音箱技术简析
  • void*在c语言中什么意思(非常详细)
  • scanpy处理:使用自定义 python 函数读取百迈客空间转录组数据(百创智造S1000)
  • 深度学习:智能车牌识别系统(python)
  • htop筛选进程时,出现重复进程
  • 德州仪器技术干货 | 48V 集成式热插拔电子保险丝:为现代 AI 数据中心高效供电
  • Python案例实战《水果识别模型训练及调用》
  • Linux 内核学习(7) --- 字符设备驱动
  • eFish-SBC-RK3576工控板CAN接口测试操作指南
  • leetcode 3341. 到达最后一个房间的最少时间 I 中等
  • Unity_JK框架【3】 事件系统的简单使用示例
  • 169.多数元素
  • openstack虚拟机状态异常处理
  • java集合菜鸟教程
  • 从 CodeBuddy Craft 到 edgeone-pages-mcp 上线算命网站的一次完整体验分享
  • 多语言网站的 UX 陷阱与国际化实践陷阱清单
  • 前端面试每日三题 - Day 27
  • 【Python】os模块
  • 使用 Gradio + Qwen3 + vLLM 部署 Text2SQL 多表查询系统
  • 【Prometheus】深入解析 Prometheus 特殊标签 `__param_<name>`:动态抓取参数的艺术
  • Android 数据持久化之数据库存储 Room 框架
  • 50个精选DeepSeek指令