Spring GetMapping注解無效 原因分析和解決

筆記簡述
本學習筆記是由GetMapping注解無效這個問題引起的,最后發現是自己的xml配置錯誤
調試源碼最后發現了spring早已經廢棄了默認的URL獲取方法,而是采用了最新的方法去實現
并記錄下新的URL的獲取以及映射的一些細節和過程,最好可以和Spring MVC URL映射 學習(上) Spring MVC URL映射 學習(下)再結合源碼調試了解其中的細節

學習spring的時候,學習GetMapping注解,了解到他是在spring4.3加的新注解,整合了@RequestMapping(method = RequestMethod.GET),讓代碼能夠更加簡潔。

如下圖圈住的代碼,從含義來說是一模一樣的,可是在實踐的時候,GetMapping卻不能傳遞value值,不知道自己到底哪一步錯了。記錄在這里,等著明確知道答案了,再補充。

image

查看源碼打斷點調試發現如下信息

在獲取注解信息的時候,GetMapping的屬性


image

可是在匹配到RequestMapping的時候,只有method信息,并不包含value信息


image

導致了從GetMapping注解上就沒法獲取到URL信息,從而就出現了注冊handler的時候,URL信息不全,最后的handlerMap信息如下圖


image

也就導致了GetMapping注解無效的情況,但是還是沒有發現其原因

2018年03月17日00:17:08 更新 已經發現了問題所在并解決了
先說解決方案,在xml文件中加入<mvc:annotation-driven />,就可以正常使用GetMapping注解了

無效的原因

在起初使用GetMapping的時候,查看源碼發現是由DefaultAnnotationHandlerMapping類調用實現的,而在文檔中明確說明了@deprecated as of Spring 3.2,意味著從spring3.2開始就不再推薦使用該類了,而與此同時GetMapping是從spring4.3才加入的產物,那么必然存在著使用了GetMapping的同時又使用老的類完成URL屬性獲取操作的問題。

直接的問題就是使用了GetMapping之后,參數無法重新拷貝到RequestMapping中,從而使得數據丟失。

AnnotationUtils 類

static <A extends Annotation> A synthesizeAnnotation(A annotation, Object annotatedElement) {
    if (annotation == null) {
        return null;
    }
    if (annotation instanceof SynthesizedAnnotation) {
        return annotation;
    }

    Class<? extends Annotation> annotationType = annotation.annotationType();
    if (!isSynthesizable(annotationType)) {
        return annotation;
    }

    DefaultAnnotationAttributeExtractor attributeExtractor =
            new DefaultAnnotationAttributeExtractor(annotation, annotatedElement);
    InvocationHandler handler = new SynthesizedAnnotationInvocationHandler(attributeExtractor);
    
    Class<?>[] exposedInterfaces = new Class<?>[] {annotationType, SynthesizedAnnotation.class};
    return (A) Proxy.newProxyInstance(annotation.getClass().getClassLoader(), exposedInterfaces, handler);
}

public static <A extends Annotation> A synthesizeAnnotation(Map<String, Object> attributes,
        Class<A> annotationType, AnnotatedElement annotatedElement) {

    Assert.notNull(annotationType, "'annotationType' must not be null");
    if (attributes == null) {
        return null;
    }

    MapAnnotationAttributeExtractor attributeExtractor =
            new MapAnnotationAttributeExtractor(attributes, annotationType, annotatedElement);
    InvocationHandler handler = new SynthesizedAnnotationInvocationHandler(attributeExtractor);
    Class<?>[] exposedInterfaces = (canExposeSynthesizedMarker(annotationType) ?
            new Class<?>[] {annotationType, SynthesizedAnnotation.class} : new Class<?>[] {annotationType});
    return (A) Proxy.newProxyInstance(annotationType.getClassLoader(), exposedInterfaces, handler);
}

細看上面兩個函數,方法參數中一個帶著屬性字段,另一個沒有,其實這兩個函數就是新舊的生成RequestMapping注解的方法,其中帶有屬性字段的是新的執行函數,傳遞著GetMapping注解的屬性,確保數據的連貫性。

源碼分析

URL映射 獲取

接下來就來學習下xml配置<mvc:annotation-driven />的執行過程,老套路直接定位到AnnotationDrivenBeanDefinitionParser類(PS:如果這點存在疑問可以看看[dfgdfg](fff

如果查看這個類的parse過程,大概可以發現就是注冊和添加了RequestMappingHandlerMapping、RequestMappingHandlerAdapter 適配器等操作,以及額外的cors、aop等操作。

執行RequestMappingHandlerMapping的afterPropertiesSet去實現實例化的步驟中完成對URL屬性的讀取和拼接、存儲的過程

image

如圖,最后RequestMappingHandlerMapping實例化完成后,就去執行了initHandlerMethod的方法,和spring mvc獲取URL信息一樣的操作,遍歷所有的類,得到每個類的方法,再解析每個可行的方法的注解。

最后執行到了RequestMappingHandlerMapping類的getMappingForMethod方法

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;
}

private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
    RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
    // 獲取RequestMapping注解信息
    RequestCondition<?> condition = (element instanceof Class ?
            getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
    return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}

上述代碼中對每個handlerType都進行了createRequestMappingInfo處理,我感覺沒必要啊,畢竟是屬于類層級的,類的注解信息獲取一次就好了,然后和各類自身的方法合并即可,大不了加入緩存也行,而不是每次都實際解析操作,這點感覺怪怪的

AnnotatedElementUtils 類

public static <A extends Annotation> A findMergedAnnotation(AnnotatedElement element, Class<A> annotationType) {
    if (!(element instanceof Class)) {
        // 如果元素不是類
        A annotation = element.getAnnotation(annotationType);
        // 直接獲取期望類型的注解
        if (annotation != null) {
           // 如果存在,就同步下,返回注解信息
            return AnnotationUtils.synthesizeAnnotation(annotation, element);
        }
    }
    AnnotationAttributes attributes = findMergedAnnotationAttributes(element, annotationType, false, false);
   // 其他情況,先發現注解可能有用的屬性信息
    return AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element);
}

