spring mvc controller 方法處理參數的過程。RequestWrapper 包裝 @RequestBody 參數 。。 filter , inter,aop


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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM