Spring源码学习(七):Spring MVC的请求响应流程

目录

1.方法到达service

1.1 processRequest方法

1.2 父类service方法

2.doDispatch方法

2.1 检查上传请求 —— checkMultipart方法

2.2 查找处理器 —— getHandler方法

2.2.1 RequestMappingHandlerMapping 的 getHandlerInternal 方法实现

2.2.2 getHandlerExecutionChain方法

2.2.3 CORS配置

2.2.4 DispatcherServlet对没有找到Handler的处理

2.3 寻找HandlerAdapter —— getHandlerAdapter方法

2.4 缓存(LastModified属性)的处理

2.5 拦截器发挥作用 —— preHandler、postHandler和afterCompletion

2.6 处理请求 —— handle方法、applyDefaultViewName方法

2.6.1 请求检查 checkRequest

2.6.2 触发处理器方法 invokeHandlerMethod

2.6.3 响应头处理

2.6.4 doDispatch的异步处理

2.6.5 设置默认视图名 —— applyDefaultViewName

2.7 处理分派结果 —— processDispatchResult

2.7.1 异常处理

2.7.2 页面渲染 —— render方法


1.方法到达service

当Spring MVC启动完毕,就可以开始准备接受来自客户端的请求了。根据Servlet规范,请求必须先发送到service()方法。在Spring MVC中,该方法实现在DispatcherServlet的父类FrameworkServlet:

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    if (HttpMethod.PATCH != httpMethod && httpMethod != null) {
        super.service(request, response);
    } else {
        this.processRequest(request, response);
    }
}

根据HttpMethod,即GET、POST、PUT、DELETE等请求类型来分派请求。这里将请求分为两类:PATCH&空,以及其他请求。对于前者,调用processRequest处理,否则调用父类的service方法处理。

1.1 processRequest方法

它首先备份了请求的LocaleContext和RequestAttributes,然后copy了一份新的,并绑定到当前线程:

LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);

RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

initContextHolders(request, localeContext, requestAttributes);

initContextHolders就是将localeContext和requestAttributes两个对象存入Holder。并且在方法最后,将备份的数据恢复过来,并触发请求处理完毕事件:

finally {
	resetContextHolders(request, previousLocaleContext, previousAttributes);
	if (requestAttributes != null) {
		requestAttributes.requestCompleted();
	}
	logResult(request, response, failureCause, asyncManager);
	publishRequestHandledEvent(request, response, startTime, failureCause);
}

备份和绑定操作完成后,调用它的核心方法doService,这是一个抽象方法,在DispatcherServlet实现,首先也是备份属性,并且最后也进行了恢复:

Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
	attributesSnapshot = new HashMap<>();
	Enumeration<?> attrNames = request.getAttributeNames();
	while (attrNames.hasMoreElements()) {
		String attrName = (String) attrNames.nextElement();
		if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
			attributesSnapshot.put(attrName, request.getAttribute(attrName));
		}
	}
}
...
finally {
	if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
		if (attributesSnapshot != null) {
			restoreAttributesAfterInclude(request, attributesSnapshot);
		}
	}
}

然后为request绑定Web容器、本地化解析器、主题解析器、主题、FlashMap管理器、inputFlashMap/outputFlashMap等属性,并调用doDispatch方法。

1.2 父类service方法

在父类的方法中,根据HttpMethod类型,分别调用了doXXX方法:

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    if (method.equals(METHOD_GET)) {
        ...
        doGet(req, resp);
        ...
    } else if (method.equals(METHOD_HEAD)) {
        ...
        doHead(req, resp);
    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);        
    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);
    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);
    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req,resp);
    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req,resp);
    } else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

对于未知类型的Http请求,则不进行分派,直接返回错误。这些do方法都实现在FrameworkServlet中,实际上都最终调用了processRequest,体现了集中入口、分协议转发、统一处理的特点。

2.doDispatch方法

先上一张流程图(来源于网络):

很清晰地描述了doDispatch方法的流程。

2.1 检查上传请求 —— checkMultipart方法

由于Spring MVC默认不支持文件上传,所以必须在请求处理的最开始就进行检查,以免在处理过程中才发现没有配置文件上传解析器,导致处理失败。checkMultipart方法源码如下:

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
    if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
        if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
            if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
                logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
            }
        }
        else if (hasMultipartException(request)) {
            logger.debug("Multipart resolution previously failed for current request - " +	"skipping re-resolution for undisturbed error rendering");
        }
        else {
            try {
                return this.multipartResolver.resolveMultipart(request);
            }
            catch (MultipartException ex) {
                if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
                    logger.debug("Multipart resolution failed for error dispatch", ex);
                }
                else {
                    throw ex;
                }
            }
        }
    }
    return request;
}

