Spring Flux中Request与HandlerMapping关系的形成过程-阿里云开发者社区

开发者社区> 数据价值> 正文

Spring Flux中Request与HandlerMapping关系的形成过程

简介: Spring Flux中的核心DispatcherHandler的处理过程分为三步,其中首步就是通过HandlerMapping接口查找Request所对应的Handler。本文就是通过阅读源码的方式,分析一下HandlerMapping接口的实现者之一——RequestMappingHandlerMapping类,用于处理基于注解的路由策略,把所有用@Controller和@RequestMapping标记的类中的Handler识别出来,以便DispatcherHandler调用的。
+关注继续查看

一、前言

Spring Flux中的核心DispatcherHandler的处理过程分为三步,其中首步就是通过HandlerMapping接口查找Request所对应的Handler。本文就是通过阅读源码的方式,分析一下HandlerMapping接口的实现者之一——RequestMappingHandlerMapping类,用于处理基于注解的路由策略,把所有用@Controller和@RequestMapping标记的类中的Handler识别出来,以便DispatcherHandler调用的。

HandlerMapping接口的另外两种实现类:1、RouterFunctionMapping用于函数式端点的路由;2、SimpleUrlHandlerMapping用于显式注册的URL模式与WebHandler配对。

文章系列

二、对基于注解的路由控制器的抽象

Spring中基于注解的控制器的使用方法大致如下:

@Controller
public class MyHandler{
    @RequestMapping("/")
    public String handlerMethod(){

    }
}

在Spring WebFlux中,对上述使用方式进行了三层抽象模型。

  1. Mapping

    • 用户定义的基于annotation的映射关系
    • 该抽象对应的类是:org.springframework.web.reactive.result.method.RequestMappingInfo
    • 比如上述例子中的 @RequestMapping("/")所代表的映射关系
  2. Handler

    • 代表控制器的类
    • 该抽象对应的类是:java.lang.Class
    • 比如上述例子中的MyHandler类
  3. Method

    • 具体处理映射的方法
    • 该抽象对应的类是:java.lang.reflect.Method
    • 比如上述例子中的String handlerMethod()方法

    基于上述三层抽象模型,进而可以作一些组合。

  4. HandlerMethod

    • Handler与Method的结合体,Handler(类)与Method(方法)搭配后就成为一个可执行的单元了
  5. Mapping vs HandlerMethod

    • 把Mapping与HandlerMethod作为字典存起来,就可以根据请求中的关键信息(路径、头信息等)来匹配到Mapping,再根据Mapping找到HandlerMethod,然后执行HandlerMethod,并传递随请求而来的参数。

理解了这个抽象模型后,接下来分析源码来理解Spring WebFlux如何处理请求与Handler之间的Mapping关系时,就非常容易了。

HandlerMapping接口及其各实现类负责上述模型的构建与运作。

三、HandlerMapping接口实现的设计模式

HandlerMapping接口实现,采用了"模版方法"这种设计模式。

1层:AbstractHandlerMapping implements HandlerMapping, Ordered, BeanNameAware

  ^  
  |  

2层:AbstractHandlerMethodMapping implements InitializingBean

  ^  
  |  

3层:RequestMappingInfoHandlerMapping

  ^  
  |  

4层:RequestMappingHandlerMapping implements EmbeddedValueResolverAware

下面对各层的职责作简要说明:

  • 第1层主要实现了对外提供模型的接口

    • 即重载了HandlerMapping接口的"Mono
  • 第2层有两个责任 —— 解析用户定义的HandlerMethod + 实现对外提供模型接口实现所需的抽象方法

    • 通过实现了InitializingBean接口的"void afterPropertiesSet()"方法,解析用户定义的Handler和Method。
    • 实现第1层对外提供模型接口实现所需的抽象方法:"Mono<?> getHandlerInternal(ServerWebExchange exchange)"
  • 第3层提供根据请求匹配Mapping模型实例的方法
  • 第4层实现一些高层次用到的抽象方法来创建具体的模型实例。

小结一下,就是HandlerMapping接口及其实现类,把用户定义的各Controller等,抽象为上述的Mapping、Handler及Method模型,并将Mapping与HandlerMethod作为字典关系存起来,还提供通过匹配请求来获得HandlerMethod的公共方法。

接下来的章节,将先分析解析用户定义的模型并缓存模型的过程,然后再分析一下匹配请求来获得HandlerMethod的公共方法的过程。

四、解析用户定义的模型并缓存模型的过程

4-1、实现InitializingBean接口

第2层AbstractHandlerMethodMapping抽象类中的一个重要方法——实现了InitializingBean接口的"void afterPropertiesSet()"方法,为Spring WebFlux带来了解析用户定义的模型并缓存模型的机会 —— Spring容器初初始化完成该类的具体类的Bean后,将会回调这个方法。
在该方法中,实现获取用户定义的Handler、Method、Mapping以及缓存Mapping与HandlerMethod映射关系的功能。

