Spring源碼從入門到放棄-Controller注冊
{toc}
@contact:zhangxin@benmu-health.com
@update:2017-03-23 02:18:31
@spirng.version:4.3.7.RELEASE
引
本文主要介紹SpringMVC中如何注冊Controller
SpringMVC中Controller由@Controller和@RequestMapping注解定義,@Controller定義對象為一個Controller,@RequestMapping定義了請求url路徑,SpringMVC內部將Controller的方法抽象為多個org.springframework.web.method.HandlerMethod
,將Method的@RequestMapping注解抽象成org.springframework.web.servlet.mvc.method.RequestMappingInfo
,一個到來的http請求,經過DispatcherServlet轉發,通過RequestMappingInfo匹配路徑,找到對應的HandlerMethod處理請求。這里的HandlerMethod可以理解為Controller的一個方法。
1.RequestMappingHandlerMapping
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
類的初始化過程完成了對@Controller和@RequestMapping兩個注解的解析該類由spring容器初始化過程解析。解析<mvc:annotation-driven />
標簽時會自動向spring容器注冊該類。並在DispatcherServlet初始化的時候,在initHandlerMappings()
方法中會從Spring容器中將該HandlerMapping作為DispatcherServlet的成員,用以處理http請求。
繼承關系:
該類實現了HandlerMapping和InitializingBean兩個接口,初始化方法afterPropertiesSet()
完成了對@Controller和@RequestMapping的解析和注冊。
2.afterPropertiesSet
Controller注冊是在初始化方法afterPropertiesSet中,首先拿到Spring容器中所有的Bean,對每一個Bean判斷是否為Controller
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
/**
* Scan beans in the ApplicationContext, detect and register handler methods.
* @see #isHandler(Class)
* @see #getMappingForMethod(Method, Class)
* @see #handlerMethodsInitialized(Map)
*/
protected void initHandlerMethods() {
//拿到所有bean的名字
String[] beanNames = getApplicationContext().getBeanNamesForType(Object.class));
for (String beanName : beanNames) {
Class<?> beanType = null;
//拿到Class
beanType = getApplicationContext().getType(beanName);
//如果改bean帶有@Controller和@RequestMapping注解
if (beanType != null && isHandler(beanType)) {
//注冊hanler mapping即Controller
detectHandlerMethods(beanName);
}
}
}
3.解析RequestMappingInfo
detectHandlerMethods
完成對一個Controller的解析,將@RequestMapping方法解析成映射和可執行的HandlerMethod,映射抽象為RequestMappingInfo(即url pattern),將可執行的HandlerMethod和RequestMappingInfo一起注冊到MappingRegistry中,DispatcherServlet收到一個請求的時候會從MappingRegistry中取出與url匹配的handler method來執行。
protected void detectHandlerMethods(final Object handler) {
Class<?> handlerType = (handler instanceof String ?
getApplicationContext().getType((String) handler) : handler.getClass());
//拿到用戶實際注冊的類,防止CGLIB代理
final Class<?> userType = ClassUtils.getUserClass(handlerType);
//選出該類打@RequestMapping的方法,並轉成Map<Method,RequestMappingInfo>
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
new MethodIntrospector.MetadataLookup<T>() {
@Override
public T inspect(Method method) {
//對每一個method,轉成RequestMappingInfo,如果不帶@RequestMapping注解則返回null
return getMappingForMethod(method, userType);
}
});
for (Map.Entry<Method, T> entry : methods.entrySet()) {
//包裝成一個可執行方法
Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
//實際為RequestMappingInfo
T mapping = entry.getValue();
//將RequestMappingInfo和handler注冊到MappingRegistry
//DispatcherServlet收到一個請求的時候會從MappingRegistry中取出與url匹配的handler來執行
registerHandlerMethod(handler, invocableMethod, mapping);
}
}
getMappingForMethod()
方法中完成了將帶有@RequestMapping注解的方法轉為RequestMappingInfo。
分別將Class和Method上的@RequestMapping拿到,用屬性生成RequestMappingInfo。然后將兩個RequestMappingInfo合並成一個。e.g. Class上的注解為path=/test,Method上的注解為path=/hello,method=POST,合並之后就是path=/test/hello,method=POST,並且為每一個RequestMappingInfo生成一個PatternsRequestCondition,用來完成DispatchServlet分發請求時url匹配。
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//解析method的@RequestMapping
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
//解析Class的@RequestMapping
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
//將兩個@RequestMapping合並
info = typeInfo.combine(info);
}
}
return info;
}
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
//拿到注解
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
return (requestMapping != null ? createRequestMappingInfo(requestMapping, null) : null);
}
protected RequestMappingInfo createRequestMappingInfo(
RequestMapping requestMapping, RequestCondition<?> customCondition) {
//用@RequestMapping的屬性生成RequestMappingInfo
return RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))//e.g. /test
.methods(requestMapping.method())//e.g. POST
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name())
.customCondition(customCondition)
.options(this.config)
.build();
}
@Override
public RequestMappingInfo build() {
ContentNegotiationManager manager = this.options.getContentNegotiationManager();
//生成路徑匹配類,DispatcherServlet中分發url請求時調用
PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(),
this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
this.options.getFileExtensions());
return new RequestMappingInfo(this.mappingName, patternsCondition,
new RequestMethodsRequestCondition(methods),
new ParamsRequestCondition(this.params),
new HeadersRequestCondition(this.headers),
new ConsumesRequestCondition(this.consumes, this.headers),
new ProducesRequestCondition(this.produces, this.headers, manager),
this.customCondition);
}
4.RequestMappingInfo與handler注冊
handler最終會被封裝成HandlerMethod
,
RequestMappingInfo與HandlerMethod都注冊到org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry
中,MappingRegistry有兩個屬性,Map<RequestMappingInfo, HandlerMethod>
和Map<url, HandlerMethod>
,維護了路徑和HandlerMethod的關系。注冊@Controller即生成這兩個Map。
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
//通過Bean和Method,抽象成可執行的HandlerMethod,即Controller的帶有@RequestMapping注解的Method
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
//注冊Map<RequestMappingInfo, HandlerMethod>
this.mappingLookup.put(mapping, handlerMethod);
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
//注冊Map<url, HandlerMethod>
this.urlLookup.add(url, mapping);
}
//http跨域配置
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
//注冊Map<RequestMappingInfo, MappingRegistration<RequestMappingInfo>>
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
} finally {
this.readWriteLock.writeLock().unlock();
}
}
5.DispatcherServlet與MappingRegistry
這里順帶提一下DispatcherServlet如何找到處理當前Http Request的HandlerMethod,最終http請求由匹配到的HandlerMethod來處理。
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<Match>();
//從Map<url, HandlerMethod>中找
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
//從所有的RequestMappingInfo中找
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
//本質為Comparator<RequestMappingInfo>
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
Collections.sort(matches, comparator);
//選出最匹配當前Request的RequestMappingInfo
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
//校驗
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
}
}
//處理url上的template variables, matrix variables
handleMatch(bestMatch.mapping, lookupPath, request);
//拿到handlerMethod
return bestMatch.handlerMethod;
} else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}