首先检查是否配置了文件上传解析器,以及请求中是否包含multipart部分,没有的话就可以不必继续了。

然后检查请求是否已经被处理过,或者曾经处理过但是有未解决的异常,这两种情况也不需要继续处理。如果请求还没处理过,则用文件上传解析器进行解析,成功解析后,将生成一个MultipartHttpServletRequest对象。这里以CommonsMultpartResolver为例说明,其resolveMultipart方法源码如下:

public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
    if (this.resolveLazily) {
        return new DefaultMultipartHttpServletRequest(request) {
            @Override
            protected void initializeMultipart() {
                MultipartParsingResult parsingResult = parseRequest(request);
                setMultipartFiles(parsingResult.getMultipartFiles());
                setMultipartParameters(parsingResult.getMultipartParameters());
                setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
            }
        };
    }
    else {
        MultipartParsingResult parsingResult = parseRequest(request);
        return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
    }
}

不难看出,懒加载方式下,只是重写了initializeMultipart方法,真正的解析还要等后面,非懒加载模式下,直接就进行解析了:

protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
    String encoding = determineEncoding(request);
    FileUpload fileUpload = prepareFileUpload(encoding);
    try {
        List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
        return parseFileItems(fileItems, encoding);
    }
    catch (FileUploadBase.SizeLimitExceededException ex) {
        throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
    }
    catch (FileUploadBase.FileSizeLimitExceededException ex) {
        throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
    }
    catch (FileUploadException ex) {
        throw new MultipartException("Failed to parse multipart servlet request", ex);
    }
}

这里有三种异常情况:请求本身太大、上传的文件太大、Request无法解析。

determineEncoding很简单,就是读取请求中character-encoding属性,没有就使用默认编码ISO-8859-1。

prepareFileUpload也很简单,就是创建一个FileUpload实例,赋一些属性:

protected FileUpload prepareFileUpload(@Nullable String encoding) {
    FileUpload fileUpload = getFileUpload();
    FileUpload actualFileUpload = fileUpload;
    if (encoding != null && !encoding.equals(fileUpload.getHeaderEncoding())) {
        actualFileUpload = newFileUpload(getFileItemFactory());
        actualFileUpload.setSizeMax(fileUpload.getSizeMax());
        actualFileUpload.setFileSizeMax(fileUpload.getFileSizeMax());
        actualFileUpload.setHeaderEncoding(encoding);
    }
    return actualFileUpload;
}

FileUpload类的parseRequest方法可以用于解析MultipartServletRequest,生成文件对象列表,它的原理就是读取请求中的数据,构造FileItem对象,然后存入列表中,去除了catch块之后的代码如下::

public List<FileItem> parseRequest(RequestContext ctx) throws FileUploadException {
    List<FileItem> items = new ArrayList<FileItem>();
    boolean successful = false;
    FileItemIterator iter = getItemIterator(ctx);
    FileItemFactory fac = getFileItemFactory();
    if (fac == null) {
        throw new NullPointerException("No FileItemFactory has been set.");
    }
    while (iter.hasNext()) {
        final FileItemStream item = iter.next();
        final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
        FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),item.isFormField(), fileName);
        items.add(fileItem);
        Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
        final FileItemHeaders fih = item.getHeaders();
        fileItem.setHeaders(fih);
    }
    successful = true;
    if (!successful) {
        for (FileItem fileItem : items) {
            try {
                fileItem.delete();
            } catch (Exception ignored) {
            }
        }
    }
}

getItemIterator方法从请求中读取了Content-Type、Content-Length、Character-Encoding等属性,构造了MultipartStream流,并返回了一个迭代器实例。每当调用hasNext时,就会调用findNextItem方法,解析MultipartStream并生成FileItemStream对象。

假如在生成FileItem的过程中抛出异常,则会执行fileItem.delete()方法显式清除临时文件。

FileItem是apache commons包的类,还需要进一步解析为Spring的MultipartParsingResult对象。此处根据FileItem对象里面封装的数据是一个普通文本表单字段,还是一个文件表单字段采用不同的处理方式。

对于文本表单字段,调用getString方法读取文件内容,然后存入multipartParameters中,读取Content-Type属性,存入multipartParameterContentTypes

String value;
String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
try {
    value = fileItem.getString(partEncoding);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值