Let's take a look at the implementation principle of spring MVC core components

Posted by Muses on Fri, 28 Jan 2022 05:15:49 +0100

In spring MVC, if we need to open the annotation, that is, @ RequestMapping, which we often use, and the @ Controller annotation needs to take effect, we usually use the label "< MVC: annotation driven / >" provided by the spring MVC framework to complete it. So how does this tag complete annotation driven to make annotations effective? Let's reveal the effective process!

1. What did the label do?

First of all, you need to know what the tag has done. First, you have to find out how the tag is parsed. As for label parsing, it has been described in detail in the previous article. You can refer to the article: "It's not easy for the Spring tag to take effect" Here, we directly find the corresponding tag parser AnnotationDrivenBeanDefinitionParser class, and then find the parse method in the class. The core operation is to create several beandefinitions through new and put them into the container. One of the more important definitions is RequestMappingHandlerMapping. Of course, there are several other beandefinitions. Don't worry. We'll introduce them later.

Summary: the tag here can be considered as doing one thing for the time being, that is, creating a Bean definition with the type of RequestMappingHandlerMapping, and then putting it into the Bean definition registry of Spring.

2. After that, what preparations need to be made?

After step 1 above is completed, there is a Bean definition of type RequestMappingHandlerMapping in the container, so the next step must be to create a Bean through this Bean definition. How? Let's take a look at the inheritance diagram of the RequestMappingHandlerMapping class, as follows:

This seemingly complex inheritance diagram is actually very simple. It just implements a bunch of extension point interfaces provided by Spring. You can take a look at the extension point at the end of Aware. There are no special operations in it. Take another look at the implementation of an InitializaingBean extension point method in RequestMappingHandlerMapping. After clicking, the code is as follows:

@Override
public void afterPropertiesSet() {
 this.config = new RequestMappingInfo.BuilderConfiguration();
 this.config.setUrlPathHelper(getUrlPathHelper());
 this.config.setPathMatcher(getPathMatcher());
 this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
 this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
 this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
 this.config.setContentNegotiationManager(getContentNegotiationManager());
  
  super.afterPropertiesSet();
}

It doesn't seem to have anything to do with @ RequestMapping, but after calling the afterpropertieset method of the parent class, you will find something fishy after opening the implementation in the parent class. The code is as follows:

@Override
public void afterPropertiesSet() {
 initHandlerMethods();
}

According to the naming habits specified in the Spring framework, you can guess that this is the way to initialize the processor method. Continue to check this method, you can see the following code, which makes a filter according to the name of the bean. As we mentioned in the previous article, you can specify the bean name of the declared bean in Spring, or you can not specify it or not. By default, it is generated according to the fully qualified name of the class, and certainly not with "scopedTarget." Start, so the processcandidebean method will be called!

protected void initHandlerMethods() {
 for (String beanName : getCandidateBeanNames()) {
  // If the name of the bean is not "scopedTarget." start
  if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
   // Process candidate beans
   processCandidateBean(beanName);
  }
 }
 handlerMethodsInitialized(getHandlerMethods());
}

In the processCandidateBean method, first call the getType method provided by BeanFactory according to the bean name to obtain the class type defined by the bean, and then make a key judgment. The code is as follows:

protected void processCandidateBean(String beanName) {
 Class<?> beanType = null;
 try {
  beanType = obtainApplicationContext().getType(beanName);
 }
 catch (Throwable ex) {}
 
 // If the bean type resolved according to the bean name is marked with @ controller or @ RequestMapping annotation, the parsing operation of the request mapping method will be carried out
 if (beanType != null && isHandler(beanType)) {
  // Resolve the mapping relationship between the request URL and the Controller
  detectHandlerMethods(beanName);
 }
}

The isHandler method in this if judgment is implemented as follows:

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

You can see that it is to judge whether this class is marked with @ Controller annotation and @ RequestMapping annotation. Therefore, it can be locked. The processCandidateBean method is used to complete the preparation of the @ RequestMapping annotation function. If either of these two annotations is marked on the class, it will execute the detectHandlerMethods method, that is, the probe processor method. I have to admire the great gods who wrote the Spring framework. The names are so vivid and appropriate!

After that, continue to check the detectHandlerMethods method, as follows:

protected void detectHandlerMethods(Object handler) {
 // Get the class type of the Handler, that is, the class corresponding to the Controller
 Class<?> handlerType = (handler instanceof String ?
   obtainApplicationContext().getType((String) handler) : handler.getClass());
 if (handlerType != null) {
  // Whether the current Class is a proxy Class is determined by whether the Class name contains the $$symbol
  Class<?> userType = ClassUtils.getUserClass(handlerType);
  Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
    (MethodIntrospector.MetadataLookup<T>) method -> {
     try {
      return getMappingForMethod(method, userType);
     }
     catch (Throwable ex) {}
    });
  // mapping is a RequestMappingInfo object
  methods.forEach((method, mapping) -> {
   Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
   registerHandlerMethod(handler, invocableMethod, mapping);
  });
 }
}

This method looks rather coquettish, but only needs to confirm three points:

