Members of the HandlerMapping family can be divided into two branches. One is inherited from AbastractUrlHandlerMapping, the other is inherited from AbastracHandlerMethodMapping, and both are inherited from AbastractHandlerMapping.
The green box AbstractHandlerMapping abstract class implements the skeleton logic of [obtain the processors and interceptors corresponding to the request], and then provides the getHandlerInternal(HttpServletRequest request) template method, which is implemented by the subclass.
Subclass of AbstractHandlerMapping:
The red box is "AbstractUrlHandlerMapping", which matches based on the URL. Of course, in actual development, this method is basically not used, and is replaced by @ RequestMapping # and other annotation methods. However, this method is still used for some path matching built-in in Spring MVC.
Yellow box: AbstractHandlerMethodMapping system, matching based on Method. For example, we are familiar with @ RequestMapping and other annotation methods.
The white box MatchableHandlerMapping interface defines the interface method to judge whether the request matches the specified {pattern} path.
AbstractHandlerMapping
AbstractHandlerMapping uses template mode to design the overall structure of HandlerMapping, and then subclasses can provide some initial values and specific algorithms by implementing the template method. AbstractHandlerMapping inherits WebApplicationObjectSupport. During initialization, the template method initApplicationContext will be called, that is, AbstractHandlerMapping is created in initApplicationContext.
// AbstractHandlerMapping.java @Override protected void initApplicationContext() throws BeansException { // Empty method. Give it to the subclass implementation, which is used to register custom interceptors into interceptors. At present, there is no subclass implementation. extendInterceptors(this.interceptors); // Scan the beans of registered mappedInterceptors and add them to mappedInterceptors detectMappedInterceptors(this.adaptedInterceptors); // Initialize interceptors to HandlerInterceptor type and add them to mappedInterceptors initInterceptors(); }
The detectmappedinterceptors (list < handlerinterceptor > mappedInterceptors) method scans the beans of registered mappedInterceptors and adds them to mappedInterceptors. The code is as follows:
// AbstractHandlerMapping.java protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) { // Scan the beans of registered mappedInterceptors and add them to mappedInterceptors // MappedInterceptor will match according to the request path and whether to intercept or not. mappedInterceptors.addAll( BeanFactoryUtils.beansOfTypeIncludingAncestors( obtainApplicationContext(), MappedInterceptor.class, true, false).values()); }
initInterceptors() method, initialize 'interceptors' to HandlerInterceptor type and add it to' mappedInterceptors'. The code is as follows:
// AbstractHandlerMapping.java protected void initInterceptors() { if (!this.interceptors.isEmpty()) { // Traversing the interceptors array for (int i = 0; i < this.interceptors.size(); i++) { // Get interceptor object Object interceptor = this.interceptors.get(i); if (interceptor == null) { // If it is empty, an IllegalArgumentException is thrown throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null"); } // Initialize interceptors to HandlerInterceptor type and add them to mappedInterceptors // Note that the HandlerInterceptor does not need path matching and directly intercepts all this.adaptedInterceptors.add(adaptInterceptor(interceptor)); // <x> } } }
You can see that AbstractHandlerMapping saves all interceptors, and its properties have two List type interceptors
/** * The interceptor array configured will not be used directly * * Instead, in the {@ link #initInterceptors()} method, initialize to {@ link #adaptedInterceptors} for use * * There are two ways to add: * * 1. {@link #setInterceptors(Object...)} method * 2. {@link #extendInterceptors(List)} method */ private final List<Object> interceptors = new ArrayList<>(); /** * The initialized interceptor HandlerInterceptor array matches the Url when used */ private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
After initialization, let's see how to get the Handler and Interceptor. getHandler(HttpServletRequest request) method to obtain the Handler executionchain object corresponding to the request. The code is as follows:
// DispatcherServlet.java @Nullable protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { // Traversing the HandlerMapping array for (HandlerMapping mapping : this.handlerMappings) { // Get the HandlerExecutionChain object corresponding to the request HandlerExecutionChain handler = mapping.getHandler(request); // Get, return if (handler != null) { return handler; } } } return null; }
// AbstractHandlerMapping.java @Override @Nullable public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { // <1> Get the processor. This method is an abstract method implemented by subclasses Object handler = getHandlerInternal(request); // <2> If not, the default processor is used if (handler == null) { handler = getDefaultHandler(); } // <3> If it cannot be obtained, null is returned if (handler == null) { return null; } // Bean name or resolved handler? // <4> If the processor found is of String type, find the Bean type corresponding to String from the container as the processor. if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } // <5> Get HandlerExecutionChain object 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 (CorsUtils.isCorsRequest(request)) { CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } // <7> Return return executionChain; }
getHandlerExecutionChain(Object handler, HttpServletRequest request) method to obtain the HandlerExecutionChain object. The code is as follows:
// AbstractHandlerMapping.java protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { // Create HandlerExecutionChain object HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler)); // Get request path String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); // Traverse the adaptedInterceptors array to get the interceptor matching the request for (HandlerInterceptor interceptor : this.adaptedInterceptors) { // It needs to be matched. If the path matches, it will be added to the chain if (interceptor instanceof MappedInterceptor) { MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) { // matching chain.addInterceptor(mappedInterceptor.getInterceptor()); } // Without matching, it is directly added to the chain } else { chain.addInterceptor(interceptor); } } return chain; }
AbstractHandlerMethodMapping
AbstractHandlerMethodMapping implements the InitializingBean interface, inherits the AbstractHandlerMapping abstract class, takes' Method 'as the HandlerMapping abstract class of' Handler ', and provides general skeleton methods such as initialization and registration of Mapping. This is what we often call the template Method pattern.
// AbstractHandlerMethodMapping.java public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean { /** * Mapping registry */ private final MappingRegistry mappingRegistry = new MappingRegistry(); /** * Mapping Naming strategy */ @Nullable private HandlerMethodMappingNamingStrategy<T> namingStrategy; }
Generic T represents a kind of condition specially used to match the Handler. The condition here is not only url, but also Request type (get, post), Request parameter, Head, etc. can be used as the condition to match the Handler. RequestMappingInfo is used by default.
MappingRegistry is the private class of AbstractHandlerMethodMapping and the Mapping registry.
// AbstractHandlerMethodMapping.java#MappingRegistry /** * registry * * KEY: MappingInfo */ private final Map<T, MappingRegistration<T>> registry = new HashMap<>(); /** * The registry stores the correspondence between the matching condition and the HandlerMethod * * KEY: MappingInfo */ private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>(); /** * The mapping of direct URLs saves the correspondence between URLs and matching conditions * * KEY: Direct URL * VALUE: MappingInfo array */ private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>(); /** * MappingInfo Mapping of the name of to HandlerMethod * * KEY: Mapping Name of * VALUE: HandlerMethod array */ private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>(); /** * cors */ private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>(); /** * Read write lock */ private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
initialization
After knowing some key properties, let's take a look at its initialization. AbstractHandlerMapping implements the InitializingBean interface, and the spring container calls its afterpropertieset () method for initialization.
// AbstractHandlerMethodMapping.java /** * Scan only accessible handlermethods */ private boolean detectHandlerMethodsInAncestorContexts = false; @Override public void afterPropertiesSet() { // <x> How to initialize the processor initHandlerMethods(); } /** * Scan beans in the ApplicationContext, detect and register handler methods. * @see #getCandidateBeanNames() * @see #processCandidateBean * @see #handlerMethodsInitialized */ protected void initHandlerMethods() { //Traverse beans and process them one by one for (String beanName : getCandidateBeanNames()) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { // < 1.2 > processing beans processCandidateBean(beanName); } } //Methods of initializing the processor. At present, it is an empty method, and there is no specific implementation yet handlerMethodsInitialized(getHandlerMethods()); }
getCandidateBeanNames() method, get the names of all beans, and then perform traversal processing.
// AbstractHandlerMethodMapping.java /** * Determine the names of candidate beans in the application context. * @since 5.1 * @see #setDetectHandlerMethodsInAncestorContexts * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors */ protected String[] getCandidateBeanNames() { return (this.detectHandlerMethodsInAncestorContexts ? // Accessible BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) : obtainApplicationContext().getBeanNamesForType(Object.class)); }
processCandidateBean(String beanName) method to judge whether the Bean is a processor. If so, scan the processor method. The code is as follows:
// AbstractHandlerMethodMapping.java /** * Determine the type of the specified candidate bean and call * {@link #detectHandlerMethods} if identified as a handler type. * <p>This implementation avoids bean creation through checking * {@link org.springframework.beans.factory.BeanFactory#getType} * and calling {@link #detectHandlerMethods} with the bean name. * @param beanName the name of the candidate bean * @since 5.1 * @see #isHandler * @see #detectHandlerMethods */ protected void processCandidateBean(String beanName) { // <1> Get the type corresponding to the Bean 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); } } // Judge whether the Bean is a processor. If so, scan the processor method if (beanType != null && isHandler(beanType)) { // <2.1> detectHandlerMethods(beanName); // <2.2> } }
Call #ishandler (class <? > beantype) abstract method to judge whether the Bean type is processor. The specific implementation is in RequestMappingHandlerMapping
@Override protected boolean isHandler(Class<?> beanType) { //Judge the @ Controller annotation on the class or the RequestMapping annotation on the method return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); }
detectHandlerMethods are methods to initialize the processor. The code is as follows:
// AbstractHandlerMethodMapping.java /** * Look for handler methods in a handler. * @param handler the bean name of a handler or a handler instance */ protected void detectHandlerMethods(final Object handler) { // <1> Get processor type Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass()); if (handlerType != null) { // <2> Get the real class. Because handlerType may be a proxy class final Class<?> userType = ClassUtils.getUserClass(handlerType); // <3> Get a collection of matching methods Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); // Abstract method, subclass implementation if (logger.isTraceEnabled()) { logger.trace("Mapped " + methods.size() + " handler method(s) for " + userType + ": " + methods); } // <4> Traverse methods and register HandlerMethod one by one methods.forEach((key, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(key, userType); registerHandlerMethod(handler, invocableMethod, mapping); }); } }
#Getmappingformethod (method method, class <? > handlertype) method to obtain the RequestMappingInfo object on the method, which is constructed based on @ RequestMapping \. The code is as follows:
// RequestMappingHandlerMapping.java /** * Uses method and type-level @{@link RequestMapping} annotations to create * the RequestMappingInfo. * @return the created RequestMappingInfo, or {@code null} if the method * does not have a {@code @RequestMapping} annotation. * @see #getCustomMethodCondition(Method) * @see #getCustomTypeCondition(Class) */ @Override @Nullable protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { // <1> Create a RequestMappingInfo object based on the @ RequestMapping annotation on the method RequestMappingInfo info = createRequestMappingInfo(method); if (info != null) { // <2> Based on the @ RequestMapping annotation on the class, merge it RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); if (typeInfo != null) { info = typeInfo.combine(info); } // <3> If there is a prefix, set it to info String prefix = getPathPrefix(handlerType); if (prefix != null) { info = RequestMappingInfo.paths(prefix).build().combine(info); } } return info; }
#Registerhandlermethod (object handler, method, t mapping) method, register HandlerMethod
// AbstractHandlerMethodMapping.java /** * Register HandlerMethod * * Register a handler method and its unique mapping. Invoked at startup for * each detected handler method. * @param handler the bean name of the handler or the handler instance * @param method the method to register * @param mapping the mapping conditions associated with the handler method * @throws IllegalStateException if another method was already registered * under the same mapping */ protected void registerHandlerMethod(Object handler, Method method, T mapping) { this.mappingRegistry.register(mapping, handler, method); } // AbstractHandlerMethodMapping.java#MappingRegistry public void register(T mapping, Object handler, Method method) { // <1> Obtain write lock this.readWriteLock.writeLock().lock(); try { // < 2.1 > create HandlerMethod object HandlerMethod handlerMethod = createHandlerMethod(handler, method); // < 2.2 > verify that the current mapping does not exist, otherwise an IllegalStateException will be thrown assertUniqueMethodMapping(handlerMethod, mapping); // < 2.3 > Add mapping + HandlerMethod to mappingLookup this.mappingLookup.put(mapping, handlerMethod); // < 3.1 > get the normal URL array corresponding to mapping List<String> directUrls = getDirectUrls(mapping); // < 3.2 > add to url + mapping to urlLookup set for (String url : directUrls) { this.urlLookup.add(url, mapping); } // <4> Initialize nameLookup String name = null; if (getNamingStrategy() != null) { // < 4.1 > get the name of Mapping name = getNamingStrategy().getName(handlerMethod, mapping); // < 4.2 > add the name of mapping + HandlerMethod to nameLookup addMappingName(name, handlerMethod); } // <5> TODO 1012 cors CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { this.corsLookup.put(handlerMethod, corsConfig); } // <6> Create a MappingRegistration object and add mapping + MappingRegistration to the registry this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod)); } finally { // <7> Release write lock this.readWriteLock.writeLock().unlock(); } }
use
getHandlerInternal(ServerWebExchange exchange) method to obtain the HandlerMethod object corresponding to the request. The code is as follows:
// AbstractHandlerMethodMapping.java /** * Look up a handler method for the given request. */ @Override protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { // Get the requested path String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); // Acquire read lock this.mappingRegistry.acquireReadLock(); try { // Get HandlerMethod object HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); // Further, obtain the HandlerMethod object return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } finally { // Release read lock this.mappingRegistry.releaseReadLock(); } }
Call #lookupHandlerMethod(ServerWebExchange exchange) method to obtain HandlerMethod object.
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { // <1> Store results that match the current request on the List<Match> matches = new ArrayList<>(); // < 1.1 > priority, matching based on direct URL Mapping List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); } // < 1.2 > if you can't get the matching conditions directly by using louuppath, scan all the mappings in the registry for matching if (matches.isEmpty()) { // No choice but to go through all mappings... addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } // <2> If a Match is found, the handlerMethod property of the best matched Match object is obtained if (!matches.isEmpty()) { // < 2.1 > create a MatchComparator object and sort the matches results Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); matches.sort(comparator); // < 2.2 > get the first Match object Match bestMatch = matches.get(0); // < 2.3 > handle cases where there are multiple Match objects!! if (matches.size() > 1) { if (logger.isTraceEnabled()) { logger.trace(matches.size() + " matching mappings: " + matches); } if (CorsUtils.isPreFlightRequest(request)) { return PREFLIGHT_AMBIGUOUS_MATCH; } // Compare bestMatch and secondBestMatch. If they are equal, there is a problem and an IllegalStateException is thrown // Because the two priorities are the same, it means that it is impossible to judge who has the highest priority 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 + "}"); } } // < 2.4 > process the first Match object handleMatch(bestMatch.mapping, lookupPath, request); // < 2.5 > returns the handlerMethod attribute of the first Match object return bestMatch.handlerMethod; // <3> If no match is found, the mismatch is handled } else { return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); } }
In the whole process, match is used as the carrier. Match is an internal class that encapsulates the matching condition and HandlerMothd attributes.
Addmatchingmappings (collection < T > mappings, list < match > matches, serverwebexchange exchange) method to match the current request with the Mapping in the registry. If the matching is successful, a Mapping record will be generated and added to # matches #. The code is as follows:
// AbstractHandlerMethodMapping.java private void addMatchingMappings(Collection<T> mappings, List<Match> matches, ServerWebExchange exchange) { // Traversing the Mapping array for (T mapping : mappings) { // <1> Perform matching T match = getMatchingMapping(mapping, exchange); // <2> If there is a Match, a Match object is created and added to matches if (match != null) { matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping))); } } }
getMatchingMapping(T mapping, HttpServletRequest request) is implemented by the subclass RequestMappingInfoHandlerMapping
@Override protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) { return info.getMatchingCondition(request); }
RequestMappingInfo
// RequestMappingInfo.java /** * name */ @Nullable private final String name; /** * Condition of request path */ private final PatternsRequestCondition patternsCondition; /** * Condition of request method */ private final RequestMethodsRequestCondition methodsCondition; /** * Condition of parameter */ private final ParamsRequestCondition paramsCondition; /** * Condition of request header */ private final HeadersRequestCondition headersCondition; /** * Conditions for consumable content type */ private final ConsumesRequestCondition consumesCondition; /** * Conditions for producible content type */ private final ProducesRequestCondition producesCondition; /** * Custom conditions */ private final RequestConditionHolder customConditionHolder;
#getMatchingCondition(HttpServletRequest request) method to obtain the matching condition from the current RequestMappingInfo. If it matches, a new RequestMappingInfo object is created based on its matching criteria. If there is no match, null is returned. The code is as follows:
@SuppressWarnings("Duplicates") @Override @Nullable public RequestMappingInfo getMatchingCondition(HttpServletRequest request) { // Match methodsCondition, paramsCondition, headercondition, consumesCondition, producesCondition RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request); ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request); HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request); ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request); ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request); // If any one is empty, null is returned, indicating that the matching failed if (methods == null || params == null || headers == null || consumes == null || produces == null) { return null; } // Match patternsCondition PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request); if (patterns == null) { // If patterns is empty, null will be returned, indicating that matching failed return null; } // Match customConditionHolder RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request); if (custom == null) { // If custom is empty, null will be returned, indicating that matching failed return null; } // Create a matching RequestMappingInfo object. // Why create a RequestMappingInfo object? Because of the current RequestMappingInfo object, a methodsCondition can configure GET, POST, DELETE and other conditions, but it actually matches a request type. At this time, the methods only represent the matching one. return new RequestMappingInfo(this.name, patterns, methods, params, headers, consumes, produces, custom.getCondition()); }
#compareTo(RequestMappingInfo other, HttpServletRequest request) method to compare priorities. The code is as follows:
// RequestMappingInfo.java /** * Compares "this" info (i.e. the current instance) with another info in the context of a request. * <p>Note: It is assumed both instances have been obtained via * {@link #getMatchingCondition(HttpServletRequest)} to ensure they have conditions with * content relevant to current request. */ @Override public int compareTo(RequestMappingInfo other, HttpServletRequest request) { int result; // Automatic vs explicit HTTP HEAD mapping // Special processing for HEAD request method if (HttpMethod.HEAD.matches(request.getMethod())) { result = this.methodsCondition.compareTo(other.getMethodsCondition(), request); if (result != 0) { return result; } } // Compare patternsCondition result = this.patternsCondition.compareTo(other.getPatternsCondition(), request); if (result != 0) { return result; } // Compare paramsCondition result = this.paramsCondition.compareTo(other.getParamsCondition(), request); if (result != 0) { return result; } // Compare headersCondition result = this.headersCondition.compareTo(other.getHeadersCondition(), request); if (result != 0) { return result; } // Compare consumesCondition result = this.consumesCondition.compareTo(other.getConsumesCondition(), request); if (result != 0) { return result; } // Compare productscondition result = this.producesCondition.compareTo(other.getProducesCondition(), request); if (result != 0) { return result; } // Implicit (no method) vs explicit HTTP method mappings // Compare methodsCondition result = this.methodsCondition.compareTo(other.getMethodsCondition(), request); if (result != 0) { return result; } // Compare customConditionHolder result = this.customConditionHolder.compareTo(other.customConditionHolder, request); if (result != 0) { return result; } return 0; }
Although the code is very long, it actually calls the #compareTo(RequestMethodsRequestCondition other, HttpServletRequest request) method corresponding to each attribute one by one according to the priority until it is not equal