参考: https://blog.csdn.net/q957967519/article/details/91544888
今天有个需求:每个请求设置一个唯一的标识,目前是用uuid,用于数据库主键,当然也用于打印日志的时候有个唯一标识。
目前的代码是这样的, Qrs 有个属性uuid.
@ResponseBody @RequestMapping(value = "/trans", method = RequestMethod.POST,produces = "application/json;charset=UTF-8") public String trans(@RequestBody Qrs req){ req.setUuid(xxx);
MDC.put("uuid",xxx); //MDC 是logback的一个设置公共参数的类。 在logback.xml 配置pattern 使用 %X{uuid}即可打印唯一标识了
}
这样写的话,我岂不是要在每个controller 方法都要加上这一句。 那就加个filter 在进入方法前统一加上就行了
public class MyFilter implements Filter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response) throws ServletException {
request.setAttribute("uuid",xxx);
....
}
}
加上后,Qrs 的 uuid 仍然是null。 于是去了解下 spring mvc 去如何给参数赋值的。
debug 跟源码总结 分两步 :
1. HandlerMethodArgumentsResolverComposite 遍历所有HandlerMethodArgumentResolver的实现类,调用其
supportsParameter 方法判断该resolver 是否可以处理该参数(通常判断依据就是参数的注解,如@RequestBody)
2. 根据找到的resolver , 调用其 resolveArgument()方法, 该方法中会调用相关的messageConverters 给参数赋值。
具体代码:
HandlerMethodArgumentResolverComposite 类: 1. 判断是否有能处理该参数的resolver public boolean supportsParameter(MethodParameter parameter) { return this.getArgumentResolver(parameter) != null; } 2. 如果有 ,存入argumentResolverCache(当有相同参数类型是,直接去该resolver) private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter); if (result == null) { Iterator var3 = this.argumentResolvers.iterator(); while(var3.hasNext()) { HandlerMethodArgumentResolver methodArgumentResolver = (HandlerMethodArgumentResolver)var3.next(); if (this.logger.isTraceEnabled()) { this.logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" + parameter.getGenericParameterType() + "]"); } if (methodArgumentResolver.supportsParameter(parameter)) { result = methodArgumentResolver; this.argumentResolverCache.put(parameter, methodArgumentResolver); break; } } } return result; } 3. 调用resolveArgument方法,实际是调用上一步找到resolver. public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter); Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]"); return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); }
本例 是@RequestBody 注解,所以找到resolver 是 RequestResponseBodyMethodProcessor
RequestResponseBodyMethodProcessor 类: @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class);//所以支持@RequestBody 注解的参数 } 2. 处理参数,主要readWithMessageConverters @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { Object arg = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter); WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); return arg; } 3. 该类继承AbstractMessageConverterMethodArgumentResolver,重要代码是 this.messageConverters 这个循环,找到相应转换类,跟踪代码找到的是MappingJackson2HttpMessageConverter @SuppressWarnings("unchecked") protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam, Type targetType) throws IOException, HttpMediaTypeNotSupportedException { MediaType contentType; try { contentType = inputMessage.getHeaders().getContentType(); } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotSupportedException(ex.getMessage()); } if (contentType == null) { contentType = MediaType.APPLICATION_OCTET_STREAM; } Class<?> contextClass = methodParam.getContainingClass(); Class<T> targetClass = (Class<T>) ResolvableType.forMethodParameter(methodParam, targetType).resolve(Object.class); for (HttpMessageConverter<?> converter : this.messageConverters) { if (converter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter; if (genericConverter.canRead(targetType, contextClass, contentType)) { if (logger.isDebugEnabled()) { logger.debug("Reading [" + targetType + "] as "" + contentType + "" using [" + converter + "]"); } return genericConverter.read(targetType, contextClass, inputMessage); } } if (converter.canRead(targetClass, contentType)) { if (logger.isDebugEnabled()) { logger.debug("Reading [" + targetClass.getName() + "] as "" + contentType + "" using [" + converter + "]"); } return ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage); } } throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); } 4. 对应 AbstractJackson2HttpMessageConverter.read 方法,读取请求流中的数据, 后面估计是利用反射赋值,没有再深入了解了。 private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) { try { return this.objectMapper.readValue(inputMessage.getBody(), javaType); } catch (IOException var4) { throw new HttpMessageNotReadableException("Could not read JSON: " + var4.getMessage(), var4); } }
总结:controller 方法中的@RequestBody pojo 对象是从 输入流中获取数据的,所以操作request.setAttribute 是没有作用的。
暂未找到实现文章开头需求的方法,有知道的大神请留言。
今天又心血来潮,还是想在进入方法前处理RequestBody 对象,要加日志,加验签, 找了一下,找到一个方法:
使用filter + RequestWrapper 对请求流包装。
继承HttpServletRequestWrapper ,可以读取request 的 流,然后在重写其getInputStream 把想要的数据在放回数据流
import org.apache.commons.lang.StringUtils; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; /** * @Author zc * @Date 2019/9/19 */ public class RequestWrapper extends HttpServletRequestWrapper { private final String body; /** * 处理数据: 验签 + 日志 * @param body * @return */ private String handlerData(String body){ if(StringUtils.isNotEmpty(body)){ } return body; } public RequestWrapper(HttpServletRequest request) throws IOException { super(request); //读取inputstream数据流,怎么读自己定。 StringBuilder stringBuilder = new StringBuilder(); BufferedReader bufferedReader = null; try { InputStream inputStream = request.getInputStream(); if (inputStream != null) { bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8")); char[] charBuffer = new char[128]; int bytesRead; while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { stringBuilder.append(charBuffer, 0, bytesRead); } } } catch (IOException ex) { throw ex; } finally { bufferedReader = null; } String bs = stringBuilder.toString(); //处理读取的数据,想怎么处理怎么处理 body = handlerData(bs); } @Override public ServletInputStream getInputStream() throws IOException { //记得将处理后的数据放回流中 final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes("UTF-8")); ServletInputStream servletInputStream = new ServletInputStream() { public boolean isFinished() { return false; } public boolean isReady() { return false; } public void setReadListener(ReadListener readListener) {} public int read(){ return byteArrayInputStream.read(); } }; return servletInputStream; } }
//filter 使用该包装类对request包装后,转发
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if(request instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) request);
}
if(requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
但是我使用本方式有个难点:我的秘钥需要从数据库读取,项目使用spring mvc , 我想用@Autowired注入机构service. 但是注意到包装类是
通过new RequestWrapper 方式创建实例的,而且filter 也是没法使用spring mvc 注解的。
上述问题解决办法:1. RequestBodyAdvice 和 ResponseBodyAdvice 分别对请求响应参数处理。
2. 直接使用aop环绕通知,方法前后织入代码
filter 对所有请求其作用,其依赖servlet容器。 inter 只是对action 起作用,不依赖servlet.
filter 就是一个方法回调, inter是基于java 反射的。
filter 可以理解是在进入请求之前的一个处理,而inter 是一个围绕请求的一个处理。