keyword:Spring ResponseBodyAdvice RequestBodyAdvice RequestMappingHandlerAdapter ServletInvocableHandlerMethod DispatcherServlet

需求:springMVC的rest接口对失败的接口进行日志打印。打印请求方法参数,URL,返回数据

思路1:

优点:实现简单,理解简单。缺点:不能打印请求参数内容,如果rest方法执行抛出异常无效

直接实现一个自定义的org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice。分析源码ResponseBodyAdvice#beforeBodyWrite方法执行的节点为

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse) {
// 省略
    if (selectedMediaType != null) {
      selectedMediaType = selectedMediaType.removeQualityValue();
      for (HttpMessageConverter<?> converter : this.messageConverters) {
        GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
            (GenericHttpMessageConverter<?>) converter : null);
        if (genericConverter != null ?
            ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
            converter.canWrite(valueType, selectedMediaType)) {
            // =========重点这里
          body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
              (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
              inputMessage, outputMessage);
          if (body != null) {
            Object theBody = body;
            LogFormatUtils.traceDebug(logger, traceOn ->
                "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
            addContentDispositionHeader(inputMessage, outputMessage);
            if (genericConverter != null) {
              genericConverter.write(body, targetType, selectedMediaType, outputMessage);
            }
            else {
              ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
            }
          }
          else {
            if (logger.isDebugEnabled()) {
              logger.debug("Nothing to write: null body");
            }
          }
          return;
        }
      }
    }
// 省略
}

可以看到12行的,在开始讲返回内容写入outputMessage之前会执行ResponseBodyAdvice#beforeBodyWrite的方法拿到新的返回对象,在这里你可以将rest接口方法返回的数据进行任意转换。org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyAdviceChain这里执行beforeBodyWrite方法是spring原生实现的类,可以看下该类的实现的beforeBodyWrite方法

  @Override
  @Nullable
  public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType,
      Class<? extends HttpMessageConverter<?>> converterType,
      ServerHttpRequest request, ServerHttpResponse response) {return processBody(body, returnType, contentType, converterType, request, response);
  }
  @Nullable
  private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,
      Class<? extends HttpMessageConverter<?>> converterType,
      ServerHttpRequest request, ServerHttpResponse response) {for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
      if (advice.supports(returnType, converterType)) {
        body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
            contentType, converterType, request, response);
      }
    }
    return body;
  }

注意14行,这里可以看出你注入的ResponseBodyAdvice不管在链条的哪个位置只要符合条件都会执行。在这里我们可以对返回对象是Result的设置支持处理,然后进行success判断然后日志打印

@Configuration
public class ResultLog implements ResponseBodyAdvice<Result.FailResult<?,?>> {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return Result.FailResult.class.isAssignableFrom(returnType.getParameterType());
    }@Override
    public Result.FailResult<?,?> beforeBodyWrite(Result.FailResult body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
       // log
        return body;
    }
}

前面说过这种模式缺点不能打印请求参数,这里可以让ResultLog实现org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice对方法请求参数进行缓存。


/**
 * @author by keray
 * date:2020/4/19 3:40 下午
 * 实现 {@link HandlerInterceptor} 保证参数线程缓存remove执行
 */
@Configuration
public class ResultLog extends RequestBodyAdviceAdapter implements ResponseBodyAdvice<Result.FailResult<?,?>>, HandlerInterceptor {
    
    private final ThreadLocal<List<ParamLog>> paramCache = new ThreadLocal<>();
    
    @Data
    @AllArgsConstructor
    public static class ParamLog {
        private String paramName;
        private Object value;
    }
    
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return Result.FailResult.class.isAssignableFrom(returnType.getParameterType());
    }@Override
    public Result.FailResult<?,?> beforeBodyWrite(Result.FailResult body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // log.error
        return body;
    }@Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }/** 
     * 缓存rest方法参数信息
     */
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        List<ParamLog> params = paramCache.get();
        if (params == null) {
            params = new LinkedList<>();
            paramCache.set(params);
        }
        params.add(new ParamLog(parameter.getParameterName(),body));
        return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
    }@Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // remove线程本地缓存
        paramCache.remove();
    }
}

org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice执行点和org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice基本一致。会在rest方法的每个参数解析完成后执行。具体可以看一下org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor的实现,核心实现在

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(org.springframework.http.HttpInputMessage, org.springframework.core.MethodParameter, java.lang.reflect.Type)

