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 是一个围绕请求的一个处理。
原文地址:https://www.cnblogs.com/zhangchenglzhao/p/11321344.html