杠精学Tomcat(1)-Tomcat基本使用和整体模块的梳理

699 阅读15分钟

tomcat使用

image.png 在tomcat项目根目录下的webapps文件夹下, 创建一个文件夹HelloServlet, 再继续依次创建WEB-INF文件夹, classes, 类包文件夹, 再在里面放一个编译好的Servlet的class文件. 再在WEB-INF下添加一个web.xml文件, 如下:

<web-app>
    <display-name>my web app</display-name>
    <servlet>
        <servlet-name>servletDemo</servlet-name>
        <servlet-class>com.darkness.servlet.ServletDemo</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>servletDemo</servlet-name>
        <url-pattern>/servletDemo</url-pattern>
    </servlet-mapping>
</web-app>

或者是在server.xml中, 在标签中加一个Context标签

<Context path="/MyContextServlet" relaodable="false"
         docBase="D:\project\code\mycontext\HelloServlet"/>

其中docBase就是项目的磁盘地址, 组成结构与上述HelloServlet项目结构一致即可, 此为描述符部署. 经过如上操作, 找到Tomcat的BootStrap类, 调用main方法启动tomcat, 在浏览器根据servlet的url-pattern访问本机8080端口, 得到如下的响应:

image.png

image.png

像tomcat部署war包, 实际上就是在webapps下看到有war包, 就解压为文件夹, 再去找里面的web.xml, 里面的Servlet. 所以上面第一种部署方式, 只要根据tomcat解析的要求, 去创建文件夹, 编写好所需要的文件就行了. 那么tomcat可不可以部署jar包呢? 实际上是不可以的. 实际上tomcat部署项目的入口是在HostConfig中的deployApps方法. 该方法提供了3种部署项目的方式: 1, XML描述符部署; 2. war包部署; 3. 文件夹部署

类名: org.apache.catalina.startup.HostConfig
/**
 * Deploy applications for any directories or WAR files that are found
 * in our "application root" directory.
 * 部署应用的三种方式
 * 1. 描述符部署
 * 2. War包部署
 * 3. 文件夹部署
 * 另外Tomcat中是使用异步多线程的方式部署应用的
 */
protected void deployApps() {
    File appBase = appBase();
    File configBase = configBase();
    String[] filteredAppPaths = filterAppPaths(appBase.list());
    // Deploy XML descriptors from configBase
    // XML描述符部署
    deployDescriptors(configBase, configBase.list());
    // war包部署
    deployWARs(appBase, filteredAppPaths);
    // 文件夹部署
    deployDirectories(appBase, filteredAppPaths);
}

其中xml描述符部署就是在server.xml中去定义context标签, 指定项目地址 war包部署我们可以看看deployWARs方法

类名: org.apache.catalina.startup.HostConfig
/**
 * Deploy WAR files.
 */
protected void deployWARs(File appBase, String[] files) {
    // ... 省略很多代码
    for (int i = 0; i < files.length; i++) {
        if (files[i].equalsIgnoreCase("META-INF"))
            continue;
        if (files[i].equalsIgnoreCase("WEB-INF"))
            continue;
        File war = new File(appBase, files[i]);
        if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
                war.isFile() && !invalidWars.contains(files[i]) ) {
                // ... 省略很多代码
            }
            // ... 省略很多代码
        }
    }
    // ... 省略很多代码
}

可以看到deployWARs中写死了要是.war结尾的文件, 否则不部署, 所以jar是不可以被tomcat部署的 而文件夹部署也就是上述直接创建文件夹的方式了.

所以tomcat部署项目, 实际上就是只要知道项目的路径就可以了, tomcat根据项目路径, 找到项目, 再读取web.xml, 读取class文件, 将servlet装载到context中, 就能实现对servlet的访问了.

tomcat的配置文件

tomcat核心配置文件是server.xml, 里面会定义端口号, 各种组件等等, tomcat的启动正是读取server.xml中的配置去启动组件, 部署项目的, 下面是去掉了各种注释的简化版的server.xml配置文件.

