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阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352