从安全SDK的BUG看SpringMVC的参数解析

1. 问题描述

最近在工作中碰到安全组的同学,在编写一个用于加解密的 Filter时考虑问题不够周全导致的 bug。这个 Filter 的原意是在客户端根据一定的条件将请求的数据进行加密,同时在服务端对数据进行解密。于是,他写了一个 Filter 来过滤请求,部分代码如下所示:

1
2
3
4
5
6
7
8
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
SecurityHttpServletRequestWrapper requestWrapper = new SecurityHttpServletRequestWrapper(
(HttpServletRequest) request);
chain.doFilter(requestWrapper, response);

}

将原始的请求包装成一个自定义的 HttpServletRequestWrapper,并重写了请求中获取参数的方法,以此来达到解密的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class SecurityHttpServletRequestWrapper extends HttpServletRequestWrapper {

private Map<String, String[]> params;

private static final Charset DEFAULT_CHARSET = Charset.forName("utf-8");

/**
* Constructs a request object wrapping the given request.
*
* @param request
* @throws IllegalArgumentException if the request is null
*/
public SecurityHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
this.params = decodeParams(request);
}

/**
* 解码,需要区分版本,验签规则:
* 1.只签名不加密的情况下,服务器对所有参数验签,所以客户端在调用签名接口后再自行添加的参数会导致服务器验签失败
* 2.签名且加密的情况下,服务器只对加密部分验签,客户端自行添加的明文参数不会参与验签
*/
private Map<String,String []> decodeParams(HttpServletRequest request) {
...
}

@Override
public String getParameter(String name) {
...
}

@Override
public Map<String, String[]> getParameterMap() {
...
}

@Override
public Enumeration getParameterNames() {
...
}

@Override
public String[] getParameterValues(String name) {
...
}

}

自己定义了一个 Request Map,并对原始参数进行解密,同时覆盖一系列的 getXXX 方法,返回解密后的参数。应该说,在大部分情况下,这个设计是没错的,这也是安全 SDK 运行了好几个版本也没出什么问题的原因。

但是,我们如果去翻阅 HttpServletRequestWrapper 的源码会发现,这个类的方法数量远不止这么几个 getXXX,如下图所示:

upload successful

当然,并不是说这里所有的方法都需要去覆盖重写,但是与加解密安全相关的都必须要进行完整的覆盖,除了之前 SDK 中的 getXXX 之外,还至少应该覆盖如下的 API:

  • public int getContentLength()
  • public long getContentLengthLong()
  • public ServletInputStream getInputStream() throws IOException

其中:getContentLength() 以及 getContentLengthLong() 是因为加解密的过程势必会导致请求数据的大小发生变化,所以这里要重新计算并返回实际的数据长度。而 getInputStream() 则是因为有一些数据并不是通过 query 参数的方式进行传递的,比如我们今天要讨论的 @RequestBody

所以当采用以上 SDK 时,对于常规的参数接入方式完全没有问题,如:

1
2
3
4
5
6
7
@ApiOperation(value = "vaid", notes = "获取 VAID")
@PostMapping("/vaid")
public IdentifierItemDTO queryForVaid(@ApiParam(value = "包名") @RequestParam String packageName,
@ApiParam(value = "应用 ID") @RequestParam String appId,
@ApiParam(value = "签名") @RequestParam String signature) {
...
}

但是如果你尝试采用 @RequestBody 来接收参数,则会发生参数无法解析的问题:

1
2
3
4
5
@ApiOperation(value = "vaid", notes = "获取 VAID")
@PostMapping("/vaid")
public IdentifierItemDTO queryForVaid(@ApiParam(value = "应用三元组") @RequestBody AppTriple triple) {
...
}

因为 @RequestBody 的内容来自于 Request.getInputStream() ,而原 SDK 并没有对这部分数据进行处理。

2. SpringMVC 的参数处理流程

