在很多系统平台中台,特别是一些敏感的后台系统,需要对用户的操作日志进行全链路记录,所以需要后台拦截所有Http调用日志。
由于大多开发框架都整合了Swagger,所以本次操作以Swagger为基准进行扩展。
一、RequestHandlerMappingInfoCache
首先,我们需要将SpringMVC中的所有请求信息进行缓存,方便后续通过请求URL获取一些日志记录需要的信息,如:方法、方法描述等。
java
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;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public 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());
// 获取所有url
Set<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
java
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
添加拦截器,拦截请求并记录相关信息。
java
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;
}
@Override
public 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;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HTTP_CALL_LOG_INFO_THREAD_LOCAL.remove();
}
}
HttpCallLogInfo
java
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 进行扩展。
java
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);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public 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调用日志存储。
java
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()
);
@Override
public 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);
}
@Override
public 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容器中。
java
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 {
@Autowired
private HttpCallLogInterceptor httpCallLogInterceptor;
@Autowired
private HttpCallLogProperties httpCallLogProperties;
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> pathPatterns = httpCallLogProperties.getPathPatterns();
if (CollectionUtils.isEmpty(pathPatterns)) {
pathPatterns = Collections.singletonList("/**"); // 拦截所有请求
}
registry.addInterceptor(httpCallLogInterceptor)
.addPathPatterns(pathPatterns)
.excludePathPatterns(httpCallLogProperties.getExcludePathPatterns());
}
@Bean
public HttpCallLogAutoStorage httpCallLogAutoStorage() {
return new HttpCallLogAutoStorage();
}
/**
* Http调用日志监听器
*
* @return
*/
@Bean
public 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 {
@Bean
public RequestHandlerMappingInfoCache requestHandlerMappingInfoCache() {
return new RequestHandlerMappingInfoCache();
}
/**
* Http调用日志拦截器
*/
@Bean
public HttpCallLogInterceptor httpCallLogInterceptor(RequestHandlerMappingInfoCache requestHandlerMappingInfoCache,
HttpCallLogProperties httpCallLogProperties) {
return new HttpCallLogInterceptor(requestHandlerMappingInfoCache, httpCallLogProperties);
}
/**
* 过滤器
*/
@Bean
public 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
java
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;
}