Springboot 记录api接口调用记录
在对第三方系统提供接口调用时,通常需要对接口调用情况进行记录以便问题追踪和排查。本文记录一下利用 HandlerInterceptor 实现对接口调用情况的记录。
主要思路
-
如何同时获取到请求和响应并进行记录?
基本思路:使用 Interception 进行请求拦截,对请求体和响应体进行解析,提取感兴趣的内容转换为数据模型;
存在问题:请求体 HttpServletRequest 和响应体 HttpServletResponse 中的数据流默认只能读取一次,在Interceptor中读取过之后会导致后续程序无法获取到正常数据。 -
如何重复读取HttpServletRequest 和 HttpServletResponse 中的数据流而不影响后续程序?
基本思路:创建继承自 HttpServletRequestWrapper 的请求包装类 BodyCachingHttpServletRequestWrapper,创建继承自 HttpServletResponseWrapper 的响应包装类BodyCachingHttpServletResponseWrapper,包装类可以实现数据流的重复读。
并在Filter中将原始的 ServletRequest 替换为 BodyCachingHttpServletRequestWrapper 包装类,将 ServletResponse 替换为 BodyCachingHttpServletResponseWrapper 包装类,以使 Interceptor 获取到的请求和响应均是包装类,达到读取流之后不影响后续程序。 -
使用自定义注解,实现对接口调用记录 的可配置。
-
获取api接口定义的基础地址,例如 /say/{message},以应用于后续接口统计。
API请求拦截主要实现(主要思路第1点)
编写实现了 HandlerInterceptor 接口的拦截器 ApiLogInterceptor ,在preHandle中记录请求信息,并把信息设置到了request上,可供Controller层使用;在afterCompletion 中记录响应,获取到完整的 ApiLogRecord 信息。
将 ApiLogInterceptor 配置进拦截器,以使拦截器生效。
/**
* API接口请求日志拦截器
* <bold>需要在WebMvcConfigurationSupport的子类中进行注册才能使当前拦截器生效,本项目注册见RequestInterceptorConf.</bold>
*
* @author: Zemel
* @date: 2020-08-06 17:25
*/
public class ApiLogInterceptor implements HandlerInterceptor {
private final Logger logger = LoggerFactory.getLogger(ApiLogInterceptor.class);
private static final String ATTR_REQUEST_ID = "Request-Id";
private static final String ATTR_REQUEST_LOG = "request-log";
private static final String DIRECTION_RECEIVE = "RECEIVE";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
logRequest(request, handler);
} catch (Exception e) {
logger.error("logRequest error", e);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// do nothing
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
try {
logResponse(request, response, handler);
} catch (Exception e) {
logger.error("logResponse error", e);
}
}
private void onLogRecord(ApiLogRecord apiLogRecord) {
logger.info("log api request: \n {}", apiLogRecord);
}
private void logRequest(HttpServletRequest request, Object handler) throws IOException {
if (!(handler instanceof HandlerMethod)) {
// 不是controller配置的接口,例如swagger,不进行记录
return;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
// controller配置的api接口地址
ApiInfo apiInfo = ApiInfoUtil.resolve(handlerMethod);
ApiLog apiLog = getApiLogAnnotation(handlerMethod);
if (apiInfo == null ||
(apiLog != null && !apiLog.log())) {
// 不记录request
return;
}
ApiLogRecord apiLogRecord = new ApiLogRecord();
apiLogRecord.setDirection(DIRECTION_RECEIVE);
apiLogRecord.setApiPath(apiInfo.getApiPath());
apiLogRecord.setRequestAddr(request.getRemoteAddr());
String url = request.getRequestURI();
String queryParams = request.getQueryString();
if (queryParams != null) {
try {
url = url + "?" + URLDecoder.decode(queryParams, "UTF-8");
} catch (UnsupportedEncodingException e) {
logger.error("拼接queryString error", e);
}
}
apiLogRecord.setRequestUrl(url);
apiLogRecord.setRequestMethod(request.getMethod());
apiLogRecord.setRequestTime(new Date());
JSONObject headerJson = new JSONObject();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
headerJson.put(headerName, request.getHeader(headerName));
}
apiLogRecord.setRequestHeaders(headerJson);
Map<String, String[]> requestParameterMap = request.getParameterMap();
JSONObject bodyJson = new JSONObject();
for (Map.Entry<String, String[]> entry : requestParameterMap.entrySet()) {
bodyJson.put(entry.getKey(), entry.getValue());
}
apiLogRecord.setRequestParams(bodyJson);
if ((request instanceof BodyCachingHttpServletRequestWrapper)
&& (apiLog == null || apiLog.requestBody())) {
BodyCachingHttpServletRequestWrapper requestWrapper = (BodyCachingHttpServletRequestWrapper) request;
apiLogRecord.setRequestBody(new String(requestWrapper.getBody()));
}
String requestId = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + UUID.randomUUID().toString().substring(0, 3);
apiLogRecord.setId(requestId);
request.setAttribute(ATTR_REQUEST_ID, requestId);
request.setAttribute(ATTR_REQUEST_LOG, apiLogRecord);
}
private void logResponse(HttpServletRequest request, HttpServletResponse response, Object handler) {
Object requestId = request.getAttribute(ATTR_REQUEST_ID);
if (requestId == null) {
// 没有requestId标记,什么都不记录
return;
}
ApiLogRecord apiLogRecord = (ApiLogRecord) request.getAttribute(ATTR_REQUEST_LOG);
apiLogRecord.setResponseTime(new Date());
Long timeConsumed = apiLogRecord.getResponseTime().getTime() - apiLogRecord.getRequestTime().getTime();
apiLogRecord.setTimeConsumed(timeConsumed);
apiLogRecord.setResponseStatus(response.getStatus());
HandlerMethod handlerMethod = (HandlerMethod) handler;
ApiLog apiLog = getApiLogAnnotation(handlerMethod);
if ((response instanceof BodyCachingHttpServletResponseWrapper)
&& (apiLog == null || apiLog.responseBody())) {
BodyCachingHttpServletResponseWrapper responseWrapper = (BodyCachingHttpServletResponseWrapper) response;
apiLogRecord.setResponseBody(new String(responseWrapper.getBody()));
}
this.onLogRecord(apiLogRecord);
}
/**
* 获取接口上的ApiLogConf注解,方法上的注解优先级高于类上的注解
*/
private static ApiLog getApiLogAnnotation(HandlerMethod handlerMethod) {
ApiLog apiLog = null;
if (handlerMethod.getBeanType().isAnnotationPresent(ApiLog.class)) {
// 因为ApiLog
apiLog = AnnotationUtils.getAnnotation(handlerMethod.getBeanType(), ApiLog.class);
}
// 接口参数解析
if (handlerMethod.getMethod().isAnnotationPresent(ApiLog.class)) {
apiLog = AnnotationUtils.getAnnotation(handlerMethod.getMethod(), ApiLog.class);
}
return apiLog;
}
}
/**
* 请求拦截器配置
*
* @author: Zemel
* @date: 2020-08-06 17:29
*/
@Configuration
public class RequestInterceptorConf extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
// 添加requestLog拦截器
registry.addInterceptor(new ApiLogInterceptor()).addPathPatterns("/**");
super.addInterceptors(registry);
}
}
使拦截器中的请求和响应流可重复读(主要思路第2点)
/**
* HttpServletRequestWrapper 包装类,用于缓存请求体以实现请求体的多次读取
*
* @author: Zemel
* @date: 2020-08-10 15:11
* @see com.dimpt.rest.config.BodyCachingHttpServletResponseWrapper
* @see com.dimpt.rest.config.BodyCachingWrapperFilter
*/
public class BodyCachingHttpServletRequestWrapper extends HttpServletRequestWrapper {
private ServletInputStreamWrapper inputStreamWrapper;
private byte[] body;
public BodyCachingHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = StreamUtils.copyToByteArray(request.getInputStream());
this.inputStreamWrapper = new ServletInputStreamWrapper(new ByteArrayInputStream(this.body));
resetInputStream();
}
private void resetInputStream() {
this.inputStreamWrapper.setInputStream(new ByteArrayInputStream(this.body != null ? this.body : new byte[0]));
}
public byte[] getBody() {
return body;
}
@Override
public ServletInputStream getInputStream() throws IOException {
return this.inputStreamWrapper;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.inputStreamWrapper));
}
private static class ServletInputStreamWrapper extends ServletInputStream {
private InputStream inputStream;
ServletInputStreamWrapper(InputStream inputStream) {
this.inputStream = inputStream;
}
void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public boolean isFinished() {
return true;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
// 只用来缓存,不需要设置读监听器
}
@Override
public int read() throws IOException {
return this.inputStream.read();
}
}
}
/**
* HttpServletResponse包装类,用于缓存响应体以实现响应体的多次读取
*
* @author: Zemel
* @date: 2020-08-10 15:14
* @see com.dimpt.rest.config.BodyCachingHttpServletRequestWrapper
* @see com.dimpt.rest.config.BodyCachingWrapperFilter
*/
public class BodyCachingHttpServletResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
private HttpServletResponse response;
public BodyCachingHttpServletResponseWrapper(HttpServletResponse response) {
super(response);
this.response = response;
}
public byte[] getBody() {
return byteArrayOutputStream.toByteArray();
}
@Override
public ServletOutputStream getOutputStream() {
return new ServletOutputStreamWrapper(this.byteArrayOutputStream, this.response);
}
@Override
public PrintWriter getWriter() throws IOException {
return new PrintWriter(new OutputStreamWriter(this.byteArrayOutputStream, this.response.getCharacterEncoding()));
}
private static class ServletOutputStreamWrapper extends ServletOutputStream {
private ByteArrayOutputStream outputStream;
private HttpServletResponse response;
public ServletOutputStreamWrapper(ByteArrayOutputStream outputStream, HttpServletResponse response) {
this.outputStream = outputStream;
this.response = response;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener listener) {
// 只用来缓存,不需要设置读监听器
}
@Override
public void write(int b) throws IOException {
this.outputStream.write(b);
}
@Override
public void flush() throws IOException {
if (!this.response.isCommitted()) {
byte[] body = this.outputStream.toByteArray();
ServletOutputStream servletOutputStream = this.response.getOutputStream();
servletOutputStream.write(body);
servletOutputStream.flush();
}
}
}
}
/**
* 在Filter中将原始 HttpServletRequest 和 HttpServletResponse 替换为对应的缓存包装类,以在Interceptor 中对 body 进行读取
*
* @author: Zemel
* @date: 2020-08-10 15:26
* @see com.dimpt.rest.config.BodyCachingHttpServletRequestWrapper
* @see com.dimpt.rest.config.BodyCachingHttpServletResponseWrapper
*/
@Component
@WebFilter(filterName = "BodyCachingWrapperFilter", urlPatterns = "/**")
public class BodyCachingWrapperFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
BodyCachingHttpServletRequestWrapper requestWrapper = new BodyCachingHttpServletRequestWrapper((HttpServletRequest) request);
BodyCachingHttpServletResponseWrapper responseWrapper = new BodyCachingHttpServletResponseWrapper((HttpServletResponse) response);
// 这里用wrapper类代替,以达到可重复读的目的
chain.doFilter(requestWrapper, responseWrapper);
}
@Override
public void destroy() {
}
}
自定义注解使ApiLog可配置(主要思路第3点)
在ApiLogInterceptor中通过getApiLogAnnotation() 方法获取。
/**
* API接口请求记录配置项
* <p>获取注解请使用:AnnotationUtils.getAnnotation(BeanOrMethod, ApiLog.class), 否则aliasFor会不生效</p>
* 拦截器需要在
*
* @author Zemel
* @date: 2021-08-10
* @see com.dimpt.rest.anno.ApiLogInterceptor
* @see com.dimpt.rest.config.RequestInterceptorConf
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface ApiLog {
@AliasFor("log")
boolean value() default true;
/**
* 是否开启记录
*
* @return true if necessary
*/
@AliasFor("value")
boolean log() default true;
/**
* 是否记录请求体,只有当log()为true时生效
*
* @return true if necessary
*/
boolean requestBody() default true;
/**
* 是否记录响应体,只有当log()为true时生效
*
* @return true if necessary
*/
boolean responseBody() default true;
}
获取API接口基本信息(主要思路第4点)
- 定义记录api基本信息的实体类 ApiInfo,包含apiPath、httpMethod、beanName、beanMethod几个字段。
- 编写ApiInfoUtil根据 RequestMappingHandlerMapping 获取所有接口的 ApiInfo 信息。工具类在获取RequestMappingHandlerMapping 实例时用到了 SpringContextUtil 工具类。
public class ApiInfo {
private String apiPath;
private String httpMethod;
private String beanName;
private String beanMethod;
// setter 和 getter 省略
}
public class ApiInfoUtil {
private static Map<RequestMappingInfo, HandlerMethod> listRequestHandlerMapping() {
RequestMappingHandlerMapping mapping = SpringContextUtil.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
return mapping.getHandlerMethods();
}
public static List<ApiInfo> listApiInfo() {
//获取url与类和方法的对应信息
Map<RequestMappingInfo, HandlerMethod> map = listRequestHandlerMapping();
List<ApiInfo> apiInfoList = new ArrayList<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
RequestMappingInfo info = entry.getKey();
HandlerMethod handlerMethod = entry.getValue();
ApiInfo apiInfo = resolve(info, handlerMethod);
if (apiInfo != null) {
apiInfoList.add(apiInfo);
}
}
return apiInfoList;
}
public static ApiInfo resolve(HandlerMethod handlerMethod) {
RequestMappingInfo requestMappingInfo = null;
Map<RequestMappingInfo, HandlerMethod> requestMappingInfoHandlerMethodMap = listRequestHandlerMapping();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : requestMappingInfoHandlerMethodMap.entrySet()) {
if (entry.getValue().getMethod().toString().equals(handlerMethod.getMethod().toString())) {
requestMappingInfo = entry.getKey();
break;
}
}
return resolve(requestMappingInfo, handlerMethod);
}
private static ApiInfo resolve(RequestMappingInfo requestMappingInfo, HandlerMethod handlerMethod) {
if (requestMappingInfo == null || handlerMethod == null) {
return null;
}
Set<String> patterns = requestMappingInfo.getPatternsCondition().getPatterns();
Set<RequestMethod> requestMethodSet = requestMappingInfo.getMethodsCondition().getMethods();
ApiInfo apiInfo = null;
if (patterns.iterator().hasNext() && requestMethodSet.iterator().hasNext()) {
String apiPath = patterns.iterator().next();
String httpMethod = requestMethodSet.iterator().next().toString();
apiInfo = new ApiInfo();
apiInfo.setApiPath(apiPath);
apiInfo.setHttpMethod(httpMethod);
apiInfo.setBeanName(handlerMethod.getBeanType().getName());
apiInfo.setBeanMethod(handlerMethod.getMethod().getName());
}
return apiInfo;
}
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringContextUtil.applicationContext == null) {
SpringContextUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}