Spring MVC source code analysis

From 17 years of contact with java to now, it has been almost four years. Before, I mainly did the development related to C language. I can say that I started learning java from the beginning. The specific bitterness course is not shown.

There have been quite a lot of discussions on the content of spring MVC. This paper wants to analyze it from the perspective of the most direct and closest to the source code. It is not only a sort of self-knowledge, but also wants to share the skills and experience of learning the source code, so that more people can avoid detours and improve the ability of the code.
The project is very simple, and the structure is shown in the figure below:


The specific code will be put on github.
Operation effect:

How is the controller called step by step?


By adding a breakpoint to the controller method code and debugging startup, you can see the calling roadmap. We can generally see that DispatcherServlet is the entry, and then processed through RequestMappingHandlerAdapter#handle(), ServletInvocableHandlerMethod#invokeAndHandle(), and finally method Invoke() reflection called. Analyze from these steps:
1.RequestMappingHandlerAdapter is an implementation class of HandlerAdapter interface. The implementation classes of HandlerAdapter are as follows: ① RequestMappingHandlerAdapter uses RequestMapping to mark the controller method, The handler he supports is HandlerMethod (created when scanning the controller, which will be described later) ② AnnotationMethodHandlerAdapter has been discarded and has the same function as 1 ③ HttpRequestHandlerAdapter is responsible for calling handlers such as HttpRequestHandler, which is generally used in rpc calls based on HTTP. You can see the implementation classes of HttpRequestHandler, such as HessianServiceExporter and httpinvokerserviceexport r. It also includes ResourceHttpRequestHandler for static resource access; For example, using HessianServiceExporter, you can define a BeanNameUrlHandlerMapping and add a bean. beanName is required to start with / slash (used as url), and it is a handler itself. ④ SimpleControllerHandlerAdapter when the handler implements the controller interface (not annotation) ⑤ SimpleServletHandlerAdapter when the handler itself is a Servlet.
So many implementations are not meant to be used directly by developers, because the automatically built RequestMappingHandlerAdapter is enough. There are many implementation classes designed by the framework to support more situations.
2. Take a look at what the RequestMappingHandlerAdapter has done?
AbstractHandlerMethodAdapter is its parent class, which implements the interface method of HandlerAdapter: handle(HttpServletRequest, HttpServletResponse, Object); RequestMappingHandlerAdapter rewrites its handleInternal() method (abstract template design pattern), and goes through the following steps: checkRequest() is the check of session session, invokeHandlerMethod() calls the target method, and prepareResponse() is ready to return.
3. The important logic can be seen from the invokeHandlerMethod() method, which goes through the following steps: package the request response as a ServletWebRequest object, obtain binderFactory, modelFactory, createInvocableHandlerMethod(), create a reflective calling object, create a mavContainer object, and call the newly built invocableMethod object, Finally, getModelAndView() returns (for ResponseBody, view is null, and the returned content is directly written to HttpResponse, which will be described below).
4. The servletinvocablehandlermethod #invokeandhandle() method goes through the following steps: invokeForRequest() calls the target method, setResponseStatus() sets the http status (processing the @ ResponseStatus annotation), and this. Returnvaluehandlers. Handleretreturnvalue() processes the returned results. In addition to the setresponsestatus () method, the other two methods are very important, with more processing logic.
5. The servletinvocablehandlermethod #invokeforrequest() method first obtains the corresponding parameters through getMethodArgumentValues(), which is specifically implemented through HandlerMethodArgumentResolver#resolveArgument() (including @ RequestParam @PathVariable @RequestBody, etc., there are a series of parsing classes, which will not be discussed temporarily). doInvoke() completes the reflection call.
6.ServletInvocableHandlerMethod.returnValueHandlers.handleReturnValue() handles the response result by the combination handler of HandlerMethodReturnValueHandlerComposite. Here, an interface HandlerMethodReturnValueHandler is introduced. There are different processing methods for different return value class types, such as HttpEntity, @ RequestBody, Map, HttpHeaders, DeferredResult, ModelAndView, @ ModelAttribute Model, View, etc. For methods annotated with @ ResponseBody, the return value processing class RequestResponseBodyMethodProcessor will be selected. The treatment method is as follows
7. Call the writeWithMessageConverters() method, which is defined in the AbstractMessageConverterMethodProcessor in the parent class, and call the appropriate httpmessageconverter to write the returned result to httpresponse. Which http message converters actually correspond to the media type returned by http, such as text / plain = > StringHttpMessageConverter, application / octet stream = > ByteArrayHttpMessageConverter, / = > resourcehttpmessageconverter, ByteArrayHttpMessageConverter, application / x-www-form-urlencoded = > allencompassionformhttpmessageconverter, application/json+=>MappingJackson2HttpMessageConverter. Each may also have restrictions on the return value. For example, the ByteArrayHttpMessageConverter can only process the return value of byte [], and the StringHttpMessageConverter can only process the return value of String. After selecting the appropriate Converter, call getadvice() Beforebodywrite(), and then call genericconverter Write () performs the actual return object write httpresponse logic.
8.getAdvice().beforeBodyWrite() calls our ResponseBodyAdvice interceptor implementation class annotated with @ ControllerAdvice, so that we can make some interested modifications before writing http messages.
9. For the helloworld example project, the value is a String, so the matching type is text/html, the converter is StringHttpMessageConverter, the key method is writeInternal(), and the method body is streamutils Copy() transfers characters into byte streams by stream copying and writes them to HttpResponse.
10. As mentioned above, the main implementation logic can be discussed again in many places, and the length is very large. For example, in point 5, getMethodArgumentValues() obtains the input parameters of the controller method. There are also quite a few related processing classes. Breakpoint tracking can be viewed as needed.

