Spring source learning: Spring MVC request response process

Keywords: Attribute Spring encoding Session

Catalog

1. Method arrival service

1.1 ProceRequest Method

1.2 Parent service Method

2.doDispatch method

2.1 Check Upload Request - Check Multipart Method

2.2 Find Processor-getHandler Method

2.2.1 Implementation of getHandler International Method for RequestMapping Handler Mapping

2.2.2 getHandler Execution Chain Method

2.2.3 CORS Configuration

2.2.4 Dispatcher Servlet Handler not found

2.3 Find Handler Adapter-getHandler Adapter Method

2.4 Processing of Cache (LastModified attribute)

2.5 Interceptors work - preHandler, postHandler and afterCompletion

2.6 Processing requests - handle method, applyDefaultViewName method

2.6.1 Request Check Request

2.6.2 Trigger Processor Method invokeHandler Method

2.6.3 Response Header Processing

2.6.4 doDispatch asynchronous processing

2.6.5 Set the default view name - applyDefaultViewName

2.7 Process Dispatch Result

2.7.1 Exception handling

2.7.2 Page Rendering-render Method

1. Method arrival service

When Spring MVC is started, you are ready to accept requests from clients. According to the Servlet specification, requests must first be sent to the service() method. In Spring MVC, this method implements the parent Framework Servlet of Dispatcher Servlet:

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);
    }
}

Requests are dispatched according to HttpMethod, namely GET, POST, PUT, DELETE and other request types. There are two types of requests: PATCH & empty, and other requests. For the former, call processRequest processing, otherwise call the service method of the parent class.

1.1 ProceRequest Method

It first backs up the requested LocalContext and Request Attributes, then copies a new one and binds it to the current thread:

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 is to store two objects, localeContext and requestAttributes, into Holder. At the end of the method, the backup data is restored and the request processing event is triggered:

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

After the backup and binding operations are completed, the core method doService is called. This is an abstract method, which is implemented in Dispatcher Servlet. First, it is also the backup attribute, and finally it is restored.

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);
		}
	}
}

Then bind Web container, local parser, theme parser, theme, FlashMap manager, inputFlashMap / output FlashMap and other properties for request, and call the doDispatch method.

1.2 Parent service Method

In the method of the parent class, according to the HttpMethod type, the doXXX method is called separately:

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);
    }
}

For an unknown type of Http request, no dispatch is made and an error is returned directly. These do methods are implemented in Framework Servlet. In fact, they all call process Request, which embodies the characteristics of centralized entry, sub-protocol forwarding and unified processing.

2.doDispatch method

First, a flow chart (from the network):

The flow of the doDispatch method is clearly described.

2.1 Check Upload Request - Check Multipart Method

Because Spring MVC does not support file upload by default, it must be checked at the very beginning of request processing, so that no configuration file upload parser can be found during processing, resulting in processing failure. The source code of the checkMultipart method is as follows:

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;
}

First, check whether the file upload parser is configured, and whether the request contains a multipart part part part. If not, you don't need to continue.

Then check if the request has been processed, or if it has been processed, but there are unresolved exceptions, neither of which needs to be processed further. If the request has not been processed, the file upload parser is used for parsing. After successful parsing, a MultipartHttpServletRequest object is generated. This paper takes Commons MultpartResolver as an example to illustrate its resolveMultipart method source code as follows:

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());
    }
}

It is not difficult to see that under the lazy loading mode, only the initializeMultipart method is rewritten, and the real parsing will wait for later. In the non-lazy loading mode, the parsing will be carried out directly:

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);
    }
}

There are three exceptions: the request itself is too large, the uploaded file is too large, and the Request cannot be parsed.

DeterminmineEncoding is very simple. It reads the character-encoding attribute in the request and uses the default encoding ISO-8859-1 without it.

prepareFileUpload is also simple: create an instance of FileUpload and assign some attributes:

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;
}

The parseRequest method of FileUpload class can be used to parse MultipartServletRequest and generate a list of file objects. Its principle is to read the data in the request, construct FileItem objects, and then store them in the list. After removing the catch block, the code is as follows:

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) {
            }
        }
    }
}

The getItemIterator method reads the properties of Content-Type, Content-Length, Character-Encoding from the request, constructs the MultipartStream stream, and returns an iterator instance. Whenever hasNext is called, the findNextItem method is called to parse the MultipartStream and generate FileItemStream objects.