<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN">
    <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
    <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on"/>
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
    <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
    <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
    <GlobalNamingResources>
        <Resource name="UserDatabase" auth="Container"
                  type="org.apache.catalina.UserDatabase"
                  description="User database that can be updated and saved"
                  factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
                  pathname="conf/tomcat-users.xml"/>
    </GlobalNamingResources>
    <Service name="Catalina">
        <Connector port="8080" protocol="HTTP/1.1"
                   connectionTimeout="20000"
                   redirectPort="8443"/>
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
        <Engine name="Catalina" defaultHost="localhost">
            <Realm className="org.apache.catalina.realm.LockOutRealm">
                <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
                       resourceName="UserDatabase"/>
            </Realm>
            <Host name="localhost" appBase="webapps"
                  unpackWARs="true" autoDeploy="true">
                <Context path="/MyServlet" relaodable="false"
                         docBase="F:/code/tomcat/webapps/HelloServlet"/>
                <Context path="/ServletDemoHello##1" docBase="F:/code/servlet/ServletDemo/target/classes" />
                <Context path="/ServletDemoHello##2" docBase="F:/code/servlet/ServletDemo/target/classes" />
                <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                       prefix="localhost_access_log." suffix=".txt"
                       pattern="%h %l %u %t &quot;%r&quot; %s %b"/>
            </Host>
        </Engine>
    </Service>
</Server>

可以看的很清楚, server.xml的配置文件是有层级结构的, 最顶层节点是server, server下面可以有多个listener, 多个service(名字也可以一样), service节点下面又可以有多个connector, 一个engine(虽然在XML中可以在一个Service中写多个Engine, 但是写在后面的Engine会覆盖写在前面的), engine下面又可以有多个host, host下面又可以有多个context, 多个valve.

在使用tomcat中, 我们可以知道实际上一个项目/应用, 就是对应一个节点, 那我们去tomcat的代码中看看, 有没有Context这一个类呢?

类名: org.apache.catalina.Context
public interface Context extends Container

发现确实有Context这么个玩意儿, 不过是个接口, 而且这个接口还继承了一个叫做Container的接口. 我们平常说tomcat是一个servlet容器, 实际上就是因为它继承了Container(容器)接口.

类名: org.apache.catalina.Container
public interface Container extends Lifecycle

我们先不看Container接口的方法, 先看看有谁继承/实现了它

image.png

好巧不巧, Context, Engine, Host, 我们都是在server.xml中见过的, 它们都是server.xml中的标签. Wrapper我们没见过, 不过问题不大, 有那3个就已经可以证明server.xml中的配置, 基本上都是解析成对应的Container的实现类了.

下面我们来逐一看看server.xml中的各个配置节点的作用

Host节点

<Host name="localhost" appBase="webapps"
                  unpackWARs="true" autoDeploy="true">

第一个属性, 是虚拟主机的意思, 在一个Engin下, 可以有多个Host, name则是它们的唯一标识区分, 当我们看到appBase="webapps", 基本就可以猜到, appBase其实就是告诉tomcat, 在这个Host中, 需要去哪里找应用. 下面我在Engine中增加一个Host

<Host name="darkness.test.servlet" appBase="webapps_virtual_host"
      unpackWARs="true" autoDeploy="true">
    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
           prefix="localhost_access_log." suffix=".txt"
           pattern="%h %l %u %t &quot;%r&quot; %s %b"/>
</Host>

我把name改成darkness.test.servlet, appBase改成webapps_virtual_host, 当然我需要在hosts文件中增加对darkness.test.servlet的DNS解析映射到127.0.0.1, 然后再在tomcat根目录下创建一个webapps_virtual_host文件夹, 如图所示

image.png 我的项目名也叫做HelloServlet, url-pattern也是servletDemo, 跟webapps文件夹下的HelloServlet完全一样, 只是返回上不一样. 启动tomcat, 浏览器访问一下, 看看效果

访问localhost效果

image.png

访问darkness.test.servlet效果

image.png

