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源