If an exception is thrown during the generation of FileItem, the fileItem.delete() method is executed to explicitly clear the temporary file.

FileItem is a class of apache commons packages and needs to be further parsed into Spring's Multipartite ParsingResult object. According to whether the data encapsulated in the FileItem object is a normal text form field or a file form field, different processing methods are adopted here.

For text form fields, call the getString method to read the file content, then store it in multipartParameters, read the Content-Type attribute, and store it in multipartParameterContentTypes.

String value;
String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
try {
    value = fileItem.getString(partEncoding);
}
catch (UnsupportedEncodingException ex) {
    value = fileItem.getString();
}
String[] curParam = multipartParameters.get(fileItem.getFieldName());
if (curParam == null) {
    multipartParameters.put(fileItem.getFieldName(), new String[] {value});
}
else {
    String[] newParam = StringUtils.addStringToArray(curParam, value);
    multipartParameters.put(fileItem.getFieldName(), newParam);
}
multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());

For file form fields, create the Commons MultipartFile object directly and add it to the multipartFiles:

CommonsMultipartFile file = createMultipartFile(fileItem);
multipartFiles.add(file.getName(), file);

protected CommonsMultipartFile createMultipartFile(FileItem fileItem) {
	CommonsMultipartFile multipartFile = new CommonsMultipartFile(fileItem);
	multipartFile.setPreserveFilename(this.preserveFilename);
	return multipartFile;
}

In any case, the three coarsened Map objects mentioned above are passed into the constructor of MultipartParsingResult and a new instance is returned.

2.2 Find Processor-getHandler Method

This method actually traverses all Handler Mapping and calls its getHandler method one by one to see if its corresponding Handler can handle the request. If a Handler Mapping satisfies the condition, it returns directly, otherwise the loop continues. If there is no result until the traversal is complete, return null.

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

The getHandler method is implemented in the AbstractHandlerMapping class. The key source code is as follows:

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        handler = getDefaultHandler();
    }
    if (handler == null) {
        return null;
    }
    if (handler instanceof String) {
        String handlerName = (String) handler;
        handler = obtainApplicationContext().getBean(handlerName);
    }
    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
    if (CorsUtils.isCorsRequest(request)) {
        CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
        CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
        CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
        executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
    }
    return executionChain;
}

defaultHandler is generally null without explicit injection, so it is not considered. Obviously the core approach is getHandler International and getHandler Execution Chain, along with the following CORS processing logic.

2.2.1 Implementation of getHandler International Method for RequestMapping Handler Mapping

The Request Mapping Handler Mapping is still illustrated here. Its getHandlerInternal method is actually located in the parent AbstractHandlerMethodMapping:

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    this.mappingRegistry.acquireReadLock();
    try {
        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }
    finally {
        this.mappingRegistry.releaseReadLock();
    }
}

The first is to intercept the URL:

public String getLookupPathForRequest(HttpServletRequest request) {
	if (this.alwaysUseFullPath) {
		return getPathWithinApplication(request);
	}
	String rest = getPathWithinServletMapping(request);
	if (!"".equals(rest)) {
		return rest;
	}
	else {
		return getPathWithinApplication(request);
	}
}

Let's first look at the getPathWithin Application method, which takes the contextPath attribute and URI of the request, then excises the same part of the URI as contextPath and returns the rest:

public String getPathWithinApplication(HttpServletRequest request) {
	String contextPath = getContextPath(request);
	String requestUri = getRequestUri(request);
	String path = getRemainingPath(requestUri, contextPath, true);
	if (path != null) {
		return (StringUtils.hasText(path) ? path : "/");
	}
	else {
		return requestUri;
	}
}

For example, if contextPath is "/ hello" and URI is "/ hello/index.html", return "/ index.html".

Let's look at the getPathWithinServletMapping method, which first calls getPathWithinApplication, then obtains the servletPath attribute in the request, and then replaces "/" in pathWithinApp with "/". The servletPath is the part before the sign in the URI. For example, if the URI is "/ hello/index.html;a=1;b=2", the servletPath is "/ hello/index.html". Then the same parts of the URI as servletPath are removed, leaving the rest:

public String getPathWithinServletMapping(HttpServletRequest request) {
	String pathWithinApp = getPathWithinApplication(request);
	String servletPath = getServletPath(request);
	String sanitizedPathWithinApp = getSanitizedPath(pathWithinApp);
	String path;
    if (servletPath.contains(sanitizedPathWithinApp)) {
		path = getRemainingPath(sanitizedPathWithinApp, servletPath, false);
	}
	else {
		path = getRemainingPath(pathWithinApp, servletPath, false);
	}
	if (path != null) {
		return path;
	}
	else {
		String pathInfo = request.getPathInfo();
		if (pathInfo != null) {
			return pathInfo;
		}
		if (!this.urlDecode) {
			path = getRemainingPath(decodeInternal(request, pathWithinApp), servletPath, false);
			if (path != null) {
				return pathWithinApp;
			}
		}
		return servletPath;
	}
}

After intercepting the URL, we can match HandlerMethod. First, we need to see if there is a perfect match.

List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
    addMatchingMappings(directPathMatches, matches, request);
}

If not, look up all Handler Methods and find the best one to continue processing. Comparator compares the attributes in the order of request type, pattern condition, params condition, headers condition, consumes condition, produces condition, methods condition, customConditionHolder:

Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
	if (CorsUtils.isPreFlightRequest(request)) {
		return new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"));
	}
	Match secondBestMatch = matches.get(1);
	if (comparator.compare(bestMatch, secondBestMatch) == 0) {
		Method m1 = bestMatch.handlerMethod.getMethod();
		Method m2 = secondBestMatch.handlerMethod.getMethod();
		String uri = request.getRequestURI();
		throw new IllegalStateException("Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
	}
}
request.setAttribute(HandlerMapping.class.getName() + ".bestMatchingHandler", bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;

The handleMatch method is rewritten in the subclass RequestMapping InfoHandlerMapping. In addition to storing the matching/comparison results in the request, the values of the @MatrixVariable and @PathVariable annotations are parsed from the request and stored in the request.

If no Handler Method is matched (for example, no Handler Method is registered at all), the handleNoMatch method will be executed, and the method will also be rewritten. The actual logic is to go through the RequestMapping Info list again to see if there is a fish leaking from the net, and if not, throw an exception.

Then you need to instantiate Handler Method. At the time of HandlerMethod creation, the handler parameter passed in is actually BeanName. Obviously, it is impossible to make method calls without instances, so it needs to be instantiated:

public HandlerMethod createWithResolvedBean() {
	Object handler = this.bean;
	if (this.bean instanceof String) {
		String beanName = (String) this.bean;
		handler = this.beanFactory.getBean(beanName);
	}
	return new HandlerMethod(this, handler);
}

2.2.2 getHandler Execution Chain Method

The essence of this method is to add the Handler interceptor to the processing chain so that the interceptor can take effect:

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
	HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
		(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
	String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
	for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
		if (interceptor instanceof MappedInterceptor) {
			MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
			if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
				chain.addInterceptor(mappedInterceptor.getInterceptor());
			}
		}
		else {
			chain.addInterceptor(interceptor);
		}
	}
	return chain;
}

2.2.3 CORS Configuration

First, get the global CORS configuration. In fact, get the value from the Map:

public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
	String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
	for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
		if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
			return entry.getValue();
		}
	}
	return null;
}

Then you get the CORS configuration of the processor, essentially the same as above:

// in AbstractHandlerMethodMapping.java
protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
	CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request);
	if (handler instanceof HandlerMethod) {
		HandlerMethod handlerMethod = (HandlerMethod) handler;
		if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) {
			return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG;
		}
		else {
			CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod);
			corsConfig = (corsConfig != null ? corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod);
		}
	}
	return corsConfig;
}

// in AbstractHandlerMapping.java
protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
	Object resolvedHandler = handler;
	if (handler instanceof HandlerExecutionChain) {
		resolvedHandler = ((HandlerExecutionChain) handler).getHandler();
	}
	if (resolvedHandler instanceof CorsConfigurationSource) {
		return ((CorsConfigurationSource) resolvedHandler).getCorsConfiguration(request);
	}
	return null;
}

Then the local and global configurations are merged to create CORS interceptors and added to the processor execution chain:

protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
    HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
    if (CorsUtils.isPreFlightRequest(request)) {
        HandlerInterceptor[] interceptors = chain.getInterceptors();
            chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
    }
    else {
        chain.addInterceptor(new CorsInterceptor(config));
    }
    return chain;
}

This is consistent with the getHandler Execution Chain principle above, and will not be repeated.

2.2.4 Dispatcher Servlet Handler not found

Although no Match throws an exception in the process of finding Handler Method, Spring handles it in the Dispatcher Servlet for the simple reason that the server throws an exception, which can only be perceived by itself and can only be perceived by the client by returning an error through Response:

protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
	if (pageNotFoundLogger.isWarnEnabled()) {
		pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
	}
	if (this.throwExceptionIfNoHandlerFound) {
		throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
				new ServletServerHttpRequest(request).getHeaders());
	}
	else {
		response.sendError(HttpServletResponse.SC_NOT_FOUND);
	}
}

2.3 Find Handler Adapter-getHandler Adapter Method

The implementation here is simple: traverse the adapter list to see which Handler can be found, and return which one.

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
	if (this.handlerAdapters != null) {
		for (HandlerAdapter adapter : this.handlerAdapters) {
			if (adapter.supports(handler)) {
				return adapter;
			}
		}
	}
	throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

For RequestMapping Handler Adapter, just check if handler is a Handler Method type and return true.

2.4 Processing of Cache (LastModified attribute)

The server has a caching mechanism. When a user requests a resource for the first time, if the request succeeds, the server will return to HTTP 200 and add a LastModified field in the response header to record the last update time of the resource on the server. When the resource is requested again, the browser will add the If-Modified-Since field in the request header to ask the server about the resource from this time. Whether it has been updated or not, if it has not been updated, it will return to HTTP 304 directly without returning resources, thus saving bandwidth.

When Spring acquires the Handler Adapter, it checks whether the Handler supports caching. If it does, and the resource has not expired, it returns directly:

String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
	long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
	if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
		return;
	}
}

It can be seen that the update time of resources is determined by Handler Adapter. Here we take the implementation of SimpleController Handler Adapter as an example:

public long getLastModified(HttpServletRequest request, Object handler) {
    if (handler instanceof LastModified) {
        return ((LastModified) handler).getLastModified(request);
    }
    return -1L;
}

The meaning is simple: if the Handler (that is, the Controller class) implements the LastModified interface, it calls its getLastModified method to get it, otherwise it returns - 1.

Check NotModified is to verify that the acquired time expires and update Response:

if (validateIfUnmodifiedSince(lastModifiedTimestamp)) {
	if (this.notModified && response != null) {
		response.setStatus(HttpStatus.PRECONDITION_FAILED.value());
	}
	return this.notModified;
}
boolean validated = validateIfNoneMatch(etag);
if (!validated) {
	validateIfModifiedSince(lastModifiedTimestamp);
}
if (response != null) {
	boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());
	if (this.notModified) {
		response.setStatus(isHttpGetOrHead ?
			HttpStatus.NOT_MODIFIED.value() : HttpStatus.PRECONDITION_FAILED.value());
	}
	if (isHttpGetOrHead) {
		if (lastModifiedTimestamp > 0 && parseDateValue(response.getHeader(LAST_MODIFIED)) == -1) {
	    	response.setDateHeader(LAST_MODIFIED, lastModifiedTimestamp);
		}
		if (StringUtils.hasLength(etag) && response.getHeader(ETAG) == null) {
			response.setHeader(ETAG, padEtagIfNecessary(etag));
		}
	}
}
return this.notModified;

validateIfUnmodifiedSince first evaluates the update time of newly acquired resources. If it is less than 0, it returns false directly. Otherwise, If-Modified-Since attribute is parsed and compared.

private boolean validateIfUnmodifiedSince(long lastModifiedTimestamp) {
    if (lastModifiedTimestamp < 0) {
        return false;
    }
    long ifUnmodifiedSince = parseDateHeader(IF_UNMODIFIED_SINCE);
    if (ifUnmodifiedSince == -1) {
        return false;
    }
    this.notModified = (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000));
    return true;
}

If validateIfUnmodifiedSince returns false, further judgment is needed. validateIfNoneMatch must return false because no etag is passed when checkNotModified method is invoked, so it will call validateIfModified Since again to judge that the content is similar to validateIfUnmodifiedSince, except that the last smaller than sign is replaced by a greater than or equal sign.

repsonse can be updated if two judgements confirm that the resource has not been modified.

2.5 Interceptors work - preHandler, postHandler and afterCompletion

After the above series of pretreatments, you can officially start using Handler to process requests. In the Servlet specification, a filter component is designed to process every Web request before and after it. Obviously, the processing granularity is too coarse. Spring MVC adds the concept of interceptor, which can be seen from the initialization of Handler Mapping and Handler lookup.

The interceptor interface Handler Interceptor defines three methods: preHandle, postHandle and afterCompletion, which act on the processors before and after method invocation and after the execution chain of the processors. In the doDispatch method, the corresponding methods of all interceptors on the processor execution chain are invoked through applyPreHandle, applyPostHandle and triggerAfterCompletion.

applyPreHandle/applyPostHandle is to traverse all interceptors and trigger its preHandle/postHandle method. Interestingly, applyPreHandle tries to call the afterCompletion method when preHandle returns false. Reading the Handler Interceptor interface annotations, we find that if the preHandle method returns true, it means that it should be processed by the next interceptor or processor. Otherwise, it can be considered that the response object has been processed by the processing chain, and it does not need to be processed repeatedly. It should be interrupted:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
	HandlerInterceptor[] interceptors = getInterceptors();
	if (!ObjectUtils.isEmpty(interceptors)) {
		for (int i = 0; i < interceptors.length; i++) {
			HandlerInterceptor interceptor = interceptors[i];
			if (!interceptor.preHandle(request, response, this.handler)) {
				triggerAfterCompletion(request, response, null);
				return false;
			}
			this.interceptorIndex = i;
		}
	}
	return true;
}

As for process Dispatch Result, we will not introduce it for the time being.

2.6 Processing requests - handle method, applyDefaultViewName method

When all preHandle methods in the execution chain are triggered, the processor can be invoked to actually process the request. handle method of Handler Adapter is invoked here. Take RequestMapping Handler Adapter as an example. handle method of Handler Adapter calls handleInternal method again.

protected ModelAndView handleInternal(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
	ModelAndView mav;
	checkRequest(request);
	if (this.synchronizeOnSession) {
		HttpSession session = request.getSession(false);
		if (session != null) {
			Object mutex = WebUtils.getSessionMutex(session);
			synchronized (mutex) {
				mav = invokeHandlerMethod(request, response, handlerMethod);
			}
		}
		else {
			mav = invokeHandlerMethod(request, response, handlerMethod);
		}
	}
	else {
		mav = invokeHandlerMethod(request, response, handlerMethod);
	}
	...
}

The function of synchronized OnSession is to have the same Session request executed serially, defaulting false.  

2.6.1 Request Check Request

First, the last check before formal processing is performed, which checks whether the request type is supported and whether the required Session object is included:

protected final void checkRequest(HttpServletRequest request) throws ServletException {
    String method = request.getMethod();
    if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
        throw new HttpRequestMethodNotSupportedException(method, this.supportedMethods);
    }
    if (this.requireSession && request.getSession(false) == null) {
        throw new HttpSessionRequiredException("Pre-existing session required but none found");
    }
}

2.6.2 Trigger Processor Method invokeHandler Method

In the first section of the method, three parameters, request, response and handlerMethod, are wrapped and analyzed, and the components needed to trigger the method are created.

ServletWebRequest webRequest = new ServletWebRequest(request, response);
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
    invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
    invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

GetData BinderFactory does three main things:

1) The method @InitBinder is annotated under the handlerMethod class and the method @InitBinder is annotated under the @ControllerAdvice class.

2) Encapsulate these methods as Invocable Handler Method, set parameter binding parsers and parameter binding factories, and add them to the list

3) Create a Servlet Request Data BinderFactory using the list from the previous step and return it

The main process of the getModelFactory method is similar to that of getDataBidnerFactory, except that the first step is to search for the @ModelAttribute annotation method and the third step is to create a ModelFactory.

createInvocableHandlerMethod encapsulates the incoming HandlerMethod object directly as ServletInvocableHandlerMethod.

Next, the various parsers configured when initializing the Handler Adapter and the binderFactory object just created are bound to the processor method.

The second section of the method mainly creates and configures logical view containers and asynchronous manager objects:

ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

The ModelMap of the processor method is also processed in the middle. In fact, it triggers all the @ModelAttribute methods just found, triggers all the triggers that can be triggered, and adds all the attributes added to the ModelMap.

If asynchronous processing is required, the ServletInvocableHandlerMethod object is further encapsulated as Concurrent ResultHandlerMethod:

if (asyncManager.hasConcurrentResult()) {
    Object result = asyncManager.getConcurrentResult();
    mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
    asyncManager.clearConcurrentResult();
    invocableMethod = invocableMethod.wrapConcurrentResult(result);
}

The third section calls the processor method and takes out the processing result (logical view). If it is asynchronous processing, it returns null first, and then obtains after asynchronous processing:

invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
	return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);

The invokeAndHandle logic is simple: use the Method.invoke method to reflect the call, and then call the return value processor configured when initializing the Handler Adapter for processing. In the middle, it also handles the case of no return value or response error:

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {
    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    setResponseStatus(webRequest);
    if (returnValue == null) {
        if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
            mavContainer.setRequestHandled(true);
            return;
        }
    }
    else if (StringUtils.hasText(getResponseStatusReason())) {
        mavContainer.setRequestHandled(true);
        return;
    }
    mavContainer.setRequestHandled(false);
    try {
        this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
    }
    catch (Exception ex) {
        throw ex;
    }
}

As you can see, if there are no return values or response errors, the logical view container tags are processed and executed smoothly without tags. The reason for this is in the getModelAndView method:

private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
    modelFactory.updateModel(webRequest, mavContainer);
    if (mavContainer.isRequestHandled()) {
        return null;
    }
    ModelMap model = mavContainer.getModel();
    ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
    if (!mavContainer.isViewReference()) {
        mav.setView((View) mavContainer.getView());
    }
    if (model instanceof RedirectAttributes) {
        Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        if (request != null) {
            RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
        }
    }
    return mav;
}

If the container indicates that the request has been processed, the processing result is not obtained. Here we encapsulate ModelMap and ViewName, Status and other attributes as ModelAndView logical view objects and return them. Special processing is also performed for View reference and redirection requests. It is worth mentioning the redirection process, where you get the output FlashMap object configured in Section 1.1 of this article.

After the whole method is invoked, the request Completed method of Servlet WebRequest is executed through the final block. The source code of this method is as follows:

public void requestCompleted() {
    executeRequestDestructionCallbacks();
    updateAccessedSessionAttributes();
    this.requestActive = false;
}

This executes the destructive callback set in the request and updates the properties in the session.

2.6.3 Response Header Processing

After the handle method has been processed, the handleInternational method also processes the response header:

if (!response.containsHeader("Cache-Control")) {
	if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
		applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
	}
	else {
		prepareResponse(response);
	}
}
return mav;

getSessionAttributesHandler returns the SessionAttributesHandler corresponding to the current processor type, if not a new one, but this method has actually been invoked when ModelFactory is created, that is to say, there must be SessionAttributesHandler when the response header is processed, and when the final processing result is obtained in the previous step. The updateModel invoked has saved session attributes. That is to say, unless the session does not contain any attributes, it will definitely enter the applyCacheSeconds branch.

In the applyCacheSeconds method, the two criteria for judging the outermost if statement are not valid by default, that is, they will enter the else branch. The cacheSeconds parameter (that is, the cacheSeconds ForSession Attribute Handlers attribute of RequestMapping Handler Adapter) defaults to 0, and the useCacheControlNoStore defaults to true, so cControl defaults to be an instance with a noStore attribute of true created by the noStore method:

protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds) {
    if (this.useExpiresHeader || !this.useCacheControlHeader) {
        if (cacheSeconds > 0) {
            cacheForSeconds(response, cacheSeconds);
        }
        else if (cacheSeconds == 0) {
            preventCaching(response);
        }
    }
    else {
        CacheControl cControl;
        if (cacheSeconds > 0) {
            cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS);
            if (this.alwaysMustRevalidate) {
                cControl = cControl.mustRevalidate();
            }
        }
        else if (cacheSeconds == 0) {
            cControl = (this.useCacheControlNoStore ? CacheControl.noStore() : CacheControl.noCache());
        }
        else {
            cControl = CacheControl.empty();
        }
        applyCacheControl(response, cControl);
    }
}

applyCacheControl modifies response based on the incoming CacheControl object:

protected final void applyCacheControl(HttpServletResponse response, CacheControl cacheControl) {
    String ccValue = cacheControl.getHeaderValue();
    if (ccValue != null) {
        response.setHeader("Cache-Control", ccValue);
		if (response.containsHeader("Pragma")) {
            response.setHeader("Pragma", "");
        }
        if (response.containsHeader("Expires")) {
            response.setHeader("Expires", "");
        }
    }
}

As for the prepareResponse branch, two methods, applyCacheControl and applyCacheSeconds, are actually called:

protected final void prepareResponse(HttpServletResponse response) {
	if (this.cacheControl != null) {
		applyCacheControl(response, this.cacheControl);
	}
	else {
		applyCacheSeconds(response, this.cacheSeconds);
	}
	if (this.varyByRequestHeaders != null) {
		for (String value : getVaryRequestHeadersToAdd(response, this.varyByRequestHeaders)) {
			response.addHeader("Vary", value);
		}
	}
}

2.6.4 doDispatch asynchronous processing

In 2.6.2, if the request needs to be processed asynchronously, it will not get the processing result, but will return directly, which is also handled in doDispatch:

if (asyncManager.isConcurrentHandlingStarted()) {
    return;
}

However, doDispatch does another processing in the final block:

finally {
	if (asyncManager.isConcurrentHandlingStarted()) {
		if (mappedHandler != null) {
			mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
		}
	}
	else {
		if (multipartRequestParsed) {
			cleanupMultipart(processedRequest);
		}
	}
}

In the applyAfterConcurrent Handling Started method, mappedHandler calls the AsyncHandler Interceptor instance to process requests and responses.

2.6.5 Set the default view name - applyDefaultViewName

The logic here is simple. If the logical view is not a view reference (that is, its view attribute is a String object), a default name is generated with RequestToViewNameTranslator:

private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
    if (mv != null && !mv.hasView()) {
        String defaultViewName = getDefaultViewName(request);
        if (defaultViewName != null) {
            mv.setViewName(defaultViewName);
        }
    }
}

2.7 Process Dispatch Result

After the processor execution chain completes the request processing, the ModelAndView logical view object is generated, but: (1) the logical view is not the page or data we see, and needs to be further rendered; (2) if there is an exception in the processing, it needs to be centralized; and (3) the afterCompletion method has not been triggered yet.

So Spring MVC provides process Dispatch Result to deal with these three issues.

2.7.1 Exception handling

In the doDispatch method, if an exception is caught, the exception is assigned to the dispatchException object, which is passed through the method call:

catch (Exception ex) {
	dispatchException = ex;
}
catch (Throwable err) {
	dispatchException = new NestedServletException("Handler dispatch failed", err);
}

At the beginning of the processDispatchResult method, exceptions are handled:

boolean errorView = false;
if (exception != null) {
	if (exception instanceof ModelAndViewDefiningException) {
		mv = ((ModelAndViewDefiningException) exception).getModelAndView();
	}
	else {
		Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
		mv = processHandlerException(request, response, handler, exception);
		errorView = (mv != null);
	}
}

This process replaces the original view with exception view. The core logic is in the processHandlerException method. In fact, it calls the resolveException method of all registered processor exception parsers to generate exception view and configure the request attributes.

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,@Nullable Object handler, Exception ex) throws Exception {
    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            exMv = resolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                break;
            }
        }
    }
    if (exMv != null) {
        if (exMv.isEmpty()) {
            request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
            return null;
        }
        if (!exMv.hasView()) {
            String defaultViewName = getDefaultViewName(request);
            if (defaultViewName != null) {
                exMv.setViewName(defaultViewName);
            }
        }
        WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
        return exMv;
    }
    throw ex;
}

2.7.2 Page Rendering-render Method

At the beginning of the method, the localization attributes are determined by using the localization parser:

Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);

Then, according to whether the view object is a view reference or a name reference in the logical view, different methods are adopted. First is the processing of name references:

view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
    throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'");
}

The actual view object needs to be resolved by name, essentially calling the resolveViewName method of the appropriate ViewResolver:

protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
    if (this.viewResolvers != null) {
        for (ViewResolver viewResolver : this.viewResolvers) {
            View view = viewResolver.resolveViewName(viewName, locale);
                if (view != null) {
                    return view;
                }
            }
        }
    return null;
}

See resolveViewName's execution process. Spring Source Learning (6): Initialization of Spring MVC The 3.8 quarter.

If the logical View uses View reference, that is, the View object is already included in the logical View, it can be taken out directly.

Next, set the HTTP status code for response and call the render method of View to render the page. Take International ResourceView as an example, its render method is implemented in AbstractView:

public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
    prepareResponse(request, response);
    renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

prepareResponse method is not an empty method, but because if condition is never satisfied, it does not do any processing. The only two methods that actually work are createMergedOutputModel and renderMergedOutputModel.

First, look at the former. This method is also implemented in the AbstractView class, which adds static and dynamic variables to the model:

protected Map<String, Object> createMergedOutputModel(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
    Map<String, Object> pathVars = (this.exposePathVariables ?
        (Map<String, Object>) request.getAttribute(View.class.getName() + ".pathVariables") : null);
    int size = this.staticAttributes.size();
    size += (model != null ? model.size() : 0);
    size += (pathVars != null ? pathVars.size() : 0);
    Map<String, Object> mergedModel = new LinkedHashMap<>(size);
    mergedModel.putAll(this.staticAttributes);
    if (pathVars != null) {
        mergedModel.putAll(pathVars);
    }
    if (model != null) {
        mergedModel.putAll(model);
    }
    if (this.requestContextAttribute != null) {
        mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
    }
    return mergedModel;
}

The exposePathVariables variable defaults to true. As you can see, when adding attributes, the priority of dynamic attributes is higher according to the order of static attributes before dynamic attributes.

Then look at the latter, one of its parameters is the result of getRequestToExpose. Its two if judgment conditions are not satisfied by default, that is, return directly without processing:

protected HttpServletRequest getRequestToExpose(HttpServletRequest originalRequest) {
    if (this.exposeContextBeansAsAttributes || this.exposedContextBeanNames != null) {
        WebApplicationContext wac = getWebApplicationContext();
        Assert.state(wac != null, "No WebApplicationContext");
        return new ContextExposingHttpServletRequest(originalRequest, wac, this.exposedContextBeanNames);
    }
    return originalRequest;
}

The renderMergedOutputModel method is implemented in the InternalResourceView class, which first binds model data to request:

exposeModelAsRequestAttributes(model, request);
exposeHelpers(request);

The second method is an extension point with no default implementation. Next, parse the position of the View:

protected String prepareForRendering(HttpServletRequest request, HttpServletResponse response) throws Exception {
    String path = getUrl();
    Assert.state(path != null, "'url' not set");
    if (this.preventDispatchLoop) {
        String uri = request.getRequestURI();
        if (path.startsWith("/") ? uri.equals(path) : uri.equals(StringUtils.applyRelativePath(uri, path))) {
            throw new ServletException("Circular view path [" + path + "]: would dispatch back " + "to the current handler URL [" + uri + "] again. Check your ViewResolver setup! " + "(Hint: This may be the result of an unspecified view, due to default view name generation.)");
        }
    }
    return path;
}

The exception thrown here must have been encountered by many people. When using SpringBoot, if you do not configure themeleaf and have the following code, you will throw the exception when accessing:

@Controller
public class TestController{
    @RequestMapping("/index")
    public String index(){
        return "index";
    }
}

However, in Spring MVC, because preventDispatch Loop defaults to false, loopback checks are not triggered here.

The next step is to get a Request Dispatcher (which normally corresponds to a JSP file when using the Internal ResourceView) and send the model data just set to the request to the actual page:

// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
    throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
    response.setContentType(getContentType());
    rd.include(request, response);
}
else {
    // Note: The forwarded resource is supposed to determine the content type itself.
    rd.forward(request, response);
}

Request Dispatcher is part of the Servlet specification. For its use and principles, see:

Servlet specification

The Principle of Request Dispatcher

At this point, the page rendering part is completed.

After that, call the afterCompletion method of the processor execution chain to handle asynchronous events or call cleanupMultipart to clean up resources.

Posted by mattcooper on Sat, 11 May 2019 23:14:05 -0700