符合预期, localhost和darkness.test.servlet都映射的127.0.0.1, 项目名也都是HelloServlet, url-pattern也是servletDemo, 但是tomcat根据虚拟主机区分了我输入的域名, 达到了两个项目隔离的效果. 所以虚拟主机也就是隔离了一下项目, 比如你想把多个项目做个分类, 但是又想用一个tomcat去部署, 就可以把同类的项目放在同一个虚拟主机中, 达到隔离的目的. 需要提一嘴, 隔离不仅仅只是做个区分, 比如你那些不同类的项目, 需要的valve也不一样, 这时候虚拟主机的作用就体现出来了, 同一个Host下的Valve当然是一样的, 但是如果搞多个Host, 那Valve当然就不一样了. 至于什么是Valve, 后面会提到.

Engin节点

Engine是Host的父节点, 它的下面可以有多个Host, 在上文中也已经验证, Engine中有一个属性, 叫做defaultHost, 顾名思义, 就是用来指定哪个Host是默认虚拟主机了. 那这个默认虚拟主机具体到底是什么用呢?

比如我浏览器访问的是127.0.0.1, 但是127.0.0.1在我所有的Host虚拟主机中没有一个叫这个名字的, 但是127.0.0.1确实是我tomcat启动所在的ip地址, tomcat启动暴露端口8080, 我是真的可以往这个socket中发数据的, 这种情况下, Engine就会选择defaultHost=localhost这个虚拟主机. 可以看看效果

image.png

可以看到访问127.0.0.1, 得到的是in webapps的servlet的返回.

下面我把Engine标签中的defaultHost改一下, 改成:

<Engine name="Catalina" defaultHost="darkness.test.servlet">

启动tomcat, 访问127.0.0.1看看效果:

image.png

效果达成, 访问127.0.0.1得到的是in webapps_virtual_host的响应, 说明defaultHost生效了.

所以Engin的作用其实没有什么特殊的, 就是把所有的Host管理起来, 然后指定一个默认的Host而已.

Context节点和Wrapper接口

从XML描述符部署项目的例子, 我们可以知道, 一个Context就是对应一个应用. 所以Context内部肯定就是servlet了. 但是实际上tomcat并没有这样做, Context中是Wrapper列表, 而一个Wrapper对应某一个类别的Servlet, 在Wrapper内部才是Servlet的列表.

管道Pipeline和阀门Valve

上面提到的不论是Context, 还是Engine, Host, Wrapper, 都是所谓的Servlet容器, 而对于每个Servlet容器来说, 都会有自己的一套固有的处理逻辑. 这些个处理逻辑被封装在Pipeline中, 而这些处理逻辑就是Valve, 阀门. 所谓阀门, 可以通俗的理解为过滤器. 过滤器大家应该都知道, 但是阀门和过滤器还是有一定区别的. 我们平时所说的过滤器, 一般是业务层面对请求做的一些通用的处理, 而Valve是还没有到业务层面的, Valve是tomcat在处理请求过程中, 在真正处理Servlet之前, 对请求做的一些通用的处理. 比如本文中Server.xml配置中, 在Engine中就配置了一个Valve, 作用是记录日志.

所以在Engine, Host, Context, Wrapper中, 都会有一个Pipeline, 里面封装的是一系列的Valve. 一个请求过来, 会先进入到Engine中, 找到对应的虚拟主机, 然后执行Valve的逻辑, 然后进入到Host层, 再找到请求对应的项目, 再执行Host中的Valve逻辑, 再进入到Context层, 找到对应的Wrapper, 再执行Valve的逻辑. 依次类推, 最终走到真正的Servlet中去执行业务逻辑.

自定义一个阀门

找到Valve接口, 看看它的继承关系

image.png

看到有一个实现了Valve的ValveBase抽象类, 我们可以自己写一个类, 去继承ValveBase, 重写抽象方法invoke, 试试看看是否能有效果

类名: com.darkness.TestValve
public class TestValve extends ValveBase {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        System.out.println("I'm a customize Valve, my name is " + name);
        getNext().invoke(request, response);
    }

}

在server.xml中, 在Context中我增加一个Valve

<Context path="/MyContextServlet" relaodable="false"
                         docBase="G:\project\code\mycontext\HelloServlet">
    <Valve className="com.darkness.TestValve" name = "法外狂徒张三"/>
</Context>

基于此, 启动tomcat, 访问HelloServlet, 发现并没有看到Valve的效果, 但是访问MyContextServlet, 就会看到效果

image.png

需要注意的是, Valve中的invoke方法的结尾, 一定要getNext().invoke(request, response), 否则Valve链就断掉了.

容器中默认的阀门

通过自己写一个自定义的阀门的案例, 我们大概知道了阀门的作用. 实际上上文中有一点是比较朦胧的, 就是"所以在Engine, Host, Context, Wrapper中, 都会有一个Pipeline, 里面封装的是一系列的Valve. 一个请求过来, 会先进入到Engine中, 找到对应的虚拟主机, 然后执行Valve的逻辑, 然后进入到Host层, 再找到请求对应的项目, 再执行Host中的Valve逻辑, 再进入到Context层, 找到对应的Wrapper, 再执行Valve的逻辑. 依次类推, 最终走到真正的Servlet中去执行业务逻辑"这段话. 看了阀门的作用, 应该就大概能猜出来了, 从上层容器到下层容器的这个过渡, 就是使用阀门实现的.

每个容器都会有一个自带的阀门, 取名为StandardXxxValve

image.png

我们在每个容器中自定义无论多少个阀门, 它们都会被添加到标准阀门之前. 我们看看每个标准阀门invoke方法是如何invoke到下个容器的阀门的

类名: org.apache.catalina.core.StandardEngineValve
Host host = request.getHost();
// ...省略N多代码
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
类名: org.apache.catalina.core.StandardHostValve
Context context = request.getContext()
// ...省略N多代码
context.getPipeline().getFirst().invoke(request, response);
类名: org.apache.catalina.core.StandardContextValve
Wrapper wrapper = request.getWrapper();
// ...省略N多代码
wrapper.getPipeline().getFirst().invoke(request, response);

可以看到, 在request这个对象中就已经确定了这个request所对应的Host, Context, Wrapper, tomcat中已经默认了每个容器的最后一个阀门就是它的标准阀门, 所以在它的标准阀门中, 会去拿到它对应的下个容器的第一个阀门: getFirst, 这样就形成了一个连接上层容器到下层容器的桥梁的作用.

Tomcat是如何调用到Servlet的service方法的

上文中描述了阀门的调用, 实际上是一个类似递归的调用, 上一个调用下一个, 下一个阀门的invoke出栈后, 上一个才会出栈, 所以Valve链总一定是会有一个尾结点的, 这个尾结点正是StandardWrapperValve. 我们看看该类中的invoke方法是做了什么(方法太长, 这里删除了亿行代码, 只保留了核心代码)

类名: org.apache.catalina.core.StandardWrapperValve
public final void invoke(Request request, Response response)
    throws IOException, ServletException {
    // 为本次请求分配servlet实例
    try {
        if (!unavailable) {
            servlet = wrapper.allocate();
        }
    } catch (Throwable e) {
       
    }
    // 创建本次请求的过滤器链
    ApplicationFilterFactory factory =
        ApplicationFilterFactory.getInstance();
    ApplicationFilterChain filterChain =
        factory.createFilterChain(request, wrapper, servlet);
    // 调用本次请求的过滤器链
    // 这里也将调用servlet的service()方法
    try {
        if ((servlet != null) && (filterChain != null)) {
            // Swallow output if needed
            if (context.getSwallowOutput()) {
                try {
                    SystemLogHandler.startCapture();
                    if (request.isAsyncDispatching()) {
                        request.getAsyncContextInternal().doInternalDispatch();
                    } else if (comet) {
                        filterChain.doFilterEvent(request.getEvent());
                        request.setComet(true);
                    } else {
                        filterChain.doFilter(request.getRequest(),
                                response.getResponse());
                    }
                } finally {
                    String log = SystemLogHandler.stopCapture();
                    if (log != null && log.length() > 0) {
                        context.getLogger().info(log);
                    }
                }
            } else {
                if (request.isAsyncDispatching()) {
                    request.getAsyncContextInternal().doInternalDispatch();
                } else if (comet) {
                    request.setComet(true);
                    filterChain.doFilterEvent(request.getEvent());
                } else {
                    filterChain.doFilter
                        (request.getRequest(), response.getResponse());
                }
            }

        }
    } catch (Throwable e) {
        
    }
}

可以看到, 在最后的阀门StandardWrapperValve中, 创建/分配(servlet可能会是单例也可能会是多例)了servlet实例, 创建了servlet的过滤器链, 再调用filterChain.doFilter(request, response)方法进入到过滤器链中, 在ApplicationFilterChain中的internalDoFilter方法, 调用完过滤器的逻辑之后, 再来执行servlet的service方法.

Request对象

上文提到, 在调用Valve链的过程中, 承上启下的Valve, 都是从Valve中直接调用request.getHost(), request.getContext()等方法直接获取到本次请求的各层容器. 至于request是如何封装的? 这个request对象到底是不是真正到时候传给Servlet的service方法中的那个HttpServletRequest对象呢? 这就得看看request是怎么从socket中的输入流中读出来封装的, 以及Valve->过滤器链调用过程中对request的处理了.

实际上tomcat在启动之后, 就会启动一个线程, 去不断的调用socket的accept方法去接受信息, 这个线程所在的类叫做Acceptor, 至于socket, 本文仅以BIO为例(毕竟简单一些). Acceptor类位于JioEndpoint中的受保护的非静态内部类. 在该类的run方法中

类名: org.apache.tomcat.util.net.JIoEndpoint.Acceptor
socket = serverSocketFactory.acceptSocket(serverSocket);
类名: org.apache.tomcat.util.net.JIoEndpoint.Acceptor
if (!processSocket(socket))

在processSocket方法中:

类名: org.apache.tomcat.util.net.JIoEndpoint.Acceptor
getExecutor().execute(new SocketProcessor(wrapper));

由此可见, SocketProcessor是一个实现了Runnable接口的类. 看看SocketProcessor的run方法的核心代码

类名: org.apache.tomcat.util.net.JIoEndpoint.SocketProcessor
state = handler.process(socket, SocketStatus.OPEN_READ);

在这段代码里, 将socket向下传递到了handler的process方法中去. 由于太过复杂, 本文也不多赘述, 这个handler在BIO中就是org.apache.coyote.http11.Http11Protocol.Http11ConnectionHandler, 先调用父类抽象类org.apache.coyote.AbstractProtocol.AbstractConnectionHandler的process方法, 最终通过模板方法模式调用到子类具体实现的createProcessor()方法拿到具体的processor, 再调用processor的process方法去处理请求. image.png

具体Http协议实现类, 也仅仅是对操作系统传输进来的输入流的解析方式不同(nio, bio), 毕竟协议本身是固定的, 但是tomcat仅仅是一个应用程序, 它的数据只能通过操作系统给进来. 所以io方式的不同必然会对应不同的实现类. 下图是抽象http协议父类AbstractHttp11Processor的process方法的部分截图. 可以看到在process方法中对输入流做了解析请求头的操作, 这个parseRequestLine又有多个实现方法, 这多个实现方法正是对应了多种不同的对输入流的解析方式, 但是不论哪种输入流解析方式, 都是遵循了Http协议的, 所以这些公共方法正是写在了抽象父类中, 在子类中去具体实现不同的解析方式. image.png

由于Request对象是被封装在协议处理类中的.

类名: org.apache.coyote.AbstractProcessor
protected Request request; // public final class org.apache.coyote.Request

需要注意的是, 这个Request对象, 并没有实现HttpServletRequest接口, 也就是说它并没有实现Servlet规范, 这个request对象仅仅是tomcat内部的一个用于存放socket读取的数据封装信息的对象而已.

所以在parseRequestLine方法中, 就通过Http协议的规范, 将socket中的输入流中的请求行部分解析到了request对象的请求行属性中去了. 在解析完请求行之后, 还会有一个parseRequestHeader方法, 作用也是类似的. 将socket中的输入流按照Http协议格式, 读取数据封装到request对象中. 进而传递给下游组件.

类名: org.apache.coyote.http11.AbstractHttp11Processor
adapter.service(request, response);

正是在抽象父类的process方法中, 走了上述这步, 去交给容器处理请求的.

类名: org.apache.catalina.connector.CoyoteAdapter
Request request = (Request) req.getNote(ADAPTER_NOTES); // Request-->Request

在这里, 实现了一个Request对象到Request对象的转换. 什么意思呢? 外部给Adapter对象的service方法传进来的参数中的request实际上是org.apache.coyote.Request的对象, 而在Adapter的service方法中, 将这个Request对象转换成一个业务层面的Request对象, 这个对象是实现了HttpServletRequest接口的, 也就是说它是实现了Servlet规范的:

包名: org.apache.catalina.connector
public class Request implements HttpServletRequest

适配器的service方法拿到内部request对象, 转换成实现了servlet规范的request对象之后, 进而就会拿到顶层容器的管道, 再拿到顶层容器的管道中的第一个Valve, 开始了阀门链的执行. 将这个实现了Servlet规范的request对象和response对象层层向下传递.

类名: org.apache.catalina.connector.CoyoteAdapter
方法名: public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
connector.getService().getContainer().getPipeline().getFirst().invoke(
        request, response);

实际上这个实现了HttpServletRequest接口的request, 并不是最终传递给我们自己写的Servlet的request对象. 因为在过滤器链的执行过程中, tomcat又偷偷摸摸的搞了一下, 在最后一个容器的最后一个阀门StandardWrapperValve中, 调用的filterChain的dofilter是这么玩的:

类名: org.apache.catalina.core.StandardWrapperValve
方法名: public final void invoke(Request request, Response response)
filterChain.doFilter
    (request.getRequest(), response.getResponse());

它使用request.getRequest()方法, 得到了一个RequestFacade对象, 看到这个命名应该就能明白, 这是一个Request的门面. 毕竟就算前面那个Request对象是实现了Servlet规范的, 但是它毕竟还是距离socket更近, 它有很多受保护的方法之类的, 总而言之就是, 不安全! tomcat肯定是想给外部一个只能按照它的想法来的对象, 所以tomcat提供了一个RequestFacade这个门面类:

包名: org.apache.catalina.connector
public class RequestFacade implements HttpServletRequest

这个门面类同样实现了HttpServletRequest接口, 里面封装了真正的request对象, 对门面对象的操作, 最终会走到内部的request对象的操作. 而这个门面, 才是最终真正传给我们自己的Servlet对象的request对象. 而对于Response对象, tomcat也是进行了类似的操作. 打印一下证明看看是不是这样的.

image.png

image.png

确实传给Servlet实例的是一个门面.

总结

本文只是讲了一下tomcat的基本简单的应用和一个请求是如何从socket中变成Request对象最终传给servlet的大致流程. 具体详细的内容太过复杂, 比如是如何解析socket流的, 是如何加载类的, nio和bio的区别. 本文都没涉及到. 因此是一个入门的学习笔记.

一个请求进来的流程:

  1. 操作系统得到其它应用传输过来的数据, 打入socket的输入流中
  2. socket接受到数据后, Acceptor中的socket.accept方法就会得到信息
  3. 从socket输入流中解析到数据, 封装到request对象中, 进而调用第一层容器(Engine)的第一个阀门
  4. 阀门层层调用, 到最后一层容器(Wrapper)的最后一个阀门(StandardWrapperValve)之后调用过滤器链
  5. 过滤器链调用完毕之后, 调用父类HttpServlet的service方法, 再调用到子类的service方法(我们重写的).