The previous article analyzed the startup process of the Servlet container in Tomcat. This article documents the flow of a request in SpringMVC.
An overview of
When a request is sent from the client to the Servlet to be processed by the program logic, it goes through the following process:
- The user makes a request from a client, either a browser or other client software.
- The request arrives at the Web server, and the HTTP server in Tomcat receives the request first.
- The connector in the HTTP server in Tomcat will first process the request.
- The connector consists of three parts: the Endpoint, Processor, and Adapter in terms of the order in which requests are executed. A connector can be thought of as processing data at the transport layer.
- The Endpoint consists of two parts, one is Accpetor, which is responsible for receiving Socket connections. The other part is the SocketProcessor. After Tomcat receives a Socket request, it needs to create a new Socket connection to follow up the subsequent actions of the request. (This step is the same as Socket programming) and sends the newly created Socket to the Processor. There is a thread pool between an Acceptor and a SocketProcessor. Upon receiving a Socket connection, an Acceptor generates a SocketProcessor task and sends it to the thread pool for processing.
- After receiving the Socket connection, the Processor processes the data carried in the Socket into a Request defined in Tomcat.
- When the Adapter receives a Request defined by Tomcat, it encapsulates the Request into a ServletRequest, which is equivalent to an adaptation of the original Request.
- After the Servlet request reaches the Servlet container, the Servlet container is layered, from the outer layer to the inner layer, Engine -> Host -> Context -> Wrapper(Servlet), each layer is related to each other in the pattern of responsibility chain. Each layer can handle requests (by configuring values). The last Wrapper calls the Servlet’s service method and the final request goes into the DispatcherServlet to be processed.
The following through the source code to analyze the entire process of a request.
Pipeline – Value processing
As mentioned earlier, Adapter ADAPTS a received Request defined in Tomcat into an HttpServletRquest. This is done in the CoyoteAdapter#service(Request, Response) method.
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
// ...
try {
// Parse and set Catalina and configuration specific
// request parameters
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
// Call the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
// ...}}Copy the code
After processing the request to type HttpServletRquest, the chain of responsibility invocation logic is entered. We first get the outermost container, Engine, of type StandardEngine. And get the Pipeline held by StandardEngine, the specific type is StandardPipeline. The first Value in the current StandardPipeline is then called. Value represents a processing point of the current Container(Container is an interface that implements the Engine, Host, Context, and Wrapper interfaces, and therefore can be called Container). The current request can be processed. Once the Value is retrieved, its invoke method is called to process the request.
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
// REQUEST without a host when no default host
// is defined. This is handled by the CoyoteAdapter.
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
}
Copy the code
As you can see, the StandardEngineValue#invoke method does nothing but pass the call along. After passing, the first Value of the Host Pipeline is also fetched. Note here that the first Value in the Host Pipeline is not StandardHostValue, but the ErrorReportValve. Here is the invoke method of the ErrorReportValve.
public void invoke(Request request, Response response) throws IOException, ServletException {
// Perform the request
getNext().invoke(request, response);
// ...
}
Copy the code
First, you can see that the invoke method comes in and immediately calls getNext().invoke(), which invokes the real StandardHostValue invoke method. So what does the ErrorReportValve do?
In fact, the ErrorReportValve is used when the request has an error, such as 404, the familiar 404 page will appear on the page. ErrorReportValve handle error pages. In the ErrorReportValue#report method, the error page is output.
Back to the StandardHostValue processing.
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Context to be used for this Request
Context context = request.getContext();
// ...
if(! response.isErrorReportRequired()) { context.getPipeline().getFirst().invoke(request, response); }// ...
}
Copy the code
StandardHostValue’s invoke method also directly calls the first Value in the Context’s Pipeline for processing. Note here that the first Value in the Context Pipeline is not StandardContextValue, but AuthenticatorBase. So the method called is the AuthenticatorBase#invoke method. AuthenticatorBase is a Value for security authentication. The user needs to pass the security authentication before continuing the process. The StandardContextValue#invoke method is invoked after the security authentication has passed.
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Disallow any direct access to resources under WEB-INF or META-INF
// No direct access to/meta-INF // meta-INF/web-INF // web-INF is allowed
MessageBytes requestPathMB = request.getRequestPathMB();
if ((requestPathMB.startsWithIgnoreCase("/META-INF/".0))
|| (requestPathMB.equalsIgnoreCase("/META-INF"))
|| (requestPathMB.startsWithIgnoreCase("/WEB-INF/".0))
|| (requestPathMB.equalsIgnoreCase("/WEB-INF"))) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// Select the Wrapper to be used for this Request
/ / get the Wrapper
Wrapper wrapper = request.getWrapper();
if (wrapper == null || wrapper.isUnavailable()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// Acknowledge the request
try {
response.sendAcknowledgement();
} catch (IOException ioe) {
container.getLogger().error(sm.getString(
"standardContextValve.acknowledgeException"), ioe);
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ioe);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported());
}
wrapper.getPipeline().getFirst().invoke(request, response);
}
Copy the code
One thing to note here is that during StandardContextValue processing, an ACK of the current request is returned. We then proceed to call the pipeline-value of the Wrapper, invoking the StandardWrapperValue#invoke method.
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// ...
// First get the Container currently held by StandardWrapperValue
StandardWrapper wrapper = (StandardWrapper) getContainer();
Servlet servlet = null;
Context context = (Context) wrapper.getParent();
// ...
// Allocate a servlet instance to process this request
if(! unavailable) { servlet = wrapper.allocate(); }// ...
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
Container container = this.container;
// ...
filterChain.doFilter(request.getRequest(), response.getResponse());
// ...
} finally {
// Release the filter chain (if any) for this request
if(filterChain ! =null) {
filterChain.release();
}
// Deallocate the allocated servlet instance
try {
if(servlet ! =null) { wrapper.deallocate(servlet); }}catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.deallocateException",
wrapper.getName()), e);
if (throwable == null) { throwable = e; exception(request, response, e); }}// If this servlet has been marked permanently unavailable,
// unload it and release this instance
try {
if((servlet ! =null) && (wrapper.getAvailable() == Long.MAX_VALUE)) { wrapper.unload(); }}catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.unloadException",
wrapper.getName()), e);
if (throwable == null) { exception(request, response, e); }}long t2=System.currentTimeMillis();
long time=t2-t1;
processingTime += time;
if( time > maxTime) maxTime=time;
if( time < minTime) minTime=time; }}Copy the code
The StandardWrapperValue#invoke method is the most verbose of all the invoke methods mentioned above.
- We first get the Container currently held in StandardWrapperValue, which is Wrapper, of type StandardWrapper.
- Call servlet = wrapper.allocate() to allocate a servlet for the current request. The Standard#allocate method can be looked at later.
- Creates a filterChain for the current request. By default, four filters are created, characterEncodingFiler, formContentFilter, requestContextFilter, and Tomcat WebSocket (JSR356) Filter. All requests are filtered.
- FilterChain Executes the doFilter method. The ApplicationFilterChain#internalDoFilter method is normally called. Run all filters on the Chain to process the request.
- After the last filter is executed, the service method (HttpServlet#service(ServletRequest, ServletResponse)) of the servlet assigned above is called.
So much for the StandardWrapperValue#invoke method. Let’s look at the StandardWrapper#allocate method mentioned in the middle, which allocates a Servlet to the current request.
public Servlet allocate(a) throws ServletException {
// ...
boolean newInstance = false;
// If not SingleThreadedModel, return the same instance every time
if(! singleThreadModel) {// Load and initialize our instance if necessary
if (instance == null| |! instanceInitialized) {synchronized (this) {
if (instance == null) {
try {
if (log.isDebugEnabled()) {
log.debug("Allocating non-STM instance");
}
// Note: We don't know if the Servlet implements
// SingleThreadModel until we have loaded it.
/ / load the Servlet
instance = loadServlet();
newInstance = true;
if(! singleThreadModel) {// For non-STM, increment here to prevent a race
// condition with unload. Bug 43683, test case
/ / # 3countAllocated.incrementAndGet(); }}catch (ServletException e) {
throw e;
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("standardWrapper.allocate"), e); }}if(! instanceInitialized) { initServlet(instance); }}}if (singleThreadModel) {
if (newInstance) {
// Have to do this outside of the sync above to prevent a
// possible deadlock
synchronized(instancePool) { instancePool.push(instance); nInstances++; }}}else {
if (log.isTraceEnabled()) {
log.trace(" Returning non-STM instance");
}
// For new instances, count will have been incremented at the
// time of creation
if(! newInstance) { countAllocated.incrementAndGet(); }returninstance; }}synchronized (instancePool) {
while (countAllocated.get() >= nInstances) {
// Allocate a new instance if possible, or else wait
if (nInstances < maxInstances) {
try {
instancePool.push(loadServlet());
nInstances++;
} catch (ServletException e) {
throw e;
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("standardWrapper.allocate"), e); }}else {
try {
instancePool.wait();
} catch (InterruptedException e) {
// Ignore}}}if (log.isTraceEnabled()) {
log.trace(" Returning allocated STM instance");
}
countAllocated.incrementAndGet();
returninstancePool.pop(); }}Copy the code
- In the StandardWrapper#allocate method, we first determine whether the current Servlet implements the SingleThreadModel interface, which is generally not implemented. And the SingleThreadModel interface is indicated to be deprecated. So instead of creating new servlets, you will use existing servlets.
- It then determines whether instance has been initialized, that is, whether the DispatcherServlet has been loaded. This is determined by the load_on_startup parameter in the configuration. If load_on_startup is negative, then no load Servlet is loaded in the Servlet container and it will be loaded at this time.
Pipeline – Value summary
The above process is all of the pipeline-value processing, now it is time to enter the Spring Servlet processing.
To summarize the entire pipeline-value processing process: StandardEngineValue -> ErrorReportValve -> StandardHostValue -> AuthenticatorBase -> StandardContextValue -> StandardWrapperValue Where StandardWrapperValue processes the most transactions. This includes assigning a servlet to the current request, creating a filterChain for the current request, executing filter, and finally calling the servlet’s service method to hand the request to the servlet for processing.
SpringMVC handling of requests
After the ApplicationFilterChain filter is executed, the Servlet’s service method is called and the Servlet handles the request.
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException(lStrings.getString("http.non_http"));
}
service(request, response);
}
Copy the code
In the HttpServlet#service(ServletRequest, ServletResponse) method, convert the ServletRequest to HttpServletRequest, And call FrameworkServlet#service(HttpServletRequest, HttpServletResponse).
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
processRequest(request, response);
}
else {
super.service(request, response); }}Copy the code
It then calls the HttpService#service(HttpServletRequest, HttpServletResponse) method, which calls the corresponding doXXX method for the different request methods, These methods eventually call back to the FrameworkServlet#processRequest(HttpServletRequest, HttpServletResponse) method. The FrameworkServlet#processRequest(HttpServletRequest, HttpServletResponse) method calls the DispatcherServlet#doService method.
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = newHashMap<>(); 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)); }}}// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
if (this.flashMapManager ! =null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if(inputFlashMap ! =null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
try {
doDispatch(request, response);
}
/ /...
}
Copy the code
Do the following things in doService:
- Set properties, including context, localeResolver, and themeResolver.
- Use the FlashMapManager to obtain the flashMap. If the flashMap is not empty, set the INPUT_FLASH_MAP_ATTRIBUTE attribute of the request. And set the OUTPUT_FLASH_MAP_ATTRIBUTE and FLASH_MAP_MANAGER_ATTRIBUTE attributes.
- Call the DispatcherServlet#doDispatcher method
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try{ processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest ! = request);// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
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; }}if(! mappedHandler.applyPreHandle(processedRequest, response)) {return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if(mappedHandler ! =null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); }}else {
// Clean up any resources used by a multipart request.
if(multipartRequestParsed) { cleanupMultipart(processedRequest); }}}}Copy the code
The doDispatcher method does the following things:
- Check whether request is a file upload operation.
- Get the current request handler using the getHandler method. This process is matched by HandlerMapping. AbstractHandlerMapping#getHandlerInternal will eventually find a matching HandlerMethod.
- And that doesn’t stop until we find the HandlerMethod, and we create a HandlerExecutionChain based on that HandlerMethod. The HandlerExecutionChain is typically a list of handlers and interceptors.
- HandlerExecutionChain puts two interceptors by default, ConversionServiceExposingInterceptor and ResourceUrlProviderExposingInterceptor. The HandlerExecutionChain has been created.
- Find the matching HandlerAdapter according to the handler in the HandlerExecutionChain. All handlerAdapters held by the current DispatcherServlet are also traversed. Use the HandlerAdapter#support method.
- Execute the HandlerExecutionChain#applyPreHandle method to perform the interceptor logic before the handle. Execute the preHandle method by iterating through all the Interceptors in the HandlerExecutionChain. ApplyPreHandle If false is returned, the request is not processed.
- Call the HandlerAdapter#handler method to process the request. The method then processes the request. The exact logic is not laid out in this article and will be examined later when the components are parsed individually.
- Call the DispatcherServlet#applyDefaultViewName method to determine if the view name needs to be converted. If need to transform, is to use the nine components of RequestToViewNameTranslator for conversion.
- Call the postHandle of the Interceptor in the HandlerExecutionChain for post-processing.
- Call the DispatcherServlet#processDispatchResult method to process the result. If an exception occurs during request processing, it is handled here. Exceptions are handled by the DispatcherServlet#processHandlerException method, which uses HandlerExceptionResolver of the nine components.
- If there is no exception in the above call, follow normal logic and call DispatcherServlet#render to render. LocaleResolver is used to set the locale of response during rendering. The View will be resolved by the ViewResolver based on the viewName in ModelAndView. Then call the View#render method to render.
- After the view is rendered, the afterCompletion method of the HandlerExecutionChain interceptor is triggered to execute the logic after processing.
As you can see, the DispatcherServlet#doService method and the execution of the DispatcherServlet#doDispatcher method use all nine components. At this point, a request is processed in SpringMVC. The nine components in SpringMVC will be analyzed later.