@Override
public void afterPropertiesSet() {

    initHandlerMethods();
    
    // Total includes detected mappings + explicit registrations via registerMapping..
    ...
}

4-2、找到用户定义的Handler

afterPropertiesSet方法中主要是调用了void initHandlerMethods()方法,具体如下:

protected void initHandlerMethods() {
    //获取Spring容器中所有Bean名字
    String[] beanNames = obtainApplicationContext().getBeanNamesForType(Object.class);

    for (String beanName : beanNames) {
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            Class<?> beanType = null;
            try {
                //获取Bean的类型
                beanType = obtainApplicationContext().getType(beanName);
            }
            catch (Throwable ex) {
                // An unresolvable bean type, probably from a lazy bean - let's ignore it.
                if (logger.isTraceEnabled()) {
                    logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
                }
            }
            
            //如果获取到类型,并且类型是Handler,则继续加载Handler方法。
            if (beanType != null && isHandler(beanType)) {
                detectHandlerMethods(beanName);
            }
        }
    }
    
    //初始化后收尾工作
    handlerMethodsInitialized(getHandlerMethods());
}

这儿首先获取Spring容器中所有Bean名字,然后循环处理每一个Bean。如果Bean名称不是以SCOPED_TARGET_NAME_PREFIX常量开头,则获取Bean的类型。如果获取到类型,并且类型是Handler,则继续加载Handler方法。

isHandler(beanType)调用,检查Bean的类型是否符合handler定义。
AbstractHandlerMethodMapping抽象类中定义的抽象方法"boolean isHandler(Class<?> beanType)",是由RequestMappingHandlerMapping类实现的。具体实现代码如下:

protected boolean isHandler(Class<?> beanType) {
    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
            AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

不难看出,对于RequestMappingHandlerMapping这个实现类来说,只有拥有@Controller或者@RequestMapping注解的类,才是Handler。(言下之意对于其他实现类来说Handler的定义不同)。

具体handler的定义,在HandlerMapping各实现类来说是不同的,这也是isHandler抽象方法由具体实现类来实现的原因。

4-3、发现Handler的Method

接下来我们要重点看一下"detectHandlerMethods(beanName);"这个方法调用。

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

    if (handlerType != null) {
        //将handlerType转换为用户类型(通常等同于被转换的类型,不过诸如CGLIB生成的子类会被转换为原始类型)
        final Class<?> userType = ClassUtils.getUserClass(handlerType);
        //寻找目标类型userType中的Methods,selectMethods方法的第二个参数是lambda表达式,即感兴趣的方法的过滤规则
        Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                //回调函数metadatalookup将通过controller定义的mapping与手动定义的mapping合并起来
                (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));
        if (logger.isTraceEnabled()) {
            logger.trace("Mapped " + methods.size() + " handler method(s) for " + userType + ": " + methods);
        }
        methods.forEach((key, mapping) -> {
            //再次核查方法与类型是否匹配
            Method invocableMethod = AopUtils.selectInvocableMethod(key, userType);
            //如果是满足要求的方法,则注册到全局的MappingRegistry实例里
            registerHandlerMethod(handler, invocableMethod, mapping);
        });
    }
}

首先将参数handler(即外部传入的BeanName或者BeanType)转换为Class<?>类型变量handlerType。如果转换成功,再将handlerType转换为用户类型(通常等同于被转换的类型,不过诸如CGLIB生成的子类会被转换为原始类型)。接下来获取该用户类型里所有的方法(Method)。循环处理每个方法,如果是满足要求的方法,则注册到全局的MappingRegistry实例里。

4-4、解析Mapping信息

其中,以下代码片段有必要深入探究一下

 Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));

MethodIntrospector.selectMethods方法的调用,将会把用@RequestMapping标记的方法筛选出来,并交给第二个参数所定义的MetadataLookup回调函数将通过controller定义的mapping与手动定义的mapping合并起来。
第二个参数是用lambda表达式传入的,表达式中将method、userType传给getMappingForMethod(method, userType)方法。

getMappingForMethod方法在高层次中是抽象方法,具体的是现在第4层RequestMappingHandlerMapping类中实现。在具体实现getMappingForMethod时,会调用到RequestMappingHandlerMapping类的下面这个方法。从该方法中,我们可以看到,首先会获得参数element(即用户在Controller中定义的方法)的RequestMapping类型的类实例,然后构造代表Mapping抽象模型的RequestmappingInfo类型实例并返回。

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

构造代表Mapping抽象模型的RequestmappingInfo类型实例,用的是createRequestMappingInfo方法,如下。可以看到RequestMappingInfo所需要的信息,包括paths、methods、params、headers、consumers、produces、mappingName,即用户定义@RequestMapping注解时所设定的可能的参数,都被存在这儿了。拥有了这些信息,当请求来到时,RequestMappingInfo就可以测试自身是否是处理该请求的人选之一了。