How does the url map to the Controller method?

It can be seen from the above that the corresponding Handler obtained from the url is obtained through HandlerMapping. For specific logic, you need to check the initialization process of HandlerMapping. Here we focus on RequestMappingHandlerMapping. Because all this happens in the doDispatch() method of the DispatcherServlet, we naturally need to see how the handlerMappings in it are initialized. After a simple search, we can see that a method #initHandlerMappings() is initialized. Aside: this method overloads the onRefresh() method of the FrameworkServlet. You can look at the calling stack at the breakpoint:

The root of initialization method calls is javax servlet. Servlet #init() method, which is the servlet specification, will automatically initialize init() when the servlet is called for the first time. The init() method is implemented by HttpServletBean. Httpservlet (Tomcat) - > HttpServletBean (spring) - > frameworkservlet - > DispatcherServlet is the inheritance relationship of DispatcherServlet.
1.initHandlerMappings() method, through beanfactoryutils #beanfactoryutils Beansoftypeincludingprocessors() gets all beans that implement HandlerMapping. (therefore, if there are custom handlermaps, they will also be found and used by DispatcherServlet). You can see that there are five handlermaps found, namely RequestMappingHandlerMapping, BeanNameUrlHandlerMapping, RouterFunctionMapping, simpleurhandlermapping, WelcomePageHandlerMapping. Where do these handlermaps come from, in fact, in webmvcconfigurationsupplier In T,
The last welcome pagehandlermapping is introduced by the EnableWebMvcConfiguration defined in springboot and is not discussed in detail.
A key operation is sorting. The order of RequestMappingHandlerMapping is 0, the order of BeanNameUrlHandlerMapping is 2, the order of RouterFunctionMapping is 3, and the order of simpleurhandlermapping is ordered LOWEST_ Precision - 1, i.e. 2147483646, is very small. The significance of sorting is that every time we find the handler according to the url, we find it in turn. If two handlermappings have the same url, or a HandlerMapping has a Fallback default handler, we will never enter our controller method.
2. Discussion on the initialization details of RequestMappingHandlerMapping in webmvcconfigurationsupport.

In fact, it creates a RequestMappingHandlerMapping object. It has a very important method, afterpropertieset (). Directly, it calls super afterPropertiesSet(), that is, the parent class AbstractHandlerMethodMapping#afterPropertiesSet() method,

    protected void initHandlerMethods() {
		for (String beanName : getCandidateBeanNames()) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				processCandidateBean(beanName);
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}

3.initHandlerMethods completes the url registration of RequestMappingHandlerMapping. Obtained by getCandidateBeanNames() method,

    protected String[] getCandidateBeanNames() {
		return (this.detectHandlerMethodsInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
				obtainApplicationContext().getBeanNamesForType(Object.class));
	}

After obtaining the bean name list, process each bean by calling the processCandidateBean() method.
4.processCandidateBean() is used to judge whether there is a method of interest on the bean. You can see that the judgment is made through the isHandler() method.

    protected void processCandidateBean(String beanName) {
		Class<?> beanType = null;
		try {
			beanType = obtainApplicationContext().getType(beanName);
		}
		catch (Throwable ex) {
			// An unresolvable bean type, probably from a lazy bean - let's ignore it.
			if (logger.isTraceEnabled()) {
				logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
			}
		}
		if (beanType != null && isHandler(beanType)) {
			detectHandlerMethods(beanName);
		}
	}
	// This method is overloaded by RequestMappingHandlerMapping
	@Override
	protected boolean isHandler(Class<?> beanType) {
		return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
				AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
	}

