Spring Mvc 源碼解析(三)— HandlerMapping初始化

image

寫在最前面

上一篇文章咱們簡單介紹了,servlet容器以及Spring Mvc 應用容器的初始化過程。並瞭解如何經過java代碼,來進行容器的初始化配置。在源碼解析(一)中咱們提到當servlet container接收到一個請求時,servlet container 會根據servlet的mapping 配置選取一個 servlet 進行請求處理,並返回 response瞭解更多servlet container 容器。咱們在實際開發的時候常常會寫不少 controller ,本文將詳細介紹handerMapping的初始化過程,以及DispatcherServlet 是如何根據Request 獲取對應的handlerMapping的。html

1、Spring Mvc初始化過程

首先咱們先來明確幾個概念:java

  • Handler 一般指用於處理request請求的實際對象,能夠類比 XxxController。在Spring Mvc中並無具體的類叫 Handler。
  • HandlerMethod 處理request請求的具體方法對應 Controller 層中的某一個註解了@RequestMappping的方法。org.springframework.web.method.HandlerMethod 該類中保存了執行某一個 request 請求的方法,以及相應的類信息。
  • RequestMappingInfo 當Spring Mvc處理請求的時候經過 Request的 method、header、參數等來匹配對應的RequestMappingInfoRequestMappingInfo一般保存的是@RequestMaping註解的信息、用來肯定每個請求具體由哪個Controller方法處理。

1.1 初始化調用鏈

Spring Mvc 的初始化調用過程以下圖:git

image

1.二、RequestMappingHandlerMapping、AbstractHandlerMethodMapping

RequestMappingHandlerMapping 實現了InitalizingBean接口熟悉Spring 容器的同窗應該知道Spring容器在啓動的時候會執行InitalizingBean.afterPropertiesSet()方法。RequestMappingHandlerMapping實現了這個方法,這裏也是Spring Mvc初始化的入口。github

1.3 正真的初始化方法initHandlerMethods()

直接看源碼,方法的邏輯很簡單配合註釋很容易理解web

/** * Scan beans in the ApplicationContext, detect and register handler methods. * @see #isHandler(Class) * @see #getMappingForMethod(Method, Class) * @see #handlerMethodsInitialized(Map) */
	protected void initHandlerMethods() {
		if (logger.isDebugEnabled()) {
			logger.debug("Looking for request mappings in application context: " + getApplicationContext());
		}
		//獲取全部容器託管的 beanName
		String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
				getApplicationContext().getBeanNamesForType(Object.class));

		for (String beanName : beanNames) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				Class<?> beanType = null;
				try {
				    //獲取 Class 信息
					beanType = getApplicationContext().getType(beanName);
				}
				catch (Throwable ex) {
					// An unresolvable bean type, probably from a lazy bean - let's ignore it.
					if (logger.isDebugEnabled()) {
						logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
					}
				}
				// isHandler()方法判斷這個類是否有 @RequestMapping 或 @Controller
				if (beanType != null && isHandler(beanType)) {
				    //發現並註冊 Controller @RequestMapping方法
					detectHandlerMethods(beanName);
				}
			}
		}
		//Spring Mvc框架沒有實現、能夠用於功能擴展。
		handlerMethodsInitialized(getHandlerMethods());
	}
複製代碼

1.4從handler中獲取HandlerMethod

/**
	 * 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) {
		Class<?> handlerType = (handler instanceof String ?
				getApplicationContext().getType((String) handler) : handler.getClass());
		final Class<?> userType = ClassUtils.getUserClass(handlerType);

		Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
				new MethodIntrospector.MetadataLookup<T>() {
					@Override
					public T inspect(Method method) {
						try {
						    //獲取 RequestMappingInfo
							return getMappingForMethod(method, userType);
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
					}
				});

		if (logger.isDebugEnabled()) {
			logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
		}
		for (Map.Entry<Method, T> entry : methods.entrySet()) {
			Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
			T mapping = entry.getValue();
			//註冊RequestMappingInfo
			registerHandlerMethod(handler, invocableMethod, mapping);
		}
	}
複製代碼

1.五、建立RequestMappingInfo

這一步會獲取方法和類上的@RequestMapping註解並經過 @RequestMaping註解配置的參數生成相應的RequestMappingInfoRequestMappingInfo中保存了不少Request須要匹配的參數。spring

一、匹配請求Url PatternsRequestCondition patternsCondition;json

二、匹配請求方法 GET等 RequestMethodsRequestCondition methodsCondition;api

三、匹配參數例如@Requestmaping(Params="action=DoXxx") ParamsRequestCondition paramsCondition;bash

四、匹配請求頭信息 HeadersRequestCondition headersCondition;mvc

五、指定處理請求的提交內容類型(Content-Type),例如application/json, text/html; ConsumesRequestCondition consumesCondition;

六、指定返回的內容類型,僅當request請求頭中的(Accept)類型中包含該指定類型才返回ProducesRequestCondition producesCondition;

七、用於用戶定製請求條件 RequestConditionHolder customConditionHolder; 舉個例子,當咱們有一個需求只要請求中包含某一個參數時均可以掉這個方法處理,就能夠定製這個匹配條件。

/** * 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
	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);
			}
		}
		return info;
	}
	
	/** * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, * supplying the appropriate custom {@link RequestCondition} depending on whether * the supplied {@code annotatedElement} is a class or method. * @see #getCustomTypeCondition(Class) * @see #getCustomMethodCondition(Method) */
	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);
	}
複製代碼

1.六、註冊RequestMappingInfo

這一步 Spring Mvc 會保存 Map<RequestmappingInfo, HandlerMethod>Map<url, RequestmappingInfo>的映射關係。這裏咱們能夠和源碼分析(一)裏的DispatcherServlet.doDispatch()方法聯繫起來,doDispatch方法經過 url 在Map<url, RequestmappingInfo>中獲取對應的 RequestMappingInfo 再根據request的信息和RequestMappingInfo的各個條件比較是否知足處理條件,若是不知足返回 404 若是知足經過RequestMappingInfoMap<RequestmappingInfo, HandlerMethod>獲取正真處理該請求的方法HandlerMathod. 最後經過反射執行具體的方法(Controller 方法)。

public void register(T mapping, Object handler, Method method) {
			this.readWriteLock.writeLock().lock();
			try {
				HandlerMethod handlerMethod = createHandlerMethod(handler, method);
				assertUniqueMethodMapping(handlerMethod, mapping);

				if (logger.isInfoEnabled()) {
					logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
				}
				//保存RequestMappingInfo 和 HandlerMathod的映射關係
				this.mappingLookup.put(mapping, handlerMethod);

				List<String> directUrls = getDirectUrls(mapping);
				for (String url : directUrls) {
				    //保存 Request Url 和 RequestMappingInfo 的對應關係
					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<T>(mapping, handlerMethod, directUrls, name));
			}
			finally {
				this.readWriteLock.writeLock().unlock();
			}
		}
複製代碼

2、實戰

需求:咱們須要對外提供一個 OpenApi 其餘項目簽名經過後既能夠調用,而不用頻繁每次寫簽名校驗的邏輯,咱們如何利用 Spring mvc 的初始化過程來定製咱們的需求?

2.一、OpenApi 註解

@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ResponseBody
public @interface OpenApi {
    //請求 url
    String value() default "";
    
    //須要參與簽名的請求參數
    String[] params() default {};
    
    //方法
    RequestMethod method() default RequestMethod.GET;
    
    ····
}

複製代碼

2.二、定製RequestMappingHandlerMapping

當獲取到方法上有@OpenApi註解時建立 RequestMappingInfo

/** * @author HODO */
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        //父類的方法調用上文已經分析過
        RequestMappingInfo mappingInfo = super.getMappingForMethod(method, handlerType);
        if (mappingInfo != null) {
            return mappingInfo;
        }
        OpenApi openApi = AnnotatedElementUtils.findMergedAnnotation(method, OpenApi.class);
        return openApi == null ? null : RequestMappingInfo.paths(openApi.value()).methods(openApi.method()).build();
    }
}
複製代碼

2.3 攔截器

攔截器攔截請求,當處理的方法包含 OpenApi 註解的時候進行簽名處理,根據業務自身的需求判斷簽名是否有效。

/** * @author HODO */
public class OpenApiSupportInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = ((HandlerMethod) handler);
            OpenApi openApi = AnnotatedElementUtils.getMergedAnnotation(handlerMethod.getMethod(), OpenApi.class);
            if (openApi != null && !checkSign(request, openApi.params())) {
                response.getWriter().write("簽名失敗");
                return false;
            }
        }
        return true;
    }
    

    private boolean checkSign(HttpServletRequest request, String[] params) {
        
        //get signKey check sign
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
複製代碼

2.四、配置增長攔截器,並替換容器默認初始化的RequestMappingHandlerMapping使用咱們定製的CustomRequestMappingHandlerMapping

@Configuration
@ComponentScan("demo")
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new OpenApiSupportInterceptor());
    }

    @Override
    protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new CustomRequestMappingHandlerMapping();
    }
}
複製代碼

2.四、Controller測試代碼

/** * @author HODO */
@Controller
@RequestMapping
public class DemoController {
    @OpenApi(value = "/api")
    public String demo2() {
        return "test2";
    }
}
複製代碼

3、總結

Spring Mvc的初始化過程比較清晰,整個過程Spring 提供的方法不少都是 protected修飾的這樣咱們能夠經過繼承靈活的定製咱們的需求。 回顧一下整個初始化過程:

  1. 經過Spring容器對 InitalizingBean.afterPropertiesSet()方法的支持開始初始化流程。

  2. 獲取容器中的全部 bean 經過 isHandler()方法區分是否須要處理。

  3. 經過方法上的@RequestMapping註解建立 RequestMappingInfo

  4. 註冊、保存保存 Map<RequestmappingInfo, HandlerMethod>Map<url, RequestmappingInfo>的映射關係方便後續調用和Request匹配。

項目地址:https://github.com/hoodoly/Spring-Mvc

聯繫方式:gunriky@163.com 有問題能夠直接聯繫

相關文章
相關標籤/搜索