思路2:

优点:可以很简单打印方法参数,url,返回值。不需要threadLocal缓存请求参数,节省内存空间。对于接口异常也能进行日志输出。缺点:理解复杂,实现并不复杂,关联内容多。

实现:通过注入自定义的org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter实现需求重写createInvocableHandlerMethod方法实现自定义的org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeForRequest方法。怎么注入自己的org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter可以参考

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerAdapter
​
  @Bean
  public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
      @Qualifier("mvcConversionService") FormattingConversionService conversionService,
      @Qualifier("mvcValidator") Validator validator) {
​
    RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
    adapter.setContentNegotiationManager(contentNegotiationManager);
    adapter.setMessageConverters(getMessageConverters());
    adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
    adapter.setCustomArgumentResolvers(getArgumentResolvers());
    adapter.setCustomReturnValueHandlers(getReturnValueHandlers());if (jackson2Present) {
      adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
      adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
    }
​
    AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
    configureAsyncSupport(configurer);
    if (configurer.getTaskExecutor() != null) {
      adapter.setTaskExecutor(configurer.getTaskExecutor());
    }
    if (configurer.getTimeout() != null) {
      adapter.setAsyncRequestTimeout(configurer.getTimeout());
    }
    adapter.setCallableInterceptors(configurer.getCallableInterceptors());
    adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());return adapter;
  }

自定义的RequestMappingHandlerAdapter必须重写createInvocableHandlerMethod方法。最好可以重新getOrder方法。

new RequestMappingHandlerAdapter() {
        @Override
        public int getOrder() {
            return super.getOrder() - 1;
        }@Override
        protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) {
            return new IServletInvocableHandlerMethod(handlerMethod);
        }
    };
    ```
重要的9行,返回了一个IServletInvocableHandlerMethod对象,这个就是日志输出的核心类
​```java
/**
 * @author by keray
 * date:2020/4/19 1:01 上午
 */
