浅谈servlet-mapping的机制(一)

​本篇文章将结合源代码介绍servlet-mapping的映射机制

1 引言

在Web应用中,经常会涉及到字符串匹配的问题。比如使用AbstractAnnotationConfigDispatcherServletInitializer的方式启动Web应用时,我们需要重写这个方法:

@Override
protected String[] getServletMappings() {
  return new String[]{"/"};
}

这里面的/是啥意思?为什么有时候还会出现/*/**的写法,这些都是用来干啥的?

我们下面便来回答这个问题。

2 Tomcat核心概念

我们先对Tomcat的核心概念进行介绍,Tomcat中主要有如下这些核心概念:

  • Server:代表整个Tomcat的服务器,StandardServer是它的一个标准实现;

  • Service:由一个或多个Connector组成的一组服务,这组服务共享同一个Engine,用于处理HTTP请求,StandardService是它的一个标准实现;

  • Engine:是用于处理整个服务器响应的容器对象,StandardEngine是它的一个标准实现;

  • Host:是一个处理特定请求的虚拟容器对象,StandardHost是它的一个标准实现;

  • Context:是一个表示Servlet上下文的容器对象,StandardContext是它的一个标准实现;

  • ServletContext:定义一系列和容器交互的方法的对象,这才是我们在Spring Web应用常说的servlet上下文对象;

  • Connector:对网络连接的抽象,表示一个网络连接;

  • ProtocolHandler:表示一个协议,比如HTTP11Protocol指1.1版本的HTTP协议;

  • AbstractEndpoint:用于建立网络请求,Tomcat中使用的是它的一个名为JIoEndpoint实现类,是阻塞类型的IO对象。

这些对象相互引用组成了整个Tomcat服务器。

Server包含一个或多个Service,在Tomcat框架中,对服务的抽象是由Service完成的。

Service中包含一个或多个Connector,这说明一个Tomcat进程可以启用多个监听端口(一般应用中只开一个端口,比如8080)。

Service中包含一个Engine对象,Engine是处理请求的引擎。Service中只能有一个Engine对象。这说明同一个Service下,不管有多少个Connector,也不管这些Connect实际启用了多少端口,但最终请求都会由同一个Engine对象来处理。

Engine中包含一个或多个HostHost可以理解是一组Servlet的集合。所以同一个Engine下面可能会有多个不同的Host的。

Host下面可以包含一个或多个Context对象,一个Context对象可以简单认为就是一个Servlet,Servlet是真正处理HTTP请求的实体,不同Servlet处理不同类型的请求(通常由请求的url地址区分)

从上面的关系可以看出,Tomcat本身还是非常灵活的,在各个层级都能支持自定义配置。但是这种灵活,实际现实中关注的人极少。目前真正能做到对Tomcat做深度定制化的更是少之又少。

Connector对象内部包含了一个ProtocolHandler对象,ProtocolHandler对象包含了一个AbstractEndpoint对象。真正打开网络端口并进行监听的其实是AbstractEndpoint的具体实现类。

上面这些,便是Tomcat中的核心概念,以及这些对象是如何构建起完整的Web容器的。

3 Tomcat请求处理过程

根据上面所述,Tomcat启动后,会由AbstractEndpoint的具体实现类打开网络端口,并持续在端口上监听网络请求。这部分由JIoEndpoint$Acceptor类来完成,相关代码如下:

class Acceptor extends AbstractEndpoint.Acceptor {
    @Override
    public void run() {
        while (running) {
            ...
            try {
                Socket socket = null;
                try {
                    socket = serverSocketFactory.acceptSocket(serverSocket);
                } catch (IOException ioe) {
                    // 处理异常情况
                }
                if (running && !paused && setSocketOptions(socket)) {
                    if (!processSocket(socket)) { // 处理请求
                        // Handler Exception
                    }
                } else {
                    // 处理异常情况
                }
            } catch (...) {
                // 处理异常情况
            }
        }
    }
}

这段代码中,我去掉了很多无关紧要的部分。主要的逻辑是通过调用serverSocketFactory.acceptSocket(serverSocket)将线程阻塞。等到有网络请求到来后,执行processSocket(socket)方法启动处理。

后续的处理过程是根据socket数据,构造request和response,然后根据上面提到的各个类之间的关系,最终找到能处理该请求的servlet对象。

在根据request查询能处理该请求的servlet对象的过程中,有这样一个方法:

private final void internalMapWrapper(Mapper.ContextVersion contextVersion, CharChunk path, MappingData mappingData) throws Exception {
        ...
        // Rule 1 -- Exact Match
        Mapper.Wrapper[] exactWrappers = contextVersion.exactWrappers;
        internalMapExactWrapper(exactWrappers, path, mappingData);
        // Rule 2 -- Prefix Match
        boolean checkJspWelcomeFiles = false;
        Mapper.Wrapper[] wildcardWrappers = contextVersion.wildcardWrappers;
        ...
        // Rule 3 -- Extension Match
        Mapper.Wrapper[] extensionWrappers = contextVersion.extensionWrappers;
        if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
            internalMapExtensionWrapper(extensionWrappers, path, mappingData, true);
        }
        // Rule 4 -- Welcome resources processing for servlets
        if (mappingData.wrapper == null) {
            boolean checkWelcomeFiles = checkJspWelcomeFiles;
            if (!checkWelcomeFiles) {
                char[] buf = path.getBuffer();
                checkWelcomeFiles = (buf[pathEnd - 1] == '/');
            }
            ...
        }
        /* welcome file processing - take 2
         * Now that we have looked for welcome files with a physical
         * backing, now look for an extension mapping listed
         * but may not have a physical backing to it. This is for
         * the case of index.jsf, index.do, etc.
         * A watered down version of rule 4
         */
        if (mappingData.wrapper == null) {
            boolean checkWelcomeFiles = checkJspWelcomeFiles;
            ...
        }
        // Rule 7 -- Default servlet
        if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
            ...
        }
        path.setOffset(pathOffset);
        path.setEnd(pathEnd);
    }

上面这段代码就是重点了,也是导致这几个配置//*/**产生不同效果的根本原因。

4 后续

这片文章就到这吧,下一篇文章中,我们将对上面那段代码进行详细解释。并实际测试验证这几个配置//*/**会产生什么效果,以及产生这些效果的原因。

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

镜悬xhs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值