Source code analysis SpringMVC’s RequestMapping annotation principle

1. Start initialization

Core: After obtaining all the beans existing in the application context, traverse them in sequence, analyze the annotation @RequestMapping that exists in each target handler & target method, and encapsulate its related attributes into an instance RequestMappingInfo. Finally, the mapping relationship between uri & handler is maintained in the internal class RequestMappingInfo in class AbstractHandlerMethodMapping.

Use the InitializingBean interface feature of RequestMappingHandlerMapping to complete the mapping relationship between request uri & handler. For specific details, refer to its parent class AbstractHandlerMethodMapping to implement its functions, as follows:

public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {<!-- -->
//inner class
private final MappingRegistry mappingRegistry = new MappingRegistry();
\t
protected void initHandlerMethods() {<!-- -->
for (String beanName : getCandidateBeanNames()) {<!-- -->//Get all beans from the application context
processCandidateBean(beanName);
}
}
\t
protected void processCandidateBean(String beanName) {<!-- -->
Class<?> beanType = obtainApplicationContext().getType(beanName);
// The condition to determine whether it is a Handler is: whether there is an annotation Controller or RequestMapping
if (beanType != null & amp; & amp; isHandler(beanType)) {<!-- -->
detectHandlerMethods(beanName);
}
}
\t\t
protected void detectHandlerMethods(Object handler) {<!-- -->//handler is a beanName of type String
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {<!-- -->
Class<?> userType = ClassUtils.getUserClass(handlerType);
// Set methods and their key: Method class in reflection. value:RequestMappingInfo
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {<!-- -->
// Call a specific implementation class, such as RequestMappingHandlerMapping
return getMappingForMethod(method, userType);
});
methods.forEach((method, mapping) -> {<!-- -->
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
\t
protected void registerHandlerMethod(Object handler, Method method, T mapping) {<!-- -->
this.mappingRegistry.register(mapping, handler, method);
}
}

1.1, RequestMappingHandlerMapping

Parse the @RequestMapping annotation related properties of the target class & target method.

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping{<!-- -->
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {<!-- -->
//Encapsulate the @RequestMapping annotation related properties of the target method into RequestMappingInfo
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {<!-- -->
//If the target handler also has the @RequestMapping annotation, it will also be encapsulated as RequestMappingInfo.
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {<!-- -->
// Merge the @RequestMapping related properties of the target method with the @RequestMapping related properties of the target handler
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);//Get the url prefix of the target handler
if (prefix != null) {<!-- -->
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}
\t
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {<!-- -->
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
return (requestMapping != null ? createRequestMappingInfo(requestMapping, null) : null);
}
\t
protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping,RequestCondition condition) {<!-- -->
// Get the @RequestMapping annotation related properties of a specific target method, and finally encapsulate it into RequestMappingInfo
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());
return builder.options(this.config).build();
}
}

1.2.MappingRegistry

public abstract class AbstractHandlerMethodMapping<T> {<!-- -->

class MappingRegistry {<!-- -->
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
//Collect the key of mappingLookup: RequestMappingInfo. value:HandlerMethod
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
\t\t
public void register(T mapping, Object handler, Method method) {<!-- -->
...
//The target method of the target class is finally encapsulated as HandlerMethod and the handler is actually the beanName of the target bean.
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
this.mappingLookup.put(mapping, handlerMethod);
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {<!-- -->
this.urlLookup.add(url, mapping);
}
...
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
}
}

2. Processing requests

SpringMVC uses the request url to obtain the target handler.

public class DispatcherServlet extends FrameworkServlet {<!-- -->
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {<!-- -->
if (this.handlerMappings != null) {<!-- -->
for (HandlerMapping mapping : this.handlerMappings) {<!-- -->
// Expand around RequestMappingHandlerMapping
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {<!-- -->
return handler;
}
}
}
return null;
}
}
public abstract class AbstractHandlerMethodMapping<T> implements InitializingBean {<!-- -->
private final MappingRegistry mappingRegistry = new MappingRegistry();
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request){<!-- -->
// Get requestUri
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
// Get the target handler from mappingRegistry
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
\t
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) {<!-- -->
List<Match> matches = new ArrayList<>();
//Use lookupPath to obtain the T of the target handler from the MappingRegistry related properties.
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
// From Chapter 1, we learned that the returned class is RequestMappingInfo, which is the related attribute of the @RequestMapping annotation.
if (directPathMatches != null) {<!-- -->
//Analyze request
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {<!-- -->
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
// If the element in local matches is empty, it means that the request header verification failed.
if (!matches.isEmpty()) {<!-- -->
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
Match bestMatch = matches.get(0);
...
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}else {<!-- -->
//Finally the relevant exception is thrown here
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
}

The exceptions thrown when request header related parameter verification fails include: HttpRequestMethodNotSupportedException, HttpMediaTypeNotSupportedException, HttpMediaTypeNotAcceptableException, UnsatisfiedServletRequestParameterException.

But it seems that these types of exceptions cannot be intercepted and handled by global interceptors.

2.1. Parsing request header related attributes

The core is to verify whether the relevant attributes in the request match the attributes in RequestMappingInfo.

public abstract class AbstractHandlerMethodMapping<T> implements InitializingBean {<!-- -->

private void addMatchingMappings(Collection mappings, List matches, HttpServletRequest request) {<!-- -->
for (T mapping : mappings) {<!-- -->
T match = getMatchingMapping(mapping, request);
if (match != null) {<!-- -->//Normal request verification passes, and the repackaged RequestMappingInfo is finally returned.
matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
}
}
}
}
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping {<!-- -->
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {<!-- -->
return info.getMatchingCondition(request);
}
}

2.1.1, RequestMappingInfo

The relevant attributes in this class are encapsulations of the field attributes annotated with @RequestMapping.

public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {<!-- -->
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {<!-- -->
//Request method: Verify the Method attribute, that is, whether it is the corresponding request method such as post or get.
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
if (methods == null) {<!-- -->
return null;
}
//Request parameters
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
if (params == null) {<!-- -->
return null;
}
//consumer parameter: Whether the contentType in the request header is consistent.
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
if (headers == null) {<!-- -->
return null;
}
//Producer parameter: Whether the Accept in the request header is consistent
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
if (consumes == null) {<!-- -->
return null;
}
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (produces == null) {<!-- -->
return null;
}
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null) {<!-- -->
return null;
}
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
if (custom == null) {<!-- -->
return null;
}
return new RequestMappingInfo(this.name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}
}