17 Question Scene Spring Web Thoughts Question Collection

17 Question Scene Spring Web Thoughts Question Collection #

Hello, I’m Fu Jian.

Welcome to the second Q&A session. Congratulations on completing two-thirds of the course. Up until now, we have resolved 38 online questions. Have you been able to apply what you’ve learned in your work? As the saying goes, “Learning is shallow without practical experience”. I hope you can turn the knowledge from “mine” into “yours” through action.

Without further ado, I will now start answering the reflection questions from Chapter 2 one by one. Feel free to add any thoughts you have in the comments section.

Lesson 9 #

There are actually many surprising aspects about URL parsing. Take a look at part of the code in Example 2:

@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestParam("name") String name){
    return name;
};

In the above code snippet, we can test what the result would be using http://localhost:8080/hi2?name=xiaoming&name=hanmeimei. Do you think the result would be xiaoming&name=hanmeimei?

For this test, the result returned is actually “xiaoming,hanmeimei”. We can trace this back to the code for parsing request parameters, referring to org.apache.tomcat.util.http.Parameters#addParameter:

public void addParameter( String key, String value )
        throws IllegalStateException {
    // Omitted non-essential code
    ArrayList<String> values = paramHashValues.get(key);
    if (values == null) {
        values = new ArrayList<>(1);
        paramHashValues.put(key, values);
    }
    values.add(value);
}

As you can see, when accessing in the form of name=xiaoming&name=hanmeimei, the parameter value parsed for name is an ArrayList collection, which contains all the values (in this case, xiaoming and hanmeimei). However, this array ultimately needs to be converted to a String type. The conversion can be viewed in the corresponding converter ArrayToStringConverter, with key code as follows:

public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    return this.helperConverter.convert(Arrays.asList(ObjectUtils.toObjectArray(source)), sourceType, targetType);
}

Here, helperConverter refers to CollectionToStringConverter, which uses “,” as the separator to convert the collection to a String type. The delimiter is defined as follows:

private static final String DELIMITER = ",";

Based on the above analysis, we know that for parameter parsing, the resulting value is actually an array. It is just during the final conversion that it may be converted to different types based on different requirements and present different values, which sometimes surprises us. After analyzing so much, let’s modify the code and test some of the conclusions drawn from the source code analysis we just did. The code modification is as follows:

@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestParam("name") String[] name){
    return Arrays.toString(name);
};

Here we change the receiving type to a String array. After retesting, we will find that the result is [xiaoming, hanmeimei], which is easier to understand and accept.

Lesson 10 #

In Case 3, we mentioned in the example of Content-Type that customizing common headers arbitrarily in the Controller layer may sometimes not work. Is this conclusion universally applicable? That is, will the same problem also exist when using other built-in containers or other development frameworks?

Actually, the answer is no. Let’s modify the pom.xml in Case 3. The goal of the modification is to make it not use the default embedded Tomcat container, but the Jetty container instead. The specific modification is as follows:

<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 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

After making the above modification, let’s run the test program again, and we will find that the Content-Type can indeed be set as we want, as shown below:

We are still executing addHeader(), but because the container has been switched, the method being called is actually the one in Jetty, specifically org.eclipse.jetty.server.Response#addHeader:

public void addHeader(String name, String value)
{
    //  Omitted other non-critical codes
    if (HttpHeader.CONTENT_TYPE.is(name))
    {
        setContentType(value);
        return;
    }
    // Omitted other non-critical codes
    _fields.add(name, value);
}

In the above code, setContentType() ultimately adds the Header. This is completely different from Tomcat. Please refer to its implementation:

public void setContentType(String contentType)
{
    // Omitted other non-critical codes
    if (HttpGenerator.__STRICT || _mimeType == null)
        // Add CONTENT_TYPE
        _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
    else
    {
        _contentType = _mimeType.asString();
        _fields.put(_mimeType.getContentTypeField());
    }
}

Comparing with the code snippet given in Case 3, here is a key part (please refer to AbstractMessageConverterMethodProcessor#writeWithMessageConverters):

MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {    
    selectedMediaType = contentType;
} else {
    // Negotiate MediaType based on the request Accept header and the specified return type (RequestMapping#produces).
}
// Omitted other codes: else

From the above code, we can see that the selection of MediaType no longer needs negotiation, because in the Jetty container, the contentType is added to the Header, so it can be directly used. However, the Tomcat container we introduced earlier did not add the contentType to the Header, so it cannot enter the branch where isContentTypePreset is true in the above code. At this point, it can only negotiate which MediaType to use based on the request Accept header and the annotation-specified return type.

To trace the root cause, the main difference lies in the different implementations of addHeader() in different containers. Let’s further explore this. First, let’s review the method definition in our Case 3 code:

import javax.servlet.http.HttpServletResponse;
public String hi3(HttpServletResponse httpServletResponse)

Although they are both interfaces HttpServletResponse, in the Jetty container, it will be instantiated as org.eclipse.jetty.server.Response, while in the Tomcat container, it will be instantiated as org.apache.catalina.connector.Response. This is why the method calls are different.

How to understand this phenomenon? The container is the communication layer, and Spring Boot is just an intermediary among them. Therefore, in Spring Boot, the HTTP Servlet Response originates from the objects provided by the original communication layer, which is also reasonable.

Through this thought exercise, we can see that for many technological uses, some conclusions are not unchangeable. Just by switching the container, the conclusion may become invalid. Therefore, only by understanding the principles can we fundamentally avoid various troubles, instead of relying solely on conclusions to “reinvent the wheel”.

Lesson 11 #

From the study of Case 1, we know that when we directly use Spring MVC instead of Spring Boot, we need to manually add JSON dependencies in order to parse JSON requests or encode JSON responses. So why don’t we need to do this when using Spring Boot?

In fact, when we use Spring Boot, we always add the relevant dependencies:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

And this dependency indirectly includes Jackson. The dependency relationship is shown in the following diagram:

The subsequent addition of Jackson encoders and decoders is similar to the key logic in ordinary Spring MVC: it checks for the existence of relevant classes. However, we can summarize that there are two styles for checking the existence of relevant classes:

  1. Directly use reflection for checking. For example, the key statement mentioned earlier:

    ClassUtils.isPresent(“com.fasterxml.jackson.databind.ObjectMapper”, null)

  2. Use @ConditionalOnClass for checking, as shown in the implementation of JacksonHttpMessageConvertersConfiguration:

package org.springframework.boot.autoconfigure.http;

@Configuration(proxyBeanMethods = false)
class JacksonHttpMessageConvertersConfiguration {
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(ObjectMapper.class)
    @ConditionalOnBean(ObjectMapper.class)
    @ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
        havingValue = "jackson", matchIfMissing = true)
    static class MappingJackson2HttpMessageConverterConfiguration {
        @Bean
        @ConditionalOnMissingBean(value = MappingJackson2HttpMessageConverter.class
        //Omitted non-essential code
        MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
            return new MappingJackson2HttpMessageConverter(objectMapper);
        }
    }
}

The above is the two methods for checking the existence of a class.

Lesson 12 #

In the student management system mentioned above, we have an interface that is responsible for deleting a student’s information based on their student ID. The code is as follows:

@RequestMapping(path = "students/{id}", method = RequestMethod.DELETE)
public void deleteStudent(@PathVariable("id") @Range(min = 1,max = 10000) String id){
    log.info("delete student: {}",id);
    // omitted business code
};

In this case, the student’s ID is obtained from the path in the request, and it is constrained to a range of 1 to 10000. So, can you figure out which resolver is responsible for parsing the ID and how the validation is triggered?

Following the analysis approach from Case 1, we can easily find that the resolver responsible for parsing the ID value is PathVariableMethodArgumentResolver. The matching requirements for this resolver are as follows:

@Override
public boolean supportsParameter(MethodParameter parameter) {
   if (!parameter.hasParameterAnnotation(PathVariable.class)) {
      return false;
   }
   if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
       PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
       return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
    }
   // To return true, the method parameter must be annotated with @PathVariable
   return true;
}

Looking at the above code, when the method parameter ID is of type String and is annotated with @PathVariable, it satisfies the matching conditions of PathVariableMethodArgumentResolver.

While examining the implementation of this resolver, we can quickly find the specific parsing method. However, when we try to trace the validation process, it seems to be nowhere in sight, which is completely different from the resolver RequestResponseBodyMethodProcessor in Case 1. So, how is the validation triggered for this case? You can consider this question as an exercise to think about later. Here’s a hint: in reality, validations directly marked on method parameters are triggered through AOP interception.

Lesson 13 #

In Case 2, we mentioned that it is necessary to avoid calling FilterChain#doFilter() multiple times in a filter. So, what would happen if a filter fails to call this method even once due to negligence?

Let’s take the modified DemoFilter as an example:

@Component
public class DemoFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("do some logic");
    }
}

For such a situation, if we don’t understand the implementation logic of the filter, we might think that it will eventually execute the business logic in the Controller layer, at most ignoring some filters that are sorted after this filter. However, in reality, the consequences are much more severe.

In our modified case, when we send an HTTP request to add a user, the response is returned as successful:

POST http://localhost:8080/regStudent/fujian- #

HTTP/1.1 200- Content-Length: 0- Date: Tue, 13 Apr 2021 11:37:43 GMT- Keep-Alive: timeout=60- Connection: keep-alive

But in reality, our Controller layer is not executed at all. Let me explain the reason to you by pasting the critical code for filter execution that we previously analyzed (ApplicationFilterChain#internalDoFilter):

private void internalDoFilter(ServletRequest request, ServletResponse response){
    if (pos < n) {
        // pos increments
        ApplicationFilterConfig filterConfig = filters[pos++];
        try {
            Filter filter = filterConfig.getFilter();
            // omit non-critical code
            // execute filter
            filter.doFilter(request, response, this);
            // omit non-critical code
        } 
        // omit non-critical code
        return;
    }
        // execute actual business logic
        servlet.service(request, response);
    } 
    // omit non-critical code
}

When our filter DemoFilter is executed and it does not call FilterChain#doFilter internally, we will reach the return statement in the above code. This not only prevents the execution of subsequent filters but also prevents the execution of the business logic servlet.service(request, response). Under such circumstances, it is not surprising that our Controller layer logic is not executed.

On the contrary, it is because each filter explicitly calls FilterChain#doFilter that the last filter has the opportunity to see the situation where pos = n when calling FilterChain#doFilter. In this situation, the return statement cannot be executed, but the business logic (servlet.service(request, response)) can be executed.

Lesson 14 #

The two cases in this lesson both occur during the startup of the Tomcat container. Do you know how Spring integrates with Tomcat to register these filters during startup?

When we start Spring by calling the following key code line:

SpringApplication.run(Application.class, args);

A specific ApplicationContext implementation is created. Taking ServletWebServerApplicationContext as an example, it calls onRefresh() to integrate with containers such as Tomcat or Jetty:

@Override
protected void onRefresh() {
   super.onRefresh();
   try {
      createWebServer();
   }
   catch (Throwable ex) {
      throw new ApplicationContextException("Unable to start web server", ex);
   }
}

Let’s take a look at the implementation of createWebServer() in the above code:

private void createWebServer() {
   WebServer webServer = this.webServer;
   ServletContext servletContext = getServletContext();
   if (webServer == null && servletContext == null) {
      ServletWebServerFactory factory = getWebServerFactory();
      this.webServer = factory.getWebServer(getSelfInitializer());
   }
   // Omitted non-key code
}

On line 6, calling factory.getWebServer() will start Tomcat. This method passes the getSelfInitializer() parameter, which returns a special format callback method this::selfInitialize to add filters, etc. It is called only after Tomcat is started.

private void selfInitialize(ServletContext servletContext) throws ServletException {
   prepareWebApplicationContext(servletContext);
   registerApplicationScope(servletContext);
   WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
   for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
      beans.onStartup(servletContext);
   }
}

After saying so much, you may not be clear about this process yet. Here I have additionally posted two call stacks to help you understand.

  1. Start Tomcat when starting Spring Boot:

  1. Tomcat calls selfInitialize after starting:

I believe that through the above call stacks, you can have a clearer understanding of the timing of Tomcat startup and filter addition.

Lesson 15 #

From the study of Case 1, we know that when Spring Boot enables Spring Security, accessing an API that requires authorization will automatically redirect to the login page shown below. Do you know how this page is generated?

In fact, after Spring Security is enabled in Spring Boot, when we anonymously access an API that requires authorization, we will find that the authorization for this API fails, resulting in a 302 redirect. The key code for the redirect can be seen in the “commence” method of LoginUrlAuthenticationEntryPoint invoked by ExceptionTranslationFilter:

public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
   // omitted non-critical code
   redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
   // omitted non-critical code
   redirectStrategy.sendRedirect(request, response, redirectUrl);
}

The specific redirect can be observed using Chrome Developer Tools:

After the redirect, the HTML page seen in the new request is generated by the following code, referring to the “generateLoginPageHtml” method in DefaultLoginPageGeneratingFilter:

private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
   String errorMsg = "Invalid credentials";
   // omitted non-critical code
 
   StringBuilder sb = new StringBuilder();
   sb.append("<!DOCTYPE html>\n"
         + "<html lang=\"en\">\n"
         + "  <head>\n"
         + "    <meta charset=\"utf-8\">\n"
         + "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n"
         + "    <meta name=\"description\" content=\"\">\n"
         + "    <meta name=\"author\" content=\"\">\n"
         + "    <title>Please sign in</title>\n"
         + "    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"
         + "    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
         + "  </head>\n"
         + "  <body>\n"
         + "     <div class=\"container\">\n");
   // omitted non-critical code
   sb.append("</div>\n");
   sb.append("</body></html>");

   return sb.toString();
}

The above code shows the rendering process of the login page, which can be seen that it is mainly completed by various filters.

Lesson 16 #

In this lesson, there are two cases. When sending the first request, corresponding resource processors and exception processors will be traversed and registered into the class member variables of DispatcherServlet. Do you know how it is triggered?

The onRefresh() interface, which implements FrameworkServlet, is called back when WebApplicationContext is initialized:

public class DispatcherServlet extends FrameworkServlet {
    @Override
    protected void onRefresh(ApplicationContext context) {
       initStrategies(context);
    }
    
    /**
     * Initialize the strategy objects that this servlet uses.
     * <p>May be overridden in subclasses in order to initialize further strategy objects.
     */
    protected void initStrategies(ApplicationContext context) {
       initMultipartResolver(context);
       initLocaleResolver(context);
       initThemeResolver(context);
       initHandlerMappings(context);
       initHandlerAdapters(context);
       initHandlerExceptionResolvers(context);
       initRequestToViewNameTranslator(context);
       initViewResolvers(context);
       initFlashMapManager(context);
    }
}

Above is the complete content of this Q&A session. See you in the next chapter!