Spring 系列(三):你真的懂@RequestMapping嗎?

七分熟pizza發表於2019-04-24

1.前言

上篇給大家介紹了Spring MVC父子容器的概念,主要提到的知識點是:

Spring MVC容器是Spring容器的子容器,當在Spring MVC容器中找不到bean的時候就去父容器找.
複製程式碼

在文章最後我也給大家也留了一個問題,既然子容器找不到就去父容器找,那乾脆把bean定義都放在父容器不就行了?是這樣嗎,我們做個實驗。

我們把<context:component-scan base-package="xx.xx.xx"/> 這條語句從spring-mvc.xml檔案中挪到spring.xml中,重啟應用。會發現報404,如下圖:

Spring 系列(三):你真的懂@RequestMapping嗎?

404說明請求的資源沒有找到,為什麼呢?

使用Spring MVC的同學一般都會以下方式定義請求地址:

@Controller
@RequestMapping("/test")
public class Test {
   @RequestMapping(value="/handle", method=RequestMethod.POST)
   public void handle();
}
複製程式碼

@Controller註解用來把一個類定義為Controller。

@RequestMapping註解用來把web請求對映到相應的處理函式。

@Controller和@RequestMapping結合起來完成了Spring MVC請求的派發流程。

為什麼兩個簡單的註解就能完成這麼複雜的功能呢?又和<context:component-scan base-package="xx.xx.xx"/>的位置有什麼關係呢?

讓我們開始分析原始碼。

2.@RequestMapping流程分析

@RequestMapping流程可以分為下面6步:

  • 1.註冊RequestMappingHandlerMapping bean 。
  • 2.例項化RequestMappingHandlerMapping bean。
  • 3.獲取RequestMappingHandlerMapping bean例項。
  • 4.接收requst請求。
  • 5.在RequestMappingHandlerMapping例項中查詢對應的handler。
  • 6.handler處理請求。

為什麼是這6步,我們展開分析。

2.1 註冊RequestMappingHandlerMapping bean

第一步還是先找程式入口。

使用Spring MVC的同學都知道,要想使@RequestMapping註解生效,必須得在xml配置檔案中配置< mvc:annotation-driven/>。因此我們以此為突破口開始分析。

Spring系列(一):bean 解析、註冊、例項化 文中我們知道xml配置檔案解析完的下一步就是解析bean。在這裡我們繼續對那個方法展開分析。如下:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
   //如果該元素屬於預設名稱空間走此邏輯。Spring的預設namespace為:http://www.springframework.org/schema/beans“
   if (delegate.isDefaultNamespace(root)) {
      NodeList nl = root.getChildNodes();
      for (int i = 0; i < nl.getLength(); i++) {
         Node node = nl.item(i);
         if (node instanceof Element) {
            Element ele = (Element) node;
            //對document中的每個元素都判斷其所屬名稱空間,然後走相應的解析邏輯
            if (delegate.isDefaultNamespace(ele)) {
               parseDefaultElement(ele, delegate);
            }
            else {
              //如果該元素屬於自定義namespace走此邏輯 ,比如AOP,MVC等。
               delegate.parseCustomElement(ele);
            }
         }
      }
   }
   else {
      //如果該元素屬於自定義namespace走此邏輯 ,比如AOP,MVC等。
      delegate.parseCustomElement(root);
   }
}
複製程式碼

方法中根據元素的名稱空間來進行不同的邏輯處理,如bean、beans等屬於預設名稱空間執行parseDefaultElement()方法,其它名稱空間執行parseCustomElement()方法。

< mvc:annotation-driven/>元素屬於mvc名稱空間,因此進入到 parseCustomElement()方法。

public BeanDefinition parseCustomElement(Element ele) {
    //解析自定義元素
    return parseCustomElement(ele, null);
}
複製程式碼

進入parseCustomElement(ele, null)方法。

public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
    //獲取該元素namespace url
    String namespaceUri = getNamespaceURI(ele);
    //得到NamespaceHandlerSupport實現類解析元素
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
複製程式碼

進入NamespaceHandlerSupport類的parse()方法。

@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
    //此處得到AnnotationDrivenBeanDefinitionParser類來解析該元素
    return findParserForElement(element, parserContext).parse(element, parserContext);
}
複製程式碼

上面方法分為兩步,(1)獲取元素的解析類。(2)解析元素。

(1)獲取解析類。

private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    String localName = parserContext.getDelegate().getLocalName(element);
    BeanDefinitionParser parser = this.parsers.get(localName);
    return parser;
}
複製程式碼

Spring MVC中含有多種名稱空間,此方法會根據元素所屬名稱空間得到相應解析類,其中< mvc:annotation-driven/>對應的是AnnotationDrivenBeanDefinitionParser解析類。

(2)解析< mvc:annotation-driven/>元素

進入AnnotationDrivenBeanDefinitionParser類的parse()方法。

@Override
public BeanDefinition parse(Element element, ParserContext context) {
    Object source = context.extractSource(element);
    XmlReaderContext readerContext = context.getReaderContext();
    //生成RequestMappingHandlerMapping bean資訊
    RootBeanDefinition handlerMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);
    handlerMappingDef.setSource(source);
    handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    handlerMappingDef.getPropertyValues().add("order", 0);
    handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
    //此處HANDLER_MAPPING_BEAN_NAME值為:RequestMappingHandlerMapping類名
    //容器中註冊name為RequestMappingHandlerMapping類名
    context.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME));
}
複製程式碼

可以看到上面方法在Spring MVC容器中註冊了一個名為“HANDLER_MAPPING_BEAN_NAME”,型別為RequestMappingHandlerMapping的bean。

至於這個bean能幹嗎,繼續往下分析。

2.2. RequestMappingHandlerMapping bean例項化

bean註冊完後的下一步就是例項化。

在開始分析例項化流程之前,我們先介紹一下RequestMappingHandlerMapping是個什麼樣類。

2.2.1 RequestMappingHandlerMapping繼承圖

Spring 系列(三):你真的懂@RequestMapping嗎?

上圖資訊比較多,我們查詢關鍵資訊。可以看到這個類間接實現了HandlerMapping介面,是HandlerMapping型別的例項。

除此之外還實現了ApplicationContextAware和IntitalzingBean 這兩個介面。

在這裡簡要介紹一下這兩個介面:

2.2.2 ApplicationContextAware介面

下面是官方介紹

public interface ApplicationContextAware extends Aware

Interface to be implemented by any object that wishes to be notified of the ApplicationContext that it runs in.
複製程式碼

該介面只包含以下方法:

void setApplicationContext(ApplicationContext applicationContext)
throws BeansException

Set the ApplicationContext that this object runs in. Normally this call will be used to initialize the object.
複製程式碼

概括一下上面表達的資訊:如果一個類實現了ApplicationContextAware介面,Spring容器在初始化該類時候會自動回撥該類的setApplicationContext()方法。這個介面主要用來讓實現類得到Spring 容器上下文資訊。

2.2.3 IntitalzingBean介面

下面是官方介紹

public interface InitializingBean

Interface to be implemented by beans that need to react once all their properties have been set by a BeanFactory: e.g. to perform custom initialization, or merely to check that all mandatory properties have been set.

複製程式碼

該介面只包含以下方法:

void afterPropertiesSet() throws Exception

Invoked by the containing BeanFactory after it has set all bean properties and satisfied BeanFactoryAware, ApplicationContextAware etc.
複製程式碼

概括一下上面表達的資訊:如果一個bean實現了該介面,Spring 容器初始化bean時會回撥afterPropertiesSet()方法。這個介面的主要作用是讓bean在初始化時可以實現一些自定義的操作。

介紹完RequestMappingHandlerMapping類後我們開始對這個類的原始碼進行分析。

2.2.2.4 RequestMappingHandlerMapping類原始碼分析

既然RequestMappingHandlerMapping實現了ApplicationContextAware介面,那例項化時候肯定會執行setApplicationContext方法,我們檢視其實現邏輯。

@Override
public final void setApplicationContext(ApplicationContext context) throws BeansException {
   if (this.applicationContext == null) {
  	this.applicationContext = context;
   }
}
複製程式碼

可以看到此方法把容器上下文賦值給applicationContext變數,因為現在是Spring MVC容器建立流程,因此此處設定的值就是Spring MVC容器 。

RequestMappingHandlerMapping也實現了InitializingBean介面,當設定完屬性後肯定會回撥afterPropertiesSet方法,再看afterPropertiesSet方法邏輯。

@Override
public void afterPropertiesSet() 
   super.afterPropertiesSet();
}
複製程式碼

上面呼叫了父類的afterPropertiesSet()方法,沿呼叫棧繼續檢視。

@Override
public void afterPropertiesSet() {
	//初始化handler函式
   initHandlerMethods();
}
複製程式碼

進入initHandlerMethods初始化方法檢視邏輯。

protected void initHandlerMethods() {
    //1.獲取容器中所有bean 的name。
    //根據detectHandlerMethodsInAncestorContexts bool變數的值判斷是否獲取父容器中的bean,預設為false。因此這裡只獲取Spring MVC容器中的bean,不去查詢父容器
    String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
         BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
         getApplicationContext().getBeanNamesForType(Object.class));
    //迴圈遍歷bean
    for (String beanName : beanNames) {
	//2.判斷bean是否含有@Controller或者@RequestMappin註解
        if (beanType != null && isHandler(beanType)) {
            //3.對含有註解的bean進行處理,獲取handler函式資訊。
              detectHandlerMethods(beanName);
      }
}
複製程式碼

上面函式分為3步。

(1)獲取Spring MVC容器中的bean。

(2)找出含有含有@Controller或者@RequestMappin註解的bean。

(3)對含有註解的bean進行解析。

第1步很簡單就是獲取容器中所有的bean name,我們對第2、3步作分析。

檢視isHandler()方法實現邏輯。

@Override
protected boolean isHandler(Class<?> beanType) {
   return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
         AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
複製程式碼

上面邏輯很簡單,就是判斷該bean是否有@Controller或@RequestMapping註解,然後返回判斷結果。

如果含有這兩個註解之一就進入detectHandlerMethods()方法進行處理。

檢視detectHandlerMethods()方法。

protected void detectHandlerMethods(final Object handler) {
    //1.獲取bean的類資訊
    Class<?> handlerType = (handler instanceof String ?
         getApplicationContext().getType((String) handler) : handler.getClass());
    final Class<?> userType = ClassUtils.getUserClass(handlerType);
    //2.遍歷函式獲取有@RequestMapping註解的函式資訊
   Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
         new MethodIntrospector.MetadataLookup<T>() {
            @Override
            public T inspect(Method method) {
               try {
                //如果有@RequestMapping註解,則獲取函式對映資訊
                return getMappingForMethod(method, userType);
               }
         });
    //3.遍歷對映函式列表,註冊handler
    for (Map.Entry<Method, T> entry : methods.entrySet()) {
      Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
      T mapping = entry.getValue();
      //註冊handler函式
      registerHandlerMethod(handler, invocableMethod, mapping);
   }
}
複製程式碼

上面方法中用了幾個回撥,可能看起來比較複雜,其主要功能就是獲取該bean和父介面中所有用@RequestMapping註解的函式資訊,並把這些儲存到methodMap變數中。

我們對上面方法進行逐步分析,看看如何對有@RequestMapping註解的函式進行解析。

先進入selectMethods()方法檢視實現邏輯。

public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
   final Map<Method, T> methodMap = new LinkedHashMap<Method, T>();
   Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>();
   Class<?> specificHandlerType = null;
    //把自身類新增到handlerTypes中
    if (!Proxy.isProxyClass(targetType)) {
        handlerTypes.add(targetType);
        specificHandlerType = targetType;
    }
    //獲取該bean所有的介面,並新增到handlerTypes中
    handlerTypes.addAll(Arrays.asList(targetType.getInterfaces()));
    //對自己及所有實現介面類進行遍歷
   for (Class<?> currentHandlerType : handlerTypes) {
      final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
      //獲取函式對映資訊
      ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() {
	    //迴圈獲取類中的每個函式,通過回撥處理
            @Override
            public void doWith(Method method) {
            //對類中的每個函式進行處理
            Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
            //回撥inspect()方法給個函式生成RequestMappingInfo  
            T result = metadataLookup.inspect(specificMethod);
            if (result != null) {
                //將生成的RequestMappingInfo儲存到methodMap中
                methodMap.put(specificMethod, result);
            }
         }
      }, ReflectionUtils.USER_DECLARED_METHODS);
   }
    //返回儲存函式對映資訊後的methodMap
    return methodMap;
}
複製程式碼

上面邏輯中doWith()回撥了inspect(),inspect()又回撥了getMappingForMethod()方法。

我們看看getMappingForMethod()是如何生成函式資訊的。

protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
    //建立函式資訊
    RequestMappingInfo info = createRequestMappingInfo(method);
    return info;
}
複製程式碼

檢視createRequestMappingInfo()方法。

private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
    //如果該函式含有@RequestMapping註解,則根據其註解資訊生成RequestMapping例項,
    //如果該函式沒有@RequestMapping註解則返回空
    RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
    //如果requestMapping不為空,則生成函式資訊MAP後返回
    return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
複製程式碼

看看createRequestMappingInfo是如何實現的。

protected RequestMappingInfo createRequestMappingInfo(
      RequestMapping requestMapping, RequestCondition<?> customCondition) {
         return RequestMappingInfo
         .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
         .methods(requestMapping.method())
         .params(requestMapping.params())
         .headers(requestMapping.headers())
         .consumes(requestMapping.consumes())
         .produces(requestMapping.produces())
         .mappingName(requestMapping.name())
         .customCondition(customCondition)
         .options(this.config)
         .build();
}
複製程式碼

可以看到上面把RequestMapping註解中的資訊都放到一個RequestMappingInfo例項中後返回。

當生成含有@RequestMapping註解的函式對映資訊後,最後一步是呼叫registerHandlerMethod 註冊handler和處理函式對映關係。

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
   this.mappingRegistry.register(mapping, handler, method);
}
複製程式碼

看到把所有的handler方法都註冊到了mappingRegistry這個變數中。

到此就把RequestMappingHandlerMapping bean的例項化流程就分析完了。

2.3 獲取RequestMapping bean

這裡我們回到Spring MVC容器初始化流程,檢視initWebApplicationContext方法。

protected WebApplicationContext initWebApplicationContext() {
    //1.獲得rootWebApplicationContext
    WebApplicationContext rootContext =
        WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
    if (wac == null) {
        //2.建立 Spring 容器 
        wac = createWebApplicationContext(rootContext);
    }
    //3.初始化容器
    onRefresh(wac);
    return wac;
}
複製程式碼

前兩步我們在Spring 系列(二):Spring MVC的父子容器一文中分析過,主要是建立Spring MVC容器,這裡我們重點看第3步。

進入onRefresh()方法。

@Override
protected void onRefresh(ApplicationContext context) {
    //執行初始化策略 
    initStrategies(context);
}
複製程式碼

進入initStrategies方法,該方法進行了很多初始化行為,為減少干擾我們只過濾出與本文相關內容。

protected void initStrategies(ApplicationContext context) {
   //初始化HandlerMapping
   initHandlerMappings(context);
}
複製程式碼

進入initHandlerMappings()方法。

private void initHandlerMappings(ApplicationContext context) {
    //容器中查詢name為"ANDLER_MAPPING_BEAN_NAME"的例項
    HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
    //把找到的bean放到hanlderMappings中。
    this.handlerMappings = Collections.singletonList(hm);
}
複製程式碼

此處我們看到從容器中獲取了name為 “HANDLER_MAPPING_BEAN_NAME”的bean,這個bean大家應該還記得吧,就是前面註冊並例項化了的RequestMappingHandlerMapping bean。

2.4 接收請求

DispatchServlet繼承自Servlet,那所有的請求都會在service()方法中進行處理。

檢視service()方法。

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) {
    //獲取請求方法
    HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    //若是patch請求執行此邏輯
    if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
        processRequest(request, response);
   }
    //其它請求走此邏輯
   else {
      super.service(request, response);
   }
}
複製程式碼

我們以get、post請求舉例分析。檢視父類service方法實現。

protected void service(HttpServletRequest req, HttpServletResponse resp){
    String method = req.getMethod();
    if (method.equals(METHOD_GET)) {
        //處理get請求
        doGet(req, resp);
    } else if (method.equals(METHOD_POST)) {
        //處理post請求
        doPost(req, resp)
    }
} 
複製程式碼

檢視doGet()、doPost()方法實現。

@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response){
    processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response {
    processRequest(request, response);
}
複製程式碼

可以看到都呼叫了processRequest()方法,繼續跟蹤。

protected final void processRequest(HttpServletRequest request, HttpServletResponse response){
    //處理請求
    doService(request, response);
}
複製程式碼

檢視doService()方法。

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) {
    //處理請求
    doDispatch(request, response);
}
複製程式碼