如果你用过 Spring2.x 及以下的版本,可能会比较清楚,通常我们在配置 SpringMVC 时,需要手动的给 HandlerAdapter 添加各种 HandlerMethodArgumentResolver,而这个 HandlerMethodArgumentResolver 就是用来处理各类参数转换及绑定的,由于太过久远,我们就不分析2.x版本的实现原理了。

2.1 RequestMappingHandlerMapping

我们看看从Spring3.x 之后提供的基于 Namespace 的配置方式(原理大同小异)。与 HandlerMethodArgumentResolver 相关的配置如下所示:

1
2
3
4
5
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="resolver.MyResolver"/>
</mvc:argument-resolvers>
</mvc:annotation-driven>

它是包含在 \mvc:annotation-drive\ 中的,所以我们从源头分析起,这个 Namespace 的处理入口类是 MvcNamespaceHandler

1
2
3
4
5
6
7
8
9
10
public class MvcNamespaceHandler extends NamespaceHandlerSupport {

@Override
public void init() {
registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
registerBeanDefinitionParser("default-servlet-handler", new DefaultServletHandlerBeanDefinitionParser());
...
}

}

这个节点是由 AnnotationDrivenBeanDefinitionParser 去解析的,在这个解析器里面非常重要的一点是定义了如下的 SpringBean——RequestMappingHandlerMapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
Object source = parserContext.extractSource(element);
XmlReaderContext readerContext = parserContext.getReaderContext();

CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
parserContext.pushContainingComponent(compDefinition);

RuntimeBeanReference contentNegotiationManager = getContentNegotiationManager(element, source, parserContext);
...
RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
handlerAdapterDef.setSource(source);
handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters);
addRequestBodyAdvice(handlerAdapterDef);
addResponseBodyAdvice(handlerAdapterDef);
...
}

这个 Bean 非常重要,是 SpringMVC 的几个核心之一,甚至说是 核心的也不为过。它定义了数据解析参数绑定返回值处理视图解析内容绑定消息转换等等一系列的组件,所以这些组件的相互配合完成了所有 SpringMVC 的使命。

2.2 HandlerMethodArgumentResolver

上一节提到 RequestMappingHandlerMapping 是整个 SpringMVC 中最核心的组件,而其中整合了我们今天的主角:HandlerMethodArgumentResolver;这个组件主要有两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 策略接口,用于将请求中的参数根据相应的配置绑定到方法参数上去
*/
public interface HandlerMethodArgumentResolver {

/**
* 判断该策略是否适用于该参数
*/
boolean supportsParameter(MethodParameter parameter);

/**
* 实际解析方法参数的过程
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;

}

这是设计模式中的 策略模式 的运用,整个 SpringMVC 中提供了许多的不同场景下的实现,在真正进行参数解析与绑定时,会遍历所有的 Resolver ,调用其 supportsParameter() 方法来确定是否支持当前这个参数的处理,如果支持,则再调用 resolveArgument() 方法来实现真正的参数解析(同时还会进行相应的绑定、转移过程,这利益于方法参数中的 WebDataBinderFactory 的支持)。

注:在 Spring 中,几乎所有的策略模式的实现都是类似这样的,先中收集所有的策略,然后supportsXXX() 方法来确定该策略是否符合当前的场景,最终在符合要求的策略上调用其 executeXXX() 之类的方法来完成真正的能力实现。

2.3 系统自带的 HandlerMethodArgumentResolver

了解了 HandlerMethodArgumentResolver 后我们再回到 RequestMappingHandlerMapping 组件中,看看 SpringMVC 自带了多少的 Resolver ,添加自带 Resolver 的过程在其 afterPropertiesSet() 方法,这个是 SpringBean 的生命周期回调方法(可以参考我另一篇文章《Spring中Bean的生命周期》),将在 Bean 构造完成后进行回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();

if (this.argumentResolvers == null) {

// 添加系统自带的各种 ArgumentResolver
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();

this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}

再看看这个获取自带 Resolver 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Return the list of argument resolvers to use including built-in resolvers
* and custom resolvers provided via {@link #setCustomArgumentResolvers}.
*/
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();

// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());

// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}

// Catch-all
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));

return resolvers;
}

可以看到共分为四类:

  • Annotation-based argument resolution:基于注解处理的参数解析器
  • Type-based argument resolution:基于类型的参数解析器
  • Custom arguments resolution:用户自定义的解析器,我们在配置文件中添加的自定义解析器 就属于这类
  • Catch-all:其它

这四个类型囊括了 SpringMVC 中所有的参数解析,使得我们在编写 Controller.method 时,不再需要手动的去 request 中去获取参数,绝大部分情况下只需要定义参数(或者注解参数)即可,而各个不同的 Resolver 会根据我们的定义(注解)完成对应的参数解析与绑定。

3. @RequestBody 的解析

了解了 SpringMVC 对于参数的解析过程后,我们再来看看第一章遇到的问题产生的原因及解决方案:@RequestBody 注解的参数是如何解析绑定定?

3.1 supportsParameter()

根据我们前面的分析,在四个参数解析器分类中,@RequestBody 的的解析应该是属于 基于注解处理的参数解析器 ,所以我们只需要在系统默认注册的那些基于注解处理的参数解析器中去查找分析即可。

同时,前面也提到,具体哪个 Resolver 会应用到这个参数的解析过程是由其 supportsParameter() 方法来决定的,那么就很容易的找到 RequestResponseBodyMethodProcessor 组件,我们看看它的源码部分:

1
2
3
4
5
6
7
8
9
10
11
12
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {

...

@Override
public boolean supportsParameter(MethodParameter parameter) {
// 只处理标注有 *@RequestBody* 的参数
return parameter.hasParameterAnnotation(RequestBody.class);
}

...
}

非常的简单,也很直接的通过 parameter.hasParameterAnnotation(RequestBody.class) 来声明该策略只处理标注有 @RequestBody 的参数;

3.2 resolveArgument()

真正解析参数的源码过程如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {

...

/**
* Throws MethodArgumentNotValidException if validation fails.
* @throws HttpMessageNotReadableException if {@link RequestBody#required()}
* is {@code true} and there is no body content or if there is no suitable
* converter to read the content with.
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

parameter = parameter.nestedIfOptional();

// 解析参数并进行相应的转换
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());

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 adaptArgumentIfNecessary(arg, parameter);
}

...
@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);

// InputMessage 实际上就是 ServletServerHttpRequest
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);

Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null) {
if (checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getMethod().toGenericString());
}
}
return arg;
}
}

注意:这里参数中的 InputMessage 实际就是 ServletServerHttpReques ,不要被名字给迷惑了

readWithMessageConverters 是最底层的参数解析及转移的过程,这个过程源码相对比较复杂,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/**
* Create the method argument value of the expected parameter type by reading
* from the given HttpInputMessage.
* @param <T> the expected type of the argument value to be created
* @param inputMessage the HTTP input message representing the current request
* @param parameter the method parameter descriptor (may be {@code null})
* @param targetType the target type, not necessarily the same as the method
* parameter type, e.g. for {@code HttpEntity<String>}.
* @return the created method argument value
* @throws IOException if the reading from the request fails
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/
@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

// 解析 MediaType ,即我们请求中的 Content-Type
MediaType contentType;
boolean noContentType = false;
try {
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}

// 解析参数类型,为后续的类型转换器作准备
Class<?> contextClass = (parameter != null ? parameter.getContainingClass() : null);
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = (parameter != null ?
ResolvableType.forMethodParameter(parameter) : ResolvableType.forType(targetType));
targetClass = (Class<T>) resolvableType.resolve();
}

HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod();
Object body = NO_VALUE;

try {
// 包装一个 EmptyBodyCheckingHttpInputMessage
inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);

// MessageConverter 是参数的转换器
// 同样的套路,熟悉的模式:策略模式(收集-canXXX()/supportsXX()-doXXX())
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}

// 读取 body 内容,并进行转换器的前置、后置处理
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = genericConverter.read(targetType, contextClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
else if (targetClass != null) {
if (converter.canRead(targetClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}

// 读取 body 内容,并进行转换器的前置、后置处理
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex);
}

if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && inputMessage.getBody() == null)) {
return null;
}
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
}

return body;
}

抛开 Header 信息处理,分支处理,数据转换等,真正核心的数据都保存在 InputMessage.getBody(),而这个InputMessage 实际上就是 ServletServerHttpRequest,所以我们看看 ServletServerHttpRequest.getBody() 方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
@Override
public InputStream getBody() throws IOException {
// 通过表单提交数据
if (isFormPost(this.servletRequest)) {
return getBodyFromServletRequestParameters(this.servletRequest);
}
// 直接在 body 里面提交数据
else {
return this.servletRequest.getInputStream();
}
}

这里分了两种场景:

  • 通过表单提交:判定的依据是 content-type=”application/x-www-form-urlencoded”,同时 httpMethod=POST,这个时候,会将所有的表单序列化成字节流。如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * Use {@link javax.servlet.ServletRequest#getParameterMap()} to reconstruct the
    * body of a form 'POST' providing a predictable outcome as opposed to reading
    * from the body, which can fail if any other code has used the ServletRequest
    * to access a parameter, thus causing the input stream to be "consumed".
    */
    private static InputStream getBodyFromServletRequestParameters(HttpServletRequest request) throws IOException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
    Writer writer = new OutputStreamWriter(bos, FORM_CHARSET);

    Map<String, String[]> form = request.getParameterMap();
    for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
    String name = nameIterator.next();
    List<String> values = Arrays.asList(form.get(name));
    for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext();) {
    String value = valueIterator.next();
    writer.write(URLEncoder.encode(name, FORM_CHARSET));
    if (value != null) {
    writer.write('=');
    writer.write(URLEncoder.encode(value, FORM_CHARSET));
    if (valueIterator.hasNext()) {
    writer.write('&');
    }
    }
    }
    if (nameIterator.hasNext()) {
    writer.append('&');
    }
    }
    writer.flush();

    return new ByteArrayInputStream(bos.toByteArray());
    }
  • 通过 body 提交:直接返回 ServletRequest.getInputStream() 即可

4. 总结

根据以上的分析,我们很清楚的知道了第一节中提到的 BUG 的问题根源所在:@RequestBody 参数的绑定是由 RequestResponseBodyMethodProcessor 完成的,而 RequestResponseBodyMethodProcessor 在进行 resolveArgument 时会通过 ServletRequest.getInputStream() 读取数据流,从而完成最终的数据转换、绑定。而安全 SDK 并没有对 ServletRequest.getInputStream() 进行任何的支持,从而导致读取的数据(加密后)不符合 application/json 的格式规范,进而抛出异常。

知道原因后解决的方案就比较清晰了,就是在加解密的时候对于 ServletRequest.getInputStream() 数据同样要进行相应的处理。

当然,更严谨的来说,前面提到的 getContentLength()getContentLengthLong() 最好也要进行处理,虽然这里并没有涉及到这两个方法的执行。但 对于一些网络相关的优化或者压缩支持的时候,这两个方法是非常重要的,当实际的数据长度和这两个方法返回的数据不一致,会导致一些丢包或者数据展示不全的 bug

5. 后记

现在 Spring/SpringBoot 的更新迭代与优化后,框架的自动化程度越来越高,编写 Spring 相关的应用会越来越简单,提高了开发的生产效率。

但是,另一方面,过多的自动化,过多的细节隐藏导致的后果是越来越多的人不知道背后的原理,一旦出现问题时手足无措,不知道从哪里开始分析与排查。

所以,这里建议,想要真正学好用好 Spring 框架,可以尝试使用 Spring1.x/2.x 的版本去写一些测试 demo 应用。这两个版本的 Spring 配置过程比较复杂,各个组件都需要自己去配置,关联关系都需要自己去维护,但正因为如此,开发者更能够理解各个组件的功能、之间的依赖关系,更能够从底层的去了解 Spring 的运行原理。