It can be clearly seen that he judges whether the bean is the one we want, that is, whether the bean is marked with @ Controller annotation or @ RequestMapping annotation. Then enter the detectHandlerMethods() method.
5. The detecthandlermethods () method is the final place to get the mapping between the url and the handler. He passed the method inspector The selectmethods () static method obtains the list of methods of interest and filters them through the getMappingForMethod() method.

    protected void detectHandlerMethods(Object handler) {
		Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());

		if (handlerType != null) {
			Class<?> userType = ClassUtils.getUserClass(handlerType);
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
					});
			if (logger.isTraceEnabled()) {
				logger.trace(formatMappings(userType, methods));
			}
			methods.forEach((method, mapping) -> {
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				registerHandlerMethod(handler, invocableMethod, mapping);
			});
		}
	}
	// This method is overloaded in RequestMappingHandlerMapping
    @Override
	@Nullable
	protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
		RequestMappingInfo info = createRequestMappingInfo(method);
		if (info != null) {
			RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
			if (typeInfo != null) {
				info = typeInfo.combine(info);
			}
			String prefix = getPathPrefix(handlerType);
			if (prefix != null) {
				info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
			}
		}
		return info;
	}
	// This method is also overloaded in RequestMappingHandlerMapping
	@Nullable
	private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
		RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
		RequestCondition<?> condition = (element instanceof Class ?
				getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
		return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
	}

Obviously, it is to find whether there is a @ RequestMapping annotation marked on each method method. If not, it will return null, that is, it will not be added to the final methods collection. After discovery, register by calling the registerHandlerMethod() method.
6.

    protected void registerHandlerMethod(Object handler, Method method, T mapping) {
		this.mappingRegistry.register(mapping, handler, method);
	}
	// AbstractHandlerMethodMapping.MappingRegistry.register(T, Object, Method) method
	public void register(T mapping, Object handler, Method method) {
			// Assert that the handler method is not a suspending one.
			if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
				Class<?>[] parameterTypes = method.getParameterTypes();
				if ((parameterTypes.length > 0) && "kotlin.coroutines.Continuation".equals(parameterTypes[parameterTypes.length - 1].getName())) {
					throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
				}
			}
			this.readWriteLock.writeLock().lock();
			try {
				HandlerMethod handlerMethod = createHandlerMethod(handler, method);
				validateMethodMapping(handlerMethod, mapping);
				this.mappingLookup.put(mapping, handlerMethod);

				List<String> directUrls = getDirectUrls(mapping);
				for (String url : directUrls) {
					this.urlLookup.add(url, mapping);
				}

				String name = null;
				if (getNamingStrategy() != null) {
					name = getNamingStrategy().getName(handlerMethod, mapping);
					addMappingName(name, handlerMethod);
				}

				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();
			}
		}

Focus on these lines of code here. HandlerMethod handlerMethod = createHandlerMethod(handler, method); Create a handler method (i.e. handler), this. Mappinglookup. Put (mapping, handler method); And this urlLookup. add(url, mapping); This is where the URL mapping is finally registered. The specific usage of these two methods is to find mapping according to the URL first, and then find handlermethod according to mapping. Mapping here refers to the RequestMappingInfo entity class.
7. In the dispatcher servlet, find the handler according to the url from the HandlerMapping. How does RequestMappingHandlerMapping do?

    // From AbstractHandlerMapping abstract class
    @Override
	@Nullable
	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		Object handler = getHandlerInternal(request);
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}

		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		if (logger.isTraceEnabled()) {
			logger.trace("Mapped to " + handler);
		}
		else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
			logger.debug("Mapped to " + executionChain.getHandler());
		}

		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			config = (config != null ? config.combine(handlerConfig) : handlerConfig);
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}

		return executionChain;
	}
	// From AbstractHandlerMethodMapping
	@Override
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
		request.setAttribute(LOOKUP_PATH, lookupPath);
		this.mappingRegistry.acquireReadLock();
		try {
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}
	// From AbstractHandlerMethodMapping
	@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
		if (directPathMatches != null) {
			addMatchingMappings(directPathMatches, matches, request);
		}
		if (matches.isEmpty()) {
			// No choice but to go through all mappings...
			addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
		}

		if (!matches.isEmpty()) {
			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
			matches.sort(comparator);
			Match bestMatch = matches.get(0);
			if (matches.size() > 1) {
				if (logger.isTraceEnabled()) {
					logger.trace(matches.size() + " matching mappings: " + matches);
				}
				if (CorsUtils.isPreFlightRequest(request)) {
					return PREFLIGHT_AMBIGUOUS_MATCH;
				}
				Match secondBestMatch = matches.get(1);
				if (comparator.compare(bestMatch, secondBestMatch) == 0) {
					Method m1 = bestMatch.handlerMethod.getMethod();
					Method m2 = secondBestMatch.handlerMethod.getMethod();
					String uri = request.getRequestURI();
					throw new IllegalStateException(
							"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
				}
			}
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.handlerMethod;
		}
		else {
			return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
		}
	}

Here, although the lookupHandlerMethod() method looks very long, it is not a simple equivalent matching because it deals with Rest style cases with pathvariables. But let's focus on this mappingRegistry. The line getmappingsbyurl () is fine. Isn't that how to find mapping according to the url?
That's enough for this analysis. There are still many specific details. For example, the call adaptation of RequestMappingHandlerAdapter to handlerMethod is also quite colorful. Please update it later!

Keywords: Java Back-end Spring MVC

Added by wilsonodk on Mon, 03 Jan 2022 22:50:58 +0200