2.5 獲取handler

最終所有的web請求都由doDispatch()方法進行處理,檢視其邏輯。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    HttpServletRequest processedRequest = request;
    // 根據請求獲得真正處理的handler
    mappedHandler = getHandler(processedRequest);
    //用得到的handler處理請求,此處省略
	。。。。
}
複製程式碼

檢視getHandler()。

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    //獲取HandlerMapping例項
    for (HandlerMapping hm : this.handlerMappings) {
        //得到處理請求的handler
        HandlerExecutionChain handler = hm.getHandler(request);
        if (handler != null) {
            return handler;
        }
   }
   return null;
}
複製程式碼

這裡遍歷handlerMappings獲得所有HandlerMapping例項,還記得handlerMappings變數吧,這就是前面initHandlerMappings()方法中設定進去的值。

可以看到接下來調了用HandlerMapping例項的getHanlder()方法查詢handler,看其實現邏輯。

@Override
public final HandlerExecutionChain getHandler(HttpServletRequest request) {
    Object handler = getHandlerInternal(request);
}
複製程式碼

進入getHandlerInternal()方法。

@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) {
    //獲取函式url
    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    //查詢HandlerMethod 
    handlerMethod = lookupHandlerMethod(lookupPath, request);
}
複製程式碼

進入lookupHandlerMethod()。

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) {
    this.mappingRegistry.getMappingsByUrl(lookupPath);
}
複製程式碼

可以看到上面方法中從mappingRegistry獲取handler,這個mappingRegistry的值還記得是從哪裡來的嗎?

就是前面RequestMappingHandlerMapping 例項化過程的最後一步呼叫registerHandlerMethod()函式時設定進去的。

2.6 handler處理請求

獲取到相應的handler後剩下的事情就是進行業務邏輯。處理後返回結果,這裡基本也沒什麼好說的。

到此整個@RequestMapping的流程也分析完畢。

3.小結

認真讀完上面深入分析@RequestMapping註解流程的同學,相信此時肯定對Spring MVC有了更深一步的認識。

現在回到文章開頭的那個問題,為什麼把<context:component-scan base-package="xx.xx.xx"/>挪到spring.xml檔案中後就會404了呢?

我想看明白此文章的同學肯定已經知道答案了。

答案是:

當把<context:component-scan base-package="xx.xx.xx"/>寫到spring.xml中時,所有的bean其實都例項化在了Spring父容器中。

但是在@ReqestMapping解析過程中,initHandlerMethods()函式只是對Spring MVC 容器中的bean進行處理的,並沒有去查詢父容器的bean。因此不會對父容器中含有@RequestMapping註解的函式進行處理,更不會生成相應的handler。

所以當請求過來時找不到處理的handler,導致404。

4.尾聲

從上面的分析中,我們知道要使用@RequestMapping註解,必須得把含有@RequestMapping的bean定義到spring-mvc.xml中。

這裡也給大家個建議:

因為@RequestMapping一般會和@Controller搭配使。為了防止重複註冊bean,建議在spring-mvc.xml配置檔案中只掃描含有Controller bean的包,其它的共用bean的註冊定義到spring.xml檔案中。寫法如下:

spring-mvc.xml

<!-- 只掃描@Controller註解 -->
<context:component-scan base-package="com.xxx.controller" use-default-filters="false"
 >
    <context:include-filter type="annotation"
        expression="org.springframework.stereotype.Controller" />
</context:component-scan>
複製程式碼

spring.xml

<!-- 配置掃描註解,不掃描@Controller註解 -->
<context:component-scan base-package="com.xxx">
    <context:exclude-filter type="annotation"
        expression="org.springframework.stereotype.Controller" />
</context:component-scan>
複製程式碼

use-default-filters屬性預設為true,會掃描所有註解型別的bean 。如果配置成false,就只掃描白名單中定義的bean註解。

如果想獲得更多,歡迎關注公眾號:七分熟pizza

公眾號裡我會分享更多技術以及職場方面的經驗,大家有什麼問題也可以直接在公眾號向我提問交流。

Spring 系列(三):你真的懂@RequestMapping嗎?

相關文章