Spring MVC源码解析(三):读取@Controller@RequestMapping信息

我们平时在使用Spring MVC进行WEB开发的时候都会使用@Controller跟@RequestMapping注解定义控制器实体跟处理请求的方法的,让我们从@Controller跟@ReuqstMapping这两个注解开始就看Spring MVC工作的

从JavaDoc上获取有用的信息

我们先来看一下@Controller的JavaDoc吧,但因为从@Controller的JavaDoc上看不到什么有用的信息这里也就不贴它的代码了.从@Controller的JavaDoc上来可以知道以下信息:

  1. 表明这个类在WEB中是一个Controller
  2. Spring在扫描类的时候会把它按照@Component一样处理
  3. 和@RequestMapping注解配合使用
/**
 * Annotation for mapping web requests onto methods in request-handling classes
 * with flexible method signatures.
 *
 * <p>Both Spring MVC and Spring WebFlux support this annotation through a
 * {@code RequestMappingHandlerMapping} and {@code RequestMappingHandlerAdapter}
 * in their respective modules and package structure. For the exact list of
 * supported handler method arguments and return types in each, please use the
 * reference documentation links below:
 * <ul>
 * <li>Spring MVC
 * <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-arguments">Method Arguments</a>
 * and
 * <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-return-types">Return Values</a>
 * </li>
* </ul>
 * @author Juergen Hoeller
 * @author Arjen Poutsma
 * @author Sam Brannen
 * @since 2.5
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping

从@RequestMapping的JavaDoc可以知道Spring MVC是通过RequestMappingHandlerMapping跟RequestMappingHandlerAdapter来实现对@RequestMapping的支持的,以及对支持什么参数跟什么返回值(这些跟本文无关有兴趣的同学可以点击JavaDoc上的链接地址查看文档)

@RequestMapping跟@Controller的注解信息是如何被读取的

顺着上面的注解信息查看RequestMappingHandlerMapping类的JavaDoc发现一句比较重要的线索Creates RequestMappingInfo instances from type and method-level @RequestMapping annotations in @Controller classes(读取被@Controller标记的类下被@RequestMapping标记的方法的信息封装成@RequestInfo)
从RequestMappingHandlerMapping上找到以下相关方法

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);
            }
            String prefix = getPathPrefix(handlerType);
            if (prefix != null) {
                info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
            }
        }
        return info;
    }
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);
    }
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);
    }

从上面的代码Spring Mvc在读取@RequestMapping的步骤如下:

  1. 先分别从方法跟类上面的@RequestMapping注解信息封装成一个RequestMappingInfo 信息
    1.1 RequestMappingInfo由多个RequestCondition组成,RequestCondition代表一个HttpServletRequest的匹配规则
    1.2 RequestCondition有多种子类用于判断不同的类型,例如RequestMethodsRequestCondition就是判断Http请求的
    1.3 两个RequestMappingInfo整合成一个的时候会把Spring 定义的RequestCondition会合并在一起,正因为这样在类上配置注解设配get并且方法上设置设配post的话这个方法可以处理get,post http请求
    1.4 我们可以通过继承RequestMappingHandlerMapping类并从写getCustomTypeCondition跟getCustomMethodCondition来定制自己需要的匹配规则,在将它注入到Spring ioc时设置@Order注解配置适当的优先顺序让它在RequestMappingHandlerMapping前设配http
  2. 将从方法跟类上读取到RequestMappingInfo整个一个
  3. 判断一个类是否将映射的url添加前缀,可以通过WebMvcConfigurer类的configurePathMatch方法进行配置,例如通过如下配置定义所有被@Wkx注解的Controller类都需要添加/wkx前缀
@Configuration
public class WebConfig implements WebMvcConfigurer
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("wkx", clazz -> clazz.isAnnotationPresent(Wkx.class));
    }
}

RequestMappingHandlerMapping是在什么时候读取@RequestMapping信息

image.png

通过idea的Call Hierarchy的知道RequestMappingHandlerMapping会在其afterPropertiesSet生命周期读取的

public class AbstractHandlerMethodMapping {
    protected void initHandlerMethods() {
        // 1 获取Spring ApplicationContext中所有的装配Bean的名称
        for (String beanName : getCandidateBeanNames()) {
            if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                // 2 对所有Bean读取@RequestMapping信息 
                processCandidateBean(beanName);
            }
        }
        handlerMethodsInitialized(getHandlerMethods());
    }
}

    protected void processCandidateBean(String beanName) {
        Class<?> beanType = null;
        try {
            beanType = obtainApplicationContext().getType(beanName);
        }
        catch (Throwable ex) {
            if (logger.isTraceEnabled()) {
                logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
            }
        }
        // 3 对所有被@Controller类读取@RequestMapping信息
        if (beanType != null && isHandler(beanType)) {
            detectHandlerMethods(beanName);
        }
    }

      protected void detectHandlerMethods(Object handler) {
        Class<?> handlerType = (handler instanceof String ?
                obtainApplicationContext().getType((String) handler) : handler.getClass());

        if (handlerType != null) {
            Class<?> userType = ClassUtils.getUserClass(handlerType);
            // 4 使用工具类读取所有方法上的@RequestMapping信息  ps:国产开源任务调度框架也是使用这个工具类来读取@JobHandler注解的
            Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                    (MethodIntrospector.MetadataLookup<T>) method -> {
                        try {
                            // 5 终于轮到重头戏了上面说的RequestMappingHandlerMapping具体读取@RequestMapping信息
                            return getMappingForMethod(method, userType);
                        }
                        catch (Throwable ex) {
                            throw new IllegalStateException("Invalid mapping on handler class [" +
                                    userType.getName() + "]: " + method, ex);
                        }
                    });
            if (logger.isTraceEnabled()) {
                logger.trace(formatMappings(userType, methods));
            }
            methods.forEach((method, mapping) -> {
                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                // 6 注册前面读取的RequestMappingInfo存储起来
                registerHandlerMethod(handler, invocableMethod, mapping);
            });
        }
    }
}

将读取的RequestMappingInfo保存起来

从前面我们知道Spring mvc在启动的时候会读取@RequestMapping信息并存储到内存中,接下来我们看看是怎么存储起来的
RequestMappingHandlerMapping 会先调用其父类的registerHandlerMethod方法,其父类会把注册规则交给MappingRegistry实现

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

                List<String> directUrls = getDirectUrls(mapping);
                for (String url : directUrls) {
                    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<>(mapping, handlerMethod, directUrls, name));
            }
            finally {
                this.readWriteLock.writeLock().unlock();
            }
        }
}

从上面的源码我们注册流程如下

  1. 先将method跟相关类实例封装成一个HandlerMethod方便后续调用
  2. 以RequestMappingInfo为Key,HandlerMethod为Value映射到map中
  3. 以Url为Key,RequestMapping为多值Value映射到一个MultiValueMap中
  4. 以RequestMapping的name值为key,以HandlerMethod为Value映射到map中
  5. 获取读取方法上的类跟方法上的@CrossOrigin注解信息封装成CorsConfiguration,并以HandlerMethod为Key,CorsConfiguration为Value映射到一个map中

看到这里可以猜测Spring通过HttpServletRequest的url是如何找到相关HandlerMethod并来处理请求的.有点类似与有点类似与电商系统中根据订单号获取订单的商品信息的来进行发货的.假设订单只有一个商品.首先根据订单号(url)找到订单(ReuestMapping),多个订单(RequestMappingInfo)可以与一个商品(HandlerMethod)相关联,找到商品(HandlerMethod)之后查看是否有物流发货配置(CorsConfiguration)某些获取像生鲜只能走冷链

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容