參考: 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 是一個圍繞請求的一個處理。