@Slf4j(topic = "api-error")
public class IServletInvocableHandlerMethod extends ServletInvocableHandlerMethod {public IServletInvocableHandlerMethod(HandlerMethod handlerMethod) {
        super(handlerMethod);
    }@Override
    public Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
        for (Object o : args) {
            if (o instanceof IBaseEntity) {
                ((IBaseEntity) o).clearBaseField();
            }
        }
        if (logger.isTraceEnabled()) {
            logger.trace("Arguments: " + Arrays.toString(args));
        }
        Consumer<Object> logFail = result -> {
            try {
                if (result instanceof Result.FailResult || result instanceof Exception) {
                    String url = "错误";
                    String flag = "未知";
                    try {
                        HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
                        if (servletRequest != null) {
                            String aUrl = servletRequest.getRequestURL().toString();
                            if (StrUtil.isNotBlank(aUrl)) {
                                url = aUrl;
                            }
                            String aFlag = servletRequest.getHeader("X-User-Agent");
                            if (StrUtil.isBlank(aFlag)) {
                                aFlag = servletRequest.getHeader("User-Agent");
                            }
                            if (StrUtil.isNotBlank(aFlag)) {
                                flag = aFlag;
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    logFail(result, url, flag, args);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        };
        try {
            Object result = doInvoke(args);
            logFail.accept(result);
            return result;
        } catch (Exception e) {
            logFail.accept(e);
            throw e;
        }
    }private void logFail(Object result, String url, String flag, Object[] args) {
        StringBuilder builder = new StringBuilder();
        builder.append("\n").append("============接口异常============").append("\n");
        builder.append("  flag:").append(flag).append("\n");
        builder.append("   url:").append(url).append("\n");
        builder.append("  args:").append("\n");
        for (int i = 0; i < getMethodParameters().length; i++) {
            String json = "json解析失败";
            try {
                json = args[i] == null ? null : JSON.toJSONString(args[i]);
            } catch (Exception ignore) {
            }
            builder.append(getMethodParameters()[i].getParameterName()).append("=").append(json).append("\n");
        }
        if (result instanceof Result.FailResult) {
            builder.append("result:").append(StrUtil.format("code={},message={}", ((Result) result).getCode(), ((Result.FailResult) result).getMessage())).append("\n");
        } else {
            builder.append("result:").append(result.getClass()).append("\n");
        }
        builder.append("============end============");
        builder.append("\n");
        log.error(builder.toString());
    }}

这里的实现可以比较下spring的默认实现

  @Nullable
  public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
      Object... providedArgs) throws Exception {
​
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
      logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
  }

分析源码很简单理解。5行拿到rest方法执行参数,9行执行方法返回。和Method#invoke执行类似。

在这里我们就能拿到方法执行参数,方法放回参数,servletRequest。这样我们的错误日志数的全部信息都可以很简单的拿到。而且trc()cache{}我们能保证方法执行异常了一样能书出日志。当然异常日志也可以通过@ControllerAdvice + @ExceptionHandler的模式输出,只是这种实现依然不好拿到方法执行执行参数。

前面说到的注入自定义的org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter最简单实现是通过继承org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration对象,重新createRequestMappingHandlerAdapter方法就好了。这里注入自定义的IDelegatingWebMvcConfiguration有坑就是不能再项目显示使用org.springframework.web.servlet.config.annotation.EnableWebMvc注解,显示引入org.springframework.web.servlet.config.annotation.EnableWebMvc注解后,会导致自定义的IDelegatingWebMvcConfiguration的requestMappingHandlerAdapter方法不能执行。最终不能注入自定义的RequestMappingHandlerAdapter。

@Slf4j
@Configuration(proxyBeanMethods = false)
@Primary
@Order(0)
public class IDelegatingWebMvcConfiguration extends DelegatingWebMvcConfiguration {
​
​
    @Override
    protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
        return new RequestMappingHandlerAdapter() {
            @Override
            public int getOrder() {
                return super.getOrder() - 1;
            }@Override
            protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) {
                return new IServletInvocableHandlerMethod(handlerMethod);
            }
        };
    }
}

这里可以一下为什么注入了自定义的RequestMappingHandlerAdapter就能实现这个日志打印。
通过com.caishi.common.config.IServletInvocableHandlerMethod#invokeForRequest方法的调用栈可以找到在org.springframework.web.servlet.DispatcherServlet#doDispatch方法中执行了。这里执行的

ha.handle(processedRequest, response, mappedHandler.getHandler());

ha是org.springframework.web.servlet.HandlerAdapter也是RequestMappingHandlerAdapter实现的接口。ha的来源是

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
      for (HandlerAdapter adapter : this.handlerAdapters) {
        if (adapter.supports(handler)) {
          return adapter;
        }
      }
    }
    throw new ServletException("No adapter for handler [" + handler +
        "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
  }

可以看见熟悉的责任链,DispatcherServlet的handlerAdapters的初始化实现

  private void initHandlerAdapters(ApplicationContext context) {
    this.handlerAdapters = null;if (this.detectAllHandlerAdapters) {
      // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
      Map<String, HandlerAdapter> matchingBeans =
          BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
      if (!matchingBeans.isEmpty()) {
        this.handlerAdapters = new ArrayList<>(matchingBeans.values());
        // We keep HandlerAdapters in sorted order.
        AnnotationAwareOrderComparator.sort(this.handlerAdapters);
      }
    }
    else {
      try {
        HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
        this.handlerAdapters = Collections.singletonList(ha);
      }
      catch (NoSuchBeanDefinitionException ex) {
        // Ignore, we'll add a default HandlerAdapter later.
      }
    }// Ensure we have at least some HandlerAdapters, by registering
    // default HandlerAdapters if no other adapters are found.
    if (this.handlerAdapters == null) {
      this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
      if (logger.isTraceEnabled()) {
        logger.trace("No HandlerAdapters declared for servlet '" + getServletName() +
            "': using default strategies from DispatcherServlet.properties");
      }
    }
  }

这里可以看到只要实现了HandlerAdapter接口的bean都会本管理进来并排序了。所有前面说的最好重新自定义的RequestMappingHandlerAdapter的getOrder方法保证自定义的RequestMappingHandlerAdapter在有其他的RequestMappingHandlerAdapter注入时不能执行问题。

​效果:

============接口异常============
  flag:PostmanRuntime/7.24.0
   url:http://127.0.0.1:9368/v3/question/cq/note/list
  args:
pager={"currentPage":1,"offset":0,"page":0,"pageSize":10,"total":0}
app=true
userDetail=true
model={"userId":"20181219092713-3eb6590d-cab8-4500-9c44-eca45b5d9549"}
result:code=-1,message=null
============end============
​

flag可以 标识client源

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