獲取GetMapping的屬性得到的數據


image

獲得了方法的注解信息得到的URL信息


image

合并處理函數和方法的URL信息得到的完整的URL信息


image

URL處理

  • 先獲取方法的URL信息,如果有了再獲取類的URL信息,進行合并操作
  • 如果方法沒有有效的URL信息,則直接返回null
  • 如果類沒有URL信息,則返回方法的URL信息

最后實例化完成,該bean中的mappingRegistry存儲的URL信息,然后該數據成功的在initHandlerMappings完成賦值到dispatchservice中,并且包含了適配器的賦值

image

URL信息合并

類URL屬性和方法URL信息如何拼接成完整的URL信息

public RequestMappingInfo combine(RequestMappingInfo other) {
    String name = combineNames(other);
    // 此name會組合成類的簡寫名稱+方法名稱
    PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
    // 這就是URL信息拼接最關鍵的地方
 .....
}

public PatternsRequestCondition combine(PatternsRequestCondition other) {
    Set<String> result = new LinkedHashSet<String>();
    if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) {
        for (String pattern1 : this.patterns) {
            for (String pattern2 : other.patterns) {
                result.add(this.pathMatcher.combine(pattern1, pattern2));
                // 類的URL信息和方法URL信息拼接
            }
        }
    }
    else if (!this.patterns.isEmpty()) {
        result.addAll(this.patterns);
        // 只有類的URL信息
    }
    else if (!other.patterns.isEmpty()) {
        result.addAll(other.patterns);
        // 只有方法的URL信息
    }
    else {
        result.add("");
    }
    return new PatternsRequestCondition(result, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch,
            this.useTrailingSlashMatch, this.fileExtensions);
}

URL映射 處理

Spring MVC URL映射 學習(下)描述的不同的是,在執行getHandlerInternal方法是,進入了AbstractHandlerMethodMapping類中,而不是之前說的AbstractUrlHandlerMapping類,這就是因為具體的RequestMapping的不同而跳轉到不同的子類執行而已。

AbstractHandlerMethodMapping 類

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    // 獲取請求的URL信息
    this.mappingRegistry.acquireReadLock();
    try {
        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
        // 查找到合適的執行方法
        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }
    finally {
        this.mappingRegistry.releaseReadLock();
    }
}

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
    List<Match> matches = new ArrayList<Match>();
    List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
    // 從urlLookUp集合中完全匹配URL信息
    if (directPathMatches != null) {
       // 對篩選的全局匹配的URL屬性進行匹配操作
        addMatchingMappings(directPathMatches, matches, request);
    }
    if (matches.isEmpty()) {
        // 把所有的URL信息添加到需要對比的集合中,進行匹配操作
        // 注意,這里面是個很耗時的操作
        addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
    }

    if (!matches.isEmpty()) {
        Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
        Collections.sort(matches, comparator);
        // 對匹配到的URL集合進行排序,意味著相似的URL會被排到一起
        
        Match bestMatch = matches.get(0);
        if (matches.size() > 1) {
            if (CorsUtils.isPreFlightRequest(request)) {
               // 符合 CORS pre-flight的請求,就返回一個EmptyHandler對象,
               // 同時會拋出UnsupportedOperationException異常
                return PREFLIGHT_AMBIGUOUS_MATCH;
            }
            Match secondBestMatch = matches.get(1);
            if (comparator.compare(bestMatch, secondBestMatch) == 0) {
               // 存在兩個同等層級的URL匹配信息,然后spring就懵逼了,不知道選擇哪個了
               // 拋出IllegalStateException異常
               // 這點可以寫一個demo確實驗證下
                Method m1 = bestMatch.handlerMethod.getMethod();
                Method m2 = secondBestMatch.handlerMethod.getMethod();
                throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
                        request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
            }
        }
        handleMatch(bestMatch.mapping, lookupPath, request);
        return bestMatch.handlerMethod;
    }
    else {
        return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
    }
}

private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
    for (T mapping : mappings) {
        T match = getMatchingMapping(mapping, request);
        // 其實這個時候的mapping是RequestMappingInfo對象(一般情況)
        // 匹配出合適的URL信息
        if (match != null) {
            matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
        }
    }
}

RequestMappingInfo 類

public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
    RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
    // 匹配方法名稱,
    ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
    // 匹配參數
    HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
    // 匹配頭部信息
    ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
    // 匹配處理請求的類型,也就是Content-Type
    ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
    // 匹配相應請求的類型,從request的Accept參數中獲取

    if (methods == null || params == null || headers == null || consumes == null || produces == null) {
       // 有一個沒有匹配上就認為沒有合適的映射對象
        return null;
    }

    PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
    // 使用了Apache Ant的匹配規則去匹配path
    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());
}

關于上述的Apache Ant 在spring mvc的具體匹配是AntPathMatcher 類的 doMatch 方法

URL匹配總結

URL匹配流程圖

image
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,249評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,228評論 2 375