Spring MVC 解读——@RequestMapping (1)

Stella981
• 阅读 590

Spring MVC 解读——@RequestMapping

    为了降低文章篇幅,使得文章更目标化,简洁化,我们就不例举各种@RequestMapping的用法等内容了.

文章主要说明以下问题:

  1. Spring怎样处理@RequestMapping(怎样将请求路径映射到控制器类或方法)

  2. Spring怎样将请求分派给正确的控制器类或方法

  3. Spring如何实现灵活的控制器方法的

在Spring MVC 3.1 之前的版本中,Spring默认使用 DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter来处理 @RequestMapping注解和请求方法调用,而从3.1开始提供了一组新的API完成这些工作。相比之下,新的API更加的合理完善,开放,易拓 展,面向对象。这篇文章便是基于3.1的新API进行剖析的。

一、概念解析

    在开始之前我们先了解下新的API中引入的新接口或者类,这会有助于后面的处理过程的理解。不得不说新的API提供了更多漂亮的抽象,你能感受到面向对象的魅力。

  1. RequestMappingInfo 这个类是对请求映射的一个抽象,它包含了请求路径,请求方法,请求头等信息。其实可以看做是@RequestMapping的一个对应类。

  2. HandlerMethod 这个类封装了处理器实例(Controller Bean)和 处理方法实例(Method)以及方法参数数组(MethodParameter[])

  3. MethodParameter 这个类从2.0就有了,它封装了方法某个参数的相关信息及行为,如该参数的索引,该参数所属方法实例或构造器实例,该参数的类型等。

  4. HandlerMapping 该接口的实现类用来定义请求和处理器之前的映射关系,其中只定义了一个方法getHandler。

  5. AbstractHandlerMethodMapping 这是HandlerMapping的一个基本实现类,该类定义了请求与HandlerMethod实例的映射关系。

  6. RequestMappingInfoHandlerMapping 这个是AbstractHandlerMethodMapping的实现类,他维护了一个RequestMappingInfo和HandlerMethod的Map属性。

  7. RequestMappingHandlerMapping 这个是RequestMappingInfoHandlerMapping的子类,它将@RequestMapping注解转化为RequestMappingInfo实例,并为父类使用。也就是我们处理@RequestMapping的终点。

  8. InitializingBean 这个接口定义了其实现Bean在容器完成属性设置后可以执行自定义初始化操作,我们的AbstractHandlerMethodMapping便实现了这个接口,并且定义了一组自定义操作,就是用来检测处理我们的@RequestMapping注解。

概念讲的太多总不是什么好事。但明白了上述概念基本上就成功一半了,其中的实现相对@Autowired那篇简单多了。

二、InitialiZingBean.afterPropertySet()

我们从头开始,看看到底Spring是怎样检测并处理我们@RequestMapping注解的。不知大家还记不记的这段代码:

Object exposedObject = bean;
        try {
            populateBean(beanName, mbd, instanceWrapper);
            if (exposedObject != null) {
                exposedObject = initializeBean(beanName, exposedObject, mbd);
            }
        }

   这是BeanFactory创建Bean过程中需要执行的一段代码,其中populateBean方法便是@Autowired注解的处理过程,执行的属性的自动注入等操作。因为initializeBean方法当时与主题无关没有讲,不过这时它便是我们关注的焦点了。(上一篇@Autowired 详解

上面概念中我们讲到InitiaizingBean接口,它的实现Bean会在容器完成属性注入后执行一个自定义操作,这不就满足initializeBean方法的执行唤醒嘛,我们来看它的实现:

protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
        if (System.getSecurityManager() != null) {
            AccessController.doPrivileged(new PrivilegedAction<Object>() {
                public Object run() {
                    invokeAwareMethods(beanName, bean);
                    return null;
                }
            }, getAccessControlContext());
        }
        else {//这里检测当前Bean是否实现一些列Aware接口,并调用相关方法,我们不关心。
            invokeAwareMethods(beanName, bean);
        }
        
        Object wrappedBean = bean;
        if (mbd == null || !mbd.isSynthetic()) {//BeanPostProcessor 的回调,不关心
            wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
        }

        try {
            invokeInitMethods(beanName, wrappedBean, mbd);//这是我们需要关心的,下面看下它的实现
        }

        if (mbd == null || !mbd.isSynthetic()) {//BeanPostProcessor 的回调,不关心
            wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
        }
        return wrappedBean;
    }

   我们接着来看下invokeInitMethods方法的实现:

protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd)
            throws Throwable {
        //是否是InitializingBean的实例
        boolean isInitializingBean = (bean instanceof InitializingBean);
        if (isInitializingBean && 
                (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
            if (System.getSecurityManager() != null) {
                try {
                    AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                        public Object run() throws Exception {//利用系统安全管理器调用
                            ((InitializingBean) bean).afterPropertiesSet();
                            return null;
                        }
                    }, getAccessControlContext());
                }
            }                
            else {//调用InitializingBean的afterPropertiesSet方法。
                ((InitializingBean) bean).afterPropertiesSet();
            }
        }
        //调用自定义初始化方法。。。省略,不关心
    }

   上一篇关于mvc:annotation-driven/的文章,我们说过了,当在配置文件中加上该标记后,Spring(3.1后)会默认为我们注册RequestMappingHandlerMapping等Bean定义。而RequestMappingHandlerMapping `实现了InitializingBean接口,因此,在初始化并装配该Bean实例时,执行到上述代码是,便会执行他的afterPropertySet方法。我们接下来看看他的afterPropertySet方法:`

public void afterPropertiesSet() {
        initHandlerMethods();
    }

    //Scan beans in the ApplicationContext, detect and register handler methods.
    protected void initHandlerMethods() {
        //扫描所有注册的Bean
        String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
           BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), 
                Object.class) : getApplicationContext().getBeanNamesForType(Object.class));
        //遍历这些Bean,依次判断是否是处理器,并检测其HandlerMethod
        for (String beanName : beanNames) {
            if (isHandler(getApplicationContext().getType(beanName))){
                detectHandlerMethods(beanName);
            }
        }
        //这个方法是个空实现,不管他
        handlerMethodsInitialized(getHandlerMethods());
    }

   它直接调用了initHandlerMethods()方法,并且该方法被描述为:扫描ApplicationContext中的beans,检测并注册处理器方法。we are close。

三、检测@RequestMapping

    我们再看它是怎样判断是否是处理器的,以及怎么detect Handler Methods 的:

@Override
    protected boolean isHandler(Class<?> beanType) {
        return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||
                (AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null));
    }

   啊哈,很简单,就是看看有没有被@Controller或者@RequestMapping注解标记

protected void detectHandlerMethods(final Object handler) {
        Class<?> handlerType = (handler instanceof String) ?
                getApplicationContext().getType((String) handler) : handler.getClass();
        final Class<?> userType = ClassUtils.getUserClass(handlerType);
        Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter(){
            public boolean matches(Method method) {//只选择被@RequestMapping标记的方法
                return getMappingForMethod(method, userType) != null;
            }
        });

        for (Method method : methods) {
            //根据方法上的@RequestMapping来创建RequestMappingInfo实例。
            T mapping = getMappingForMethod(method, userType);
            //注册请求映射
            registerHandlerMethod(handler, method, mapping);
        }
    }

   整个的检测过程大致清楚了:1)遍历Handler中的所有方法,找出其中被@RequestMapping注解标记的方法。2)然后遍历这些方法,生成RequestMappingInfo实例。3)将RequestMappingInfo实例以及处理器方法注册到缓存中。

下面我们看看细节:

@Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = null;
        //获取方法method上的@RequestMapping实例。
        RequestMapping methodAnnotation = 
                                AnnotationUtils.findAnnotation(method, RequestMapping.class);
        if (methodAnnotation != null) {//方法被注解了
            RequestCondition<?> methodCondition = getCustomMethodCondition(method);//始终返回null
            info = createRequestMappingInfo(methodAnnotation, methodCondition);//创建MappingInfo
            //检查方法所属的类有没有@RequestMapping注解
            RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, 
                                                                        RequestMapping.class);
            if (typeAnnotation != null) {//有类层次的@RequestMapping注解
                RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);//null
                //将类层次的RequestMapping和方法级别的RequestMapping结合
                info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
            }
        }
        return info;
    }

   很清晰吧,先获取方法上的@RequestMapping信息,然后获取类级别上的@RequestMapping 信息,然后将两者结合,这里我们有必要再了解下怎样创建RequestMappingInfo对象的(包括他的内部结构),以及怎样将类级别的request mapping信息和方法级别的进行结合的?

private RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, 
                                                        RequestCondition<?> customCondition) {
    return new RequestMappingInfo(
         new PatternsRequestCondition(annotation.value(), getUrlPathHelper(), getPathMatcher(),
                 this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions),
         new RequestMethodsRequestCondition(annotation.method()),
         new ParamsRequestCondition(annotation.params()),
         new HeadersRequestCondition(annotation.headers()),
         new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
         new ProducesRequestCondition(annotation.produces(), annotation.headers(), 
                    getContentNegotiationManager()), 
        customCondition
    );
}

   其中涉及到了几个类,我们大致了解下含义:

  • PatternRequestCondition 它其实就是URL模式的封装,它包含了一个URL模式的Set集合。其实就是@RequestMapping注解中的value值得封装。

  • RequestMethodRequestCondition 它是@RequestMapping 注解中method属性的封装

  • ParamsRequestCondition 它是@RequestMapping注解中params属性的封装

    等等,依次类推。因此RequestMappingInfo其实就是对@RquestMapping 的封装。

下面我们再看看怎样进行Combine的:

public RequestMappingInfo combine(RequestMappingInfo other) {
    PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
    RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
    ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
    HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition);
    ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition);
    ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
    RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);

    return new RequestMappingInfo(patterns, methods, params, headers, consumes, 
                    produces, custom.getCondition());
}

   很清晰,对每一个元素都进行combine操作,我们这里只看PatternRequestCondition是怎么结合的,就是看看怎样合并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));
                }
            }
        }
        else if (!this.patterns.isEmpty()) {
            result.addAll(this.patterns);
        }
        else if (!other.patterns.isEmpty()) {
            result.addAll(other.patterns);
        }
        else {
            result.add("");
        }
        return new PatternsRequestCondition(result, this.urlPathHelper, this.pathMatcher, 
                   this.useSuffixPatternMatch,this.useTrailingSlashMatch, this.fileExtensions);
    }

    1)两个pattern都存在是,调用PathMatcher的combine方法合并两个pattern。

2)只有一个有时,使用这个。

3)两个都没有时,为空“”。

现在真正的url拼接是由PathMatcher来完成的了。我们就不看他的代码了就是一串if else的组合,重点是考虑进各种情况,我们来看下方法的注释吧:

Spring MVC 解读——@RequestMapping (1)

    清晰,全面吧,有兴趣的可以看一下代码,这里不讲了。

四、注册请求映射

上面我们已经讲了@RequestMapping的检测和处理,并且根据@RequestMapping生成了RequestMappingInfo实例,那Spring必定需要将这些信息保存起来,以处理我们的请求。

第三节中我们提到一个方法还没有分析,就是registerHandlerMethod 方法:

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
        HandlerMethod handlerMethod;
        if (handler instanceof String) {
            String beanName = (String) handler;
            handlerMethod = new HandlerMethod(beanName, getApplicationContext(), method);
        }
        else {
            handlerMethod = new HandlerMethod(handler, method);
        }
        //上面几行是根据新的处理器实例,方法实例,RequestMappingInfo来生成新的HandlerMethod实例
        //下面是从缓存中查看是否有存在的HandlerMethod实例,如果有并且不相等则抛出异常
        HandlerMethod oldHandlerMethod = handlerMethods.get(mapping);
        if (oldHandlerMethod != null && !oldHandlerMethod.equals(handlerMethod)) {
            throw new IllegalStateException();
        }
        //handlerMethods 是一个Map键是RequestMappingInfo对象,值是HandlerMethod实例
        //因此一个HandlerMethod实例可能处理多个mapping,而一个mapping实例只能由一个method处理
        this.handlerMethods.put(mapping, handlerMethod);
        //这里获取mapping实例中的所有url。
        Set<String> patterns = getMappingPathPatterns(mapping);
        for (String pattern : patterns) {
            if (!getPathMatcher().isPattern(pattern)) {
                //urlMap也是Map,键是url 模式,值是RequestMappingInfo实例
                //因此一个mapping实例可能对应多个pattern,但是一个pattern只能对应一个mapping实例
                this.urlMap.add(pattern, mapping);
            }
        }
    }

   这里可能稍微有点绕,其实道理很简单,当请求到达时,去urlMap中需找匹配的url,以及获取对应mapping实例,然后去handlerMethods中获取匹配HandlerMethod实例。

五、承上启下

篇幅有些长了,超出字数限制了,只能分成两篇了..........................

这章只分析了我们前面三个问题中的第一个,但是已经相当接近了。下一篇我们来讲,Spring怎样处理客户发来的请求,以及方法调用的。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Spring MVC 解读——@RequestMapping (2)
SpringMVC解读——@RequestMapping    上一篇文章中我们了解了Spring如何处理@RequestMapping注解,并将请求映射信息保存到系统中以处理客户端发送来的请求,但是Spring是怎样接受请求,并根据请求URL来匹配正确的处理器方法呢,更重要的是Sprin
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这