protected RequestMappingInfo createRequestMappingInfo(
            RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {

        RequestMappingInfo.Builder builder = RequestMappingInfo
                .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
                .methods(requestMapping.method())
                .params(requestMapping.params())
                .headers(requestMapping.headers())
                .consumes(requestMapping.consumes())
                .produces(requestMapping.produces())
                .mappingName(requestMapping.name());
        if (customCondition != null) {
            builder.customCondition(customCondition);
        }
        return builder.options(this.config).build();
    }

4-5、缓存Mapping与HandlerMethod关系

最后,registerHandlerMethod(handler, invocableMethod, mapping)调用将缓存HandlerMethod,其中mapping参数是RequestMappingInfo类型的。。
内部调用的是MappingRegistry实例的void register(T mapping, Object handler, Method method)方法,其中T是RequestMappingInfo类型。
MappingRegistry类维护所有指向Handler Methods的映射,并暴露方法用于查找映射,同时提供并发控制。

public void register(T mapping, Object handler, Method method) {
            this.readWriteLock.writeLock().lock();
            try {
                HandlerMethod handlerMethod = createHandlerMethod(handler, method);
                ......
                this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
            }
            finally {
                this.readWriteLock.writeLock().unlock();
            }
        }

五、匹配请求来获得HandlerMethod

AbstractHandlerMethodMapping类的“Mono getHandlerInternal(ServerWebExchange exchange)”方法,具体实现了根据请求查找HandlerMethod的逻辑。

    @Override
    public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) {
        //获取读锁
        this.mappingRegistry.acquireReadLock();
        try {
            HandlerMethod handlerMethod;
            try {
                //调用其它方法继续查找HandlerMethod
                handlerMethod = lookupHandlerMethod(exchange);
            }
            catch (Exception ex) {
                return Mono.error(ex);
            }
            if (handlerMethod != null) {
                handlerMethod = handlerMethod.createWithResolvedBean();
            }
            return Mono.justOrEmpty(handlerMethod);
        }
        //释放读锁
        finally {
            this.mappingRegistry.releaseReadLock();
        }
    }

handlerMethod = lookupHandlerMethod(exchange)调用,继续查找HandlerMethod。我们继续看一下HandlerMethod lookupHandlerMethod(ServerWebExchange exchange)方法的定义。为方便阅读,我把注释也写在了代码里。

    protected HandlerMethod lookupHandlerMethod(ServerWebExchange exchange) throws Exception {
        List<Match> matches = new ArrayList<>();
        //查找所有满足请求的Mapping,并放入列表mathes
        addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange);

        if (!matches.isEmpty()) {
            //获取比较器comparator
            Comparator<Match> comparator = new MatchComparator(getMappingComparator(exchange));
            //使用比较器将列表matches排序
            matches.sort(comparator);
            //将排在第1位的作为最佳匹配项
            Match bestMatch = matches.get(0);
            if (matches.size() > 1) {
                //将排在第2位的作为次佳匹配项
                Match secondBestMatch = matches.get(1);
            }
            handleMatch(bestMatch.mapping, bestMatch.handlerMethod, exchange);
            return bestMatch.handlerMethod;
        }
        else {
            return handleNoMatch(this.mappingRegistry.getMappings().keySet(), exchange);
        }
    }

六、总结

理解了Spring WebFlux在获取映射关系方面的抽象设计模型后,就很容易读懂代码,进而更加理解框架的具体处理方式,在使用框架时做到“知己知彼”。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
ML之FE:利用FE特征工程(单个特征及其与标签关系的可视化)对RentListingInquries(Kaggle竞赛)数据集实现房屋感兴趣程度的多分类预测(一)
ML之FE:利用FE特征工程(单个特征及其与标签关系的可视化)对RentListingInquries(Kaggle竞赛)数据集实现房屋感兴趣程度的多分类预测
17 0
PHP中全局变量$_REQUEST、 $_GET、 $_POST、 $_COOKIE 的关系和区别 (转载)
原文地址找不到了 sorry $_REQUEST、 $_GET、 $_POST、 $_COOKIE  的关系和区别:1.关系:$_REQUEST包含了$_GET、$_POST、$_COOKIE的所有内容,是它们的集合体。
880 0
ML之FE:利用FE特征工程(单个特征及其与标签关系的可视化)对RentListingInquries(Kaggle竞赛)数据集实现房屋感兴趣程度的多分类预测(二)
ML之FE:利用FE特征工程(单个特征及其与标签关系的可视化)对RentListingInquries(Kaggle竞赛)数据集实现房屋感兴趣程度的多分类预测
39 0
与信号相关的linux系统编程API
1. kill(pid_t pid, int sig); //给指定的进程发送sig信号   raise(int sig); //给当前进程发送sig信号2. 处理指定的信号    typedef void (*sighandler_t)(int);   sighandler_t   signa...
652 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
10743 0
+关注
8
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载