23 Question Scene Spring Supplement Thoughts Question Collection

23 Question Scene Spring Supplement Thoughts Question Collection #

Hello, I’m Fu Jian.

Welcome to the third Q&A session. Congratulations, you’re almost at the finish line. Up to now, we have resolved 50 online questions. Do you feel accomplished? However, in order to truly make use of what you’ve learned, you still need to practice diligently. It’s like there’s always a gap between theory and practice that we need to bridge. So without further ado, let’s start answering the chapter 3 discussion questions one by one. Feel free to add any thoughts or ideas in the comments.

Lesson 18 #

In Lesson 1, when we used Spring Data Redis, we mentioned StringRedisTemplate and RedisTemplate. So how are they created?

In fact, when we depend on spring-boot-starter, we indirectly depend on spring-boot-autoconfigure.

In this JAR file, there is a class called RedisAutoConfiguration.

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

   @Bean
   @ConditionalOnMissingBean(name = "redisTemplate")
   @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
   public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
      RedisTemplate<Object, Object> template = new RedisTemplate<>();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }

   @Bean
   @ConditionalOnMissingBean
   @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
   public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
      StringRedisTemplate template = new StringRedisTemplate();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }

}

From the above code, we can see that when there is a RedisOperations class, StringRedisTemplate and RedisTemplate beans will be created. By the way, this RedisOperations is in the Spring Data Redis JAR file.

Going back to the beginning, how is RedisAutoConfiguration discovered? Actually, it is configured in META-INF/spring.factories in spring-boot-autoconfigure as shown below:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\

So how is it loaded? Our application startup program is marked with @SpringBootApplication, and this annotation inherits the following annotation:

// Omitting non-key code
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
   // Omitting non-key code
}

When it uses the AutoConfigurationImportSelector class, it will import the RedisAutoConfiguration defined in META-INF/spring.factories. So when does the import action occur? Actually, it is triggered when the application starts, and the call stack information is as follows:

Combining the stack information above with the relevant source code, we can summarize the process of creating RedisTemplate.

When Spring starts, it tries to process all classes marked with @Configuration using ConfigurationClassPostProcessor. The processing of each configuration class is completed by ConfigurationClassParser.

In this process, it uses ConfigurationClassParser.DeferredImportSelectorHandler to handle the import. AutoConfigurationImportSelector is one of the imports, which is indirectly referenced by the @EnableAutoConfiguration annotation. It will load RedisAutoConfiguration defined in “META-INF/spring.factories”, and then we will discover the StringRedisTemplate and RedisTemplate beans.

Lesson 19 #

RuntimeException is a subclass of Exception, so if rollbackFor is set to Exception.class, it will also take effect for RuntimeException. What should we do if we need to perform a rollback operation for Exception, but not for RuntimeException?

We can specify both the rollbackFor and noRollbackFor attributes for @Transactional, as shown in the following code example:

@Transactional(rollbackFor = Exception.class, noRollbackFor = RuntimeException.class)
public void doSaveStudent(Student student) throws Exception {
    studentMapper.saveStudent(student);
    if (student.getRealname().equals("小明")) {
        throw new RuntimeException("该用户已存在");
    }
}

Lesson 20 #

Consider the following question based on Case 2: In this case, we declared the transaction propagation attribute on the method in the CardService class as @Transactional(propagation = Propagation.REQUIRES_NEW). Will it work if we use Spring’s default declaration? Why?

The answer is no. As mentioned earlier, the default transaction propagation type in Spring is REQUIRED, which means that an inner transaction joins the existing outer transaction. If we declare it as REQUIRED, when we try to manipulate the card data, we will still be using the original DataSource.

Lesson 21 #

When we compare Case 1 and Case 2, you’ll notice that regardless of whether we use query parameters or form parameters, our interface definition remains the same, as follows:

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

So does @RequestParam itself handle these two types of data?

Without considering the implementation principle, if we carefully examine the API documentation of @RequestParam, you’ll find that @RequestParam can handle not only form parameters, but also query parameters. The API documentation is as follows:

In Spring MVC, “request parameters” map to query parameters, form data, and parts in multipart requests. This is because the Servlet API combines query parameters and form data into a single map called “parameters”, and that includes automatic parsing of the request body.

If we delve a little deeper, we can also see the specific implementation from the source code.

Regardless of whether we use query parameters or form parameters to access, for the example program, the key logic for parsing is similar, and the parameter parsing is accomplished through the following call stack:

Here, we can see that the responsible resolver for parsing is RequestParamMethodArgumentResolver, and the last method called is the same. In the method org.apache.catalina.connector.Request#parseParameters, the parsing of form parameters is shown as follows:

if (!("application/x-www-form-urlencoded".equals(contentType))) {
    success = true;
    return;
}

// If reached here, it means it's a Form: "application/x-www-form-urlencoded"
int len = getContentLength();

if (len > 0) {
    int maxPostSize = connector.getMaxPostSize();
    if ((maxPostSize >= 0) && (len > maxPostSize)) {
       // omit non-critical code
    }
    byte[] formData = null;
    if (len < CACHED_POST_LEN) {
        if (postData == null) {
            postData = new byte[CACHED_POST_LEN];
        }
        formData = postData;
    } else {
        formData = new byte[len];
    }
    try {
        if (readPostBody(formData, len) != len) {
            parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
            return;
        }
    } catch (IOException e) {
          // omit non-critical code
    }
// Add Form data to parameters
parameters.processParameters(formData, 0, len);

The form data is ultimately stored in Parameters#paramHashValues.

As for the handling of query parameters, it is also in org.apache.catalina.connector.Request#parseParameters, but the code line handling it is located before the code line handling the form data. The key call code line is as follows:

parameters.handleQueryParameters();

Ultimately, it also uses org.apache.tomcat.util.http.Parameters#processParameters to add data. Naturally, it is stored in Parameters#paramHashValues.

In conclusion, although we use the same fixed annotation @RequestParam, it can handle both form and query parameters because they are all stored in the same location: Parameters#paramHashValues.

Lesson 22 #

In Case 1, we explained why the test program could not load the spring.xml file. The root cause is that when you use the following statement to load the file, they use different Resource forms to load it:

@ImportResource(locations = {"spring.xml"})

Specifically, the application loading uses ClassPathResource, while the test loading uses ServletContextResource. So how does this happen?

Actually, the type of Resource used for loading is determined by DefaultResourceLoader#getResource:

@Override
public Resource getResource(String location) {
   // Omit non-critical code
   if (location.startsWith("/")) {
      return getResourceByPath(location);
   }
   else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
      return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
   }
   else {
      try {
         // Try to parse the location as a URL...
         URL url = new URL(location);
         return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
      }
      catch (MalformedURLException ex) {
         // No URL -> resolve as resource path.
         return getResourceByPath(location);
      }
   }
}

Combining the code above, you can see that when you use the following statement:

@ImportResource(locations = {"classpath:spring.xml"})

The branch it takes is:

   // CLASSPATH_URL_PREFIX: classpath
   else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
      return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
   }

In other words, it creates a ClassPathResource.

When you use the following statement:

@ImportResource(locations = {"spring.xml"})

The branch it takes is:

      try {
         // Load by URL
         URL url = new URL(location);
         return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
      }
      catch (MalformedURLException ex) {
         // Load by path
         return getResourceByPath(location);
      }

It first tries to load by URL, and it’s clearly going to fail because the string spring.xml is not a URL. Then it uses getResourceByPath() to load, which will execute WebApplicationContextResourceLoader#getResourceByPath():

 private static class WebApplicationContextResourceLoader extends ClassLoaderFilesResourcePatternResolver.ApplicationContextResourceLoader {
    private final WebApplicationContext applicationContext;
    // Omit non-critical code
    protected Resource getResourceByPath(String path) {
        return (Resource)(this.applicationContext.getServletContext() != null ? new ServletContextResource(this.applicationContext.getServletContext(), path) : super.getResourceByPath(path));
    }
}

As you can see, at this point, it is actually closely related to the ApplicationContext. In our case, the final result returned is ServletContextResource.

I believe that by now, you can understand why a small change can lead to different generated resources. It’s just a matter of different formats defined, which create different resources and have different loading logic. As for how subsequent loading works, you can refer back to the full text.

That’s all for this Q&A session. See you in the next lesson!