Introduction 5 Minutes Easy Understanding of an HTTP Request Handling Process

Introduction 5 Minutes Easy Understanding of an HTTP Request Handling Process #

Hello, I’m Fu Jian.

In the previous chapter, we learned about common mistakes in using Spring, such as automatic injection and AOP. However, most of the time we use Spring to develop web applications. So starting from this lesson, we will learn about common mistakes in Spring Web.

Before that, I think it’s necessary to introduce you to the core process of Spring Web, so that our subsequent learning can proceed more smoothly.

So what is the core process of Spring Web? It is nothing more than the processing of an HTTP request. Here, I will use the usage of Spring Boot as an example to give you a simple overview.

First, let’s review how we can add an HTTP interface. Here is an example:

@RestController
public class HelloWorldController {
    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
         return "helloworld";
    };
}

This is a program that we are very familiar with, but for many programmers, they don’t know why it works. After all, it can work without knowing the principles behind it.

However, if you are a rigorous and ambitious person, you are probably curious to understand it. And believe me, this question may also be asked in a job interview. Let’s take a look at the story behind it together.

If you look closely at this code, you will find some key “elements”:

  1. The request path: hi
  2. The request method: GET
  3. The corresponding method execution: hi()

So, if you were asked to implement the handling of an HTTP request on your own, you might write pseudo code like this:

public class HttpRequestHandler{
    
    Map<RequestKey, Method> mapper = new HashMap<>();
    
    public Object handle(HttpRequest httpRequest){
         RequestKey requestKey = getRequestKey(httpRequest);         
         Method method = this.mapper.getValue(requestKey);
         Object[] args = resolveArgsAccordingToMethod(httpRequest, method);
         return method.invoke(controllerObject, args);
    };
}

So now, what components do you need to complete the mapping and execution of a request?

  1. You need a place (e.g. a Map) to maintain the mapping from the HTTP path/method to the specific execution method.
  2. When a request comes, you need to obtain the corresponding method to be executed based on the key information of the request.
  3. You need to parse the method definition, resolve the parameter values to be used in the method call, and then invoke the method using reflection to retrieve the return result.

In addition, you also need something to parse your HTTP request using the underlying communication layer. Only by parsing the request can you know the path/method information and proceed with the execution. Otherwise, it would be like “a clever woman without rice to cook with”.

So, based on the overall picture, you need these processes to complete the parsing and handling of a request. Next, let’s take a look at how Spring Boot implements it in the processing order, and what the corresponding key implementations look like.

First, let’s parse the HTTP request. For Spring, it doesn’t provide communication layer support itself. It relies on containers such as Tomcat and Jetty to provide communication layer support. For example, when we use Spring Boot, we indirectly rely on Tomcat. The dependency diagram is as follows:

Dependency Diagram

Moreover, it is this freely combinable relationship that allows us to directly switch containers without affecting the functionality. For example, we can switch from the default Tomcat to Jetty by using the following configuration:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- Use Jetty instead -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

After depending on Tomcat, when Spring Boot starts, it will start Tomcat to prepare for accepting connections.

How Tomcat is started can be roughly understood by the following call stack:

Call Stack

To put it simply, calling the following code line will start Tomcat:

SpringApplication.run(Application.class, args);

So why use Tomcat? You can understand it by looking at the following class:

//org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration

class ServletWebServerFactoryConfiguration {

   @Configuration(proxyBeanMethods = false)
   @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
   @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
   public static class EmbeddedTomcat {
      @Bean
      public TomcatServletWebServerFactory tomcatServletWebServerFactory(
         // Omit non-essential code
         return factory;
      }

   }

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedJetty {
   @Bean
   public JettyServletWebServerFactory JettyServletWebServerFactory(
         ObjectProvider<JettyServerCustomizer> serverCustomizers) {
       // Omit non-essential code
      return factory;
   }
}

// Omit other container configurations
}

In the previous section, we mentioned that we depend on the JAR of the embedded container Tomcat, so the following condition will be true and Tomcat will be used:

@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })

With Tomcat, when an HTTP request is made, it triggers the underlying NIO communication provided by Tomcat to receive the data. We can see this from the following code (org.apache.tomcat.util.net.NioEndpoint.Poller#run):

@Override
public void run() {
    while (true) {
         //omitted other non-critical code
         //poll registered interested events
         if (wakeupCounter.getAndSet(-1) > 0) {
               keyCount = selector.selectNow();
         } else {
               keyCount = selector.select(selectorTimeout);
 
        //omitted other non-critical code
        Iterator<SelectionKey> iterator =
            keyCount > 0 ? selector.selectedKeys().iterator() : null;

        while (iterator != null && iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            NioSocketWrapper socketWrapper = (NioSocketWrapper)  
            //handle events
            processKey(sk, socketWrapper);
            //omitted other non-critical code
           
        }
       //omitted other non-critical code
    }
}

The above code listens for and handles request events, and eventually sends the request event to a thread pool for processing. The call stack for receiving a request event is as follows:

Image

The call stack for the thread pool handling the request is as follows:

Image

In the above calls, it eventually enters the core of Spring Boot’s processing - the DispatcherServlet (the call stack above does not show the complete call, so it is not displayed). It can be said that DispatcherServlet is the central dispatch entry program used to handle HTTP requests, mapping each web request to a request processing body (API controller/method).

Let’s take a look at its core. It is essentially a servlet, so it is triggered by the following core servlet method:

javax.servlet.http.HttpServlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)

Finally, it executes the following doService() method, which dispatches and handles the request:

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
      doDispatch(request, response);
}

Let’s take a look at how it dispatches and executes:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
   
 //omitted other non-critical code
 // 1. Dispatch: Determine the handler for the current request.
  HandlerExecutionChain mappedHandler = getHandler(processedRequest);
 
 //omitted other non-critical code
 // Determine the handler adapter for the current request.
  HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
 
 //omitted other non-critical code
 // 2. Execution: Actually invoke the handler.
  mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  
 //omitted other non-critical code
     
}

In the above code, it is clear that there are two key steps:

1. Dispatch: Find the corresponding execution method based on the request

Finding the method refers to DispatcherServlet#getHandler, and the specific lookup is much more complicated than the Map lookup given in the beginning, but it is still a process of finding candidate execution methods based on the request. Here, we can get a sense of this correspondence through a debug view:

Image

The key mapping here is actually the RequestMappingHandlerMapping in the debug view above.

2. Execution: Execute the found execution method through reflection

We can verify this conclusion by referring to the following debug view, which refers to the code org.springframework.web.method.support.InvocableHandlerMethod#doInvoke:

Image

Finally, we use reflection to invoke the execution method.

Based on the above overview, you should have a basic understanding of how an HTTP request is processed. However, you may have a question: How are the mappings for handlers constructed?

To put it simply, the key is the construction process of the RequestMappingHandlerMapping bean.

After it is constructed, the afterPropertiesSet method is called to do some additional things. Here, let’s take a look at its call stack:

Image

The key operation is the AbstractHandlerMethodMapping#processCandidateBean method:

protected void processCandidateBean(String beanName) {
   //omitted non-critical code
   if (beanType != null && isHandler(beanType)) {
      detectHandlerMethods(beanName);
   }
}

The implementation of isHandler(beanType) refers to the following important code:

@Override
protected boolean isHandler(Class<?> beanType) {
   return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
         AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

Here, you will find that the key condition is whether the appropriate annotations (Controller or RequestMapping) are marked. Only when they are marked, can they be added to the mapping information. In other words, when Spring builds RequestMappingHandlerMapping, it processes all the annotations marked Controller and RequestMapping, and then parses them to build a mapping relationship between requests and processing.

The above is the core process of how Spring Boot handles an HTTP request. Essentially, it binds an embedded container (Tomcat/Jetty/others) to receive the request, then finds a suitable method for the request, and finally executes it through reflection. Of course, there are countless details in between, but they are not important. Understanding this core idea will be very beneficial for you to understand various types of error cases in Spring Web in the future!