经过一系列管道之后,来到StandardWrapperValve。Wrapper属于Tomcat中4个级别容器中最小级别的容器,与之对应的是Servlet。
路由映射器确定了Wrapper,此处将会实例化。单例则使用已经创建的,否则每次都创建。在SpringBoot中,默认对应的是DispatcherServlet。
过滤器链
在StandardWrapperValue中将创建一个过滤器链ApplicationFilterChain对象,创建时过滤器链对象做了如下处理(代码见ApplicationFilterFactory->createFilterChain()):
- 从Context容器中获取所有过滤器的相关信息
- 通过URL匹配过滤链,匹配的加入到过滤器链中
- 通过Servlet名称匹配过滤链,匹配的加入到过滤器中
创建完成后,将调用过滤器链的doFilter方法,doFilter方法默认调用internalDoFilter方法,拿到过滤器之后,调用过滤器的doFilter方法,过滤器完成,继续调用过滤器链的doFilter方法进行下一步的操作。
过滤器链全部调用完成后,会调用servlet的service方法,至此Tomcat部分结束。
OrderedCharacterEncodingFilter
将请求与响应的字符编码置为utf-8
OrderedHiddenHttpMethodFilter
如果是POST请求,能够将请求参数_method中的值转为实际的请求,如PUT、DELTE、PATCH等
OrderedFormContentFilter
如果是PUT、PATCH、DELETE请求并且类型是application/x-www-form-urlencoded的话,分析body内容获取参数params,如果params不为空,则封装请求为一个FormContentRequestWrapper然后继续过滤器链的调用(之后使用getParameter()可以获得body内请求参数),否则使用原来的请求继续过滤器链的调用(Tomcat中,application/x-www-form-urlencoded只解析POST类型的请求体)
OrderedRequestContextFilter
将请求和响应封装成ServletRequestAttributes,并放入RequestContextHolder中。它的应用:获得httpServletRequest的方式。
WsFilter
与WebSocket有关
自定义过滤器
实现Filter接口并注入IoC容器即可
请求体的解析
Http11Processor->service()方法解析请求行与请求体,初次调用Request->getParameter()方法时,会对请求体进行解析。
Request->getParameter():
if (!parametersParsed) {
parseParameters();
}
return coyoteRequest.getParameters().getParameter(name);
// getParameters()返回的是一个Parameters对象,key为参数名称,值为参数值的List集合
Request->parseParameters();
parametersParsed = true;
Parameters parameters = coyoteRequest.getParameters();
......
// 记录url中的请求参数
parameters.handleQueryParameters();
// 如果调用过getInputStream或者是getReader方法,就不会解析了
if (usingInputStream || usingReader) {
success = true;
return;
}
......
// form-data形式
if ("multipart/form-data".equals(contentType)) {
// 解析完之后返回
parseParts(false);
success = true;
return;
}
// 下面可以认为是application/x-www-form-urlencode形式的解析
// 如果不是POST请求的话,直接跳过
if( !getConnector().isParseBodyMethod(getMethod()) ) {
success = true;
return;
}
// 如果body类型不是application/x-www-form-urlencode的话,不解析直接就返回了
if (!("application/x-www-form-urlencoded".equals(contentType))) {
success = true;
return;
}
// getContentLength是Content-Length的值,表示的是请求体的长度,如果len为0说明没有内容,跳过解析
int len = getContentLength();
if (len > 0) {
// 有值的话就解析并且加入到请求参数集合的Map中
......
}
如果是以form-data形式请求,读出请求体的内容,先创建临时目录,对参数取随机文件名,创建文件并放入临时目录中。加入到parts,如果请求体中有fileName的话,认为是文件,没有fileName的话认为是请求参数,执行parameters->addParameter()方法。
MVC部分中的DispatcherServlet->doDispatch():processedRequest = checkMultipart(request);,如果contentType以multipart/开头,会将请求类型从RequestFacade转为StandardMultipartHttpServletRequest,表示这是一个多部分的请求类型。执行完方法后,删除暂存目录中的文件。
如图,multipartParameterNames是form-data中text类型的请求参数集合,multipartFiles是file类型的请求参数集合,它的key是参数名,value是文件信息,包含上传的文件名,文件流,文件暂存位置等等。业务逻辑中可通过
StandardMultipartHttpServletRequest获取文件相关信息做各种操作。
DispatcherServlet->doDispatch():
finally {
if (multipartRequestParsed) {
// 如果是form-data类型,删除暂存目录中的文件
cleanupMultipart(processedRequest);
}
}
getParameter()对multipart/form-data和application/x-www-form-urlencode的请求体进行了解析,在请求参数的绑定中,如果遇到了@RequestBody,这个时候才会解析请求体
附录:多次获得请求体
在业务逻辑中会有记录请求信息的需求,一种是给Controller做AoP,这种方式只能记录mapping中url存在的情况,另外一种是利用Filter,在到达MVC之前记录参数,但这种方式要注意,body流只能读取一次,如果参数中有@RequestBody就会报错,因此需要有一种方式多次获得请求体。
方式一:ContentCachingRequestWrapper
public class DemoFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if ("application/json".equals(request.getContentType())) {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
byte[] bytes = new byte[request.getContentLength()];
requestWrapper.getInputStream().read(bytes);
request = requestWrapper;
}
filterChain.doFilter(request, response);
}
}
将请求对象转为ContentCachingRequestWrapper,它继承了HttpServletRequestWrapper,HttpServletRequestWrapper实现了HttpServletRequest接口,从名字看出它是对请求的包装。
ContentCachingRequestWrapper->getInputStream():
private final ByteArrayOutputStream cachedContent;
private ServletInputStream inputStream;
@Override
public ServletInputStream getInputStream() throws IOException {
if (this.inputStream == null) {
this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
}
return this.inputStream;
}
ContentCachingRequestWrapper->ContentCachingInputStream->read():
@Override
public int read() throws IOException {
int ch = this.is.read();
if (ch != -1 && !this.overflow) {
if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) {
this.overflow = true;
handleContentOverflow(contentCacheLimit);
} else {
cachedContent.write(ch);
}
}
return ch;
}
ContentCachingRequestWrapper内部的inputStream是自定义的流,它继承了抽象类ServletInputStream,需要重写read方法,从上可以看出,read方法是请求流读取,所以依然只能读取一次,但是它将读取的结果存入了cachedContent对象中,所以后面可以从cachedContent获取请求体结果,对此,controller需要相应的改动
DemoController->test():
@RequestMapping("/test/**")
public String test2(HttpServletRequest httpServletRequest) throws Exception {
JSONObject jsonObject = JSON.parseObject(new String(((ContentCachingRequestWrapper) httpServletRequest).getContentAsByteArray()));
return "test**";
}
原先写在方法里的@RequestBody JSONObject jsonObject就不能再使用了,将请求对象转为ContentCachingRequestWrapper,并调用getContentAsByteArray()获取请求体转为String类型,实现了请求体的二次获取。
方式二:重写读取流
第一种方式存在弊端,所有的方法都要改动,成本很大,但是它的实现思路值得参考,那就是缓冲流以及read方法的重写。
首先写一个自定义类BodyReaderHttpServletRequestWrapper继承HttpServletRequestWrapper
private final byte[] body;
private ServletInputStream inputStream;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
byte[] bytes = new byte[request.getContentLength()];
request.getInputStream().read(bytes);
body = bytes;
}
@Override
public ServletInputStream getInputStream() throws IOException {
this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
return this.inputStream;
}
private class ContentCachingInputStream extends ServletInputStream {
private final ServletInputStream is;
private final ByteArrayInputStream byteArrayInputStream;
public ContentCachingInputStream(ServletInputStream is) {
this.is = is;
byteArrayInputStream = new ByteArrayInputStream(body);
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
// ......其他必须要实现的方法......
}
在自定义类的构造方法中读取请求体,赋给body对象,getInputStream()方法每次新建自定义流。自定义流有两个成员变量,一个是请求流,一个是缓冲流,构造方法中初始化缓冲流的值为请求体内容,之后的read()方法从缓冲流读取,这样就保证了每次流的读取都是从起始位置开始。
public class DemoFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if ("application/json".equals(request.getContentType())) {
BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) request);
request = requestWrapper;
}
}
@RequestMapping("/test/**")
public String test2(@RequestBody String string, @RequestBody JSONObject jsonObject) throws Exception {
return "test**";
}
不管多少次读取流,都没有关系了。