(1) The returned methods are a Map type, the key is a method object, and what is value?

Check the selectMethods method implementation of MethodIntrospector. In fact, this value is the return result of the second callback function of this method, that is, the result of getMappingForMethod. Check the getMappingForMethod method. Its implementation is in RequestMappingHandlerMapping. The code is as follows:

@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
 // Resolve the @ RequestMapping annotation on the method to a RequestMappingInfo object
 RequestMappingInfo info = createRequestMappingInfo(method);
 if (info != null) {
  // Resolve the @ RequestMapping annotation on the class to a RequestMappingInfo object
  RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
  // If there are annotations on the class, the information in @ RequestMapping on the class and @ RequestMapping on the method will be merged. For example, merge requestUri
  if (typeInfo != null) {
   info = typeInfo.combine(info);
  }
  String prefix = getPathPrefix(handlerType);
  if (prefix != null) {
   info = RequestMappingInfo.paths(prefix).build().combine(info);
  }
 }
 return info;
}

You can see that the return value type of this method is RequestMappingInfo. According to the name and the implementation logic of this method, this object is used to encapsulate the mapping relationship between the method and the processor.

(2) How does getMappingForMethod resolve HandlerMethod?

It combines the @ RequestMapping annotation information on the class and the @ RequestMapping information on the method, usually our request uri. That is, "there is a @ RequestMapping("/api/user ") on the class and a @ RequestMapping("/listUser ")" on the listUser method through TypeInfo After combine, it becomes / api/user/listUser. Praise the method naming in Spring again. It's very appropriate.

In the above code, when creating requestmapping, the bottom layer uses the builder design pattern. The code looks particularly elegant, as follows:

protected RequestMappingInfo createRequestMappingInfo(
  RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
 // Create RequestMappingInfo object
 RequestMappingInfo.Builder builder = RequestMappingInfo
   .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
   .methods(requestMapping.method())
   .params(requestMapping.params())
   .headers(requestMapping.headers())
   .consumes(requestMapping.consumes())
   .produces(requestMapping.produces())
   .mappingName(requestMapping.name());
 if (customCondition != null) {
  builder.customCondition(customCondition);
 }
 return builder.options(this.config).build();
}

So even if you don't understand the code, it's good to learn the code habits of big guys!

(3) After getting the methods, where does the registerHandlerMethod in the loop register this method?

After opening the registerHandlerMethod method, it is found that the method of registering the processor is completed through a MappingRegistry. The code is as follows:

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
 this.mappingRegistry.register(mapping, handler, method);
}

Check the register method of this MappingRegistry to see the real logic of saving the @ RequestMapping request path and processor method, as follows:

public void register(T mapping, Object handler, Method method) {
 // Because it is registered when Spring starts, there may be the possibility of repeated registration on each virtual machine to ensure thread safety
 this.readWriteLock.writeLock().lock();
 try {
  // Create HandlerMethod object
  HandlerMethod handlerMethod = createHandlerMethod(handler, method);
  // Verify the uniqueness of HandlerMethod
  assertUniqueMethodMapping(handlerMethod, mapping);
  // Register the correspondence between RequestHandlerMappingInfo and HandlerMethod
  this.mappingLookup.put(mapping, handlerMethod);
  // Find a URL with a match in the URL
  List<String> directUrls = getDirectUrls(mapping);
  for (String url : directUrls) {
   this.urlLookup.add(url, mapping);
  }
  String name = null;
  // If the generation policy of Mapping name is set, the policy is used to generate the name of Mapping
  // The name generation strategy set when creating RequestMappingHandlerMapping is: RequestMappingInfoHandlerMethodMappingNamingStrategy
  if (getNamingStrategy() != null) {
   name = getNamingStrategy().getName(handlerMethod, mapping);
   addMappingName(name, handlerMethod);
  }
  // Initialize cross domain configuration
  CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
  if (corsConfig != null) {
   this.corsLookup.put(handlerMethod, corsConfig);
  }
  this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
 }
 finally {
  this.readWriteLock.writeLock().unlock();
 }
}

There are also two important points in this method:
① First, there is an assertUniqueMethodMapping method, which is used to verify whether RequestMapping is unique. Sometimes the path in @ RequestMapping in multiple controllers is written repeatedly, and the startup error is verified by this;

② The mapping relationship between spring MVC request path and processor is not a direct mapping relationship between url and method, but several sets of mapping relationships are saved, including: a.mapping and HandlerMethod; b.url and mapping; c.mapping and MappingRegistration. In this MappingRegistration, there is only some more information on the basis of HandlerMethod; d.HandlerMethod and corsConfig, which will be used when corsConfig is cross domain;

Summary: in the process of creating a Bean in Spring, the corresponding relationship between RequestMapping and HandlerMethod is resolved by executing the extension point method afterpropertieset of InitializingBean, and registered in the mapper registry to complete the preparations!

3. When you're ready, what if you execute?

The length is too long. If you want to know the future, please look forward to the next decomposition.

List of Spring source code articles: https://segmentfault.com/a/11...

Topics: Java Spring Spring Boot