Tomcat源码分析

要完整跟Tomcat可以看看https://juejin.cn/post/7152446633194553374#heading-2系列,这篇文章就粗略地介绍一下。

IDEA搭建调试tomcat web流程:

1. Project Structure/Modules中增加一个IDEA的自带web模块,会自动生成web包,并附带标准的文件结构。

2. 于web包根目录下编写index.jsp或完成web代码后,在Project Structure/Artifacts里Add一个Web Application:Exploded或者Web Application: Archive,将web应用打包以便部署在tomcat上,会在out目录下生成相应的格式:Exploded是文件夹格式,方便更改,一般调试的时候选用这个,可以直接在out目录中更改代码;Archive就是war包,web代码编写调试成功后就打成war包便于传输到服务器上运行。

3. 在Run/debug Configurations中Add一个Tomcat local(本地部署),输入URL等信息,再在Deployment中选择刚才输出的Artifact,启动即会在指定URL运行Artifact中的web服务。

架构

tomcat是一个Java web容器,类似于服务器,能提供一个或多个web的运行环境。运行tomcat web需要tomcat上部署web服务的war包。

Tomcat结构最全的一张图:

image-20250525174350902

Tomcat三大逻辑组件

对于Tomcat Server提供web服务的流程,可以简单抽象为三大组件:Service、Connector、Container(四种实现:Engine、Host、Context、Wrapper),可以用一张图表现它们之间的关系:

image-20250525160222435

Server是Tomcat顶层结构,可以包含一个或多个Service。

Service

Service:表示服务,Server可以运行多个服务。比如一个Tomcat里面可运行订单服务、支付服务、用户服务等等;Server的实现类StandardServer可以包含一个到多个Services, Service的实现类为StandardService调用了Servlet Engine(引擎),而且StandardService类中也指明了该Service归属的Server。

其中默认Service就是Catalina,Catalina其实就是Tomcat的前身,提供基础的web服务。

Container

Engine:Container的顶层结构,相当于一个接口能够调控从属的Host,并生成日志等。会配置一个默认HOST,请求的host没有匹配到合适的Host就会丢给这个默认Host。

Host:虚拟主机。每个Host里可运行多个Host。

Context:在源码结构里面体现为不同的WebApps下的每个目录。

Wrapper: 对Servlet的封装,一个Wrapper即对应一个Servlet实例,负责对Servlet装载、初始化、执行以及资源回收。

在Tomcat中提供的一切的服务都是对应着不同的Servlet,Servlet可以说是最重要的一个”组件”,那么Servlet的具体实现是怎样的呢?

Connector

Connector是用来处理请求,监听在某个端口,等待请求接入,封装Request对象,分发到Container。

img

Connector的主要实现是ProtocolHandler。

ProtocolHandler包含三个部件:EndpointProcessorAdapter

  1. Endpoint用来处理底层Socket的网络连接,Processor用于将Endpoint接收到的Socket封装成Request,Adapter用于将Request交给Container进行具体的处理。
  2. Endpoint由于是处理底层的Socket网络连接,因此Endpoint是用来实现TCP/IP协议的,而Processor用来实现HTTP协议的,Adapter将请求适配到Servlet容器进行具体的处理。
  3. Endpoint的抽象实现类AbstractEndpoint里面定义了AcceptorAsyncTimeout两个内部类和一个Handler接口Acceptor用于监听请求,AsyncTimeout用于检查异步Request的超时,Handler用于处理接收到的Socket,在内部调用Processor进行处理。

具体的处理流程放在下文讲。

JavaWeb三大组件

三者的加载顺序为Listener->Filter->Servlet

org.apache.catalina.core.StandardContext类的startInternal()方法中,首先调用了listenerStart(),接着是filterStart(),最后是loadOnStartup()。这三处调用触发了Listener、Filter、Servlet的构造加载。

Servlet

Servlet

一切的核心逻辑和服务实现都是Servlet实现的。

Servlert的注册:

注释:

...
@WebServlet("/hello")
public class MyServlet extends HttpServlet {
...
}

web.xml:

  <servlet>
   <servlet-name>MyServlet</servlet-name>
   <servlet-class>Servlet.MyServlet</servlet-class>
   <load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
   <servlet-name>MyServlet</servlet-name>
   <url-pattern>/hello</url-pattern>
 </servlet-mapping>

Servlet的启动:服务器启动时(web.xml中配置load-on-startup=1,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法init(ServletConfig conf)。

Servlet是用来处理客户端请求的动态资源,当Tomcat接收到来自客户端的请求时,会将其解析成RequestServlet对象并发送到对应的Servlet上进行处理。

编写一个Servlet的实现需要实现Servlet接口的五个方法:

image-20250526143040810

实现了服务逻辑的方法是Servlet.service()

Tomcat为开发者封装了两种标准Servlet,以便于实现Servlet只需要重写核心逻辑而不用繁琐地实现多种方法:

GenericServlet、HttpServlet。

GenericServlet抽象类是一个通用的Servlet类,对其他四种方法进行了简单实现、只需要开发者重写Service。

HttpServlet抽象类是GenericServlet的子类,专门处理HTTP请求。其Service方法protected void service(HttpServletRequest req, HttpServletResponse resp)是对HTTP请求的标准处理,先解析HTTP请求,再根据不同的请求方法调用到doGet、doPost。大部分情况下只需要重写doGet等方法即可。

ServletConfig

封装了Servlet的特殊信息如类名等等。在Servler初始化的时候为其创建,一个ServletConfig对应一个Servlet。

ServletContext

当一个Context(Webapps目录)下的一个Servlet被启动,会注册一个ServletContext作为当前Context所有Servlet的共享上下文。ServletContext可以获取Web应用中的共享的资源,比如web.xml的初始化参数:

<web-app>
 <display-name>Archetype Created Web Application</display-name>
//配置上下文初始化参数name
 <context-param>
   <param-name>name</param-name>
   <param-value>Feng</param-value>
 </context-param>
 <context-param>
   <param-name>age</param-name>
   <param-value>18</param-value>
 </context-param>
</web-app>
 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       resp.setContentType("text/html; charset=UTF-8");
       PrintWriter writer = resp.getWriter();

       ServletContext servletContext = getServletContext();

       Enumeration<String> initParamerNames = servletContext.getInitParameterNames();
       while(initParamerNames.hasMoreElements()){
           String ParamerName = initParamerNames.nextElement();
           String Paramer = servletContext.getInitParameter(ParamerName);
           writer.write(ParamerName+"的值为:"+Paramer+"<br/>");
      }

       writer.close();
  }

Filter

Filter

工作流程:

Filter 流程图

会把所有的Filter注册到一个FilterChain中,然后在请求和返回响应时链式调用Filter.doFilter()。其触发顺序:

- 基于注解配置:按照类名的字符串比较规则比较,值小的先执行
- 使用web.xml配置:根据对应的Mapping的顺序组织,谁定义在上边谁就在前

FilterConfig

和Servlet类似,由于Filter也有可能访问Servlet,所以Servlet 规范将代表 ServletContext 对象和 Filter 的配置参数信息都封装到一个称为 FilterConfig 的对象中。

FilterConfig接口则用于定义FilterConfig对象应该对外提供的方法,以便在 Filter的doFilter()方法中可以调用这些方法来获取 ServletContext 对象,以及获取在 web.xml 文件中的一些初始化参数。

image-20250526153349442

Listener

Listener是一个实现了特定接口的Java程序,用于监听一个方法或者属性,当被监听的方法被调用或者属性改变时,就会自动执行某个方法。

下面有几个与Listener相关的概念

  • 事件:某个方法被调用,或者属性的改变
  • 事件源:被监听的对象(如ServletContext、requset、方法等)
  • 监听器:用于监听事件源,当发生事件时会触发监听器

监听器有类似几种接口:都继承自EventListener

事件源监听器描述
ServletContextServletContextListener用于监听 ServletContext 对象的创建与销毁
HttpSessionHttpSessionListener用于监听 HttpSession 对象的创建和销毁
ServletRequestServletRequestListener用于监听 ServletRequest 对象的创建和销毁
LivecycleLivecycleListener用于监听生命周期事件的发生
ServletContextServletContextAttributeListener用于监听 ServletContext 对象的属性新增、移除和替换
HttpSessionHttpSessionAttributeListener用于监听 HttpSession 对象的属性新增、移除和替换
ServletRequestServletRequestAttributeListener用于监听 HttpServletRequest 对象的属性新增、移除和替换
HttpSessionHttpSessionBindingListener用于监听 JavaBean 对象绑定到 HttpSession 对象和从 HttpSession 对象解绑的事件
HttpSessionHttpSessionActivationListener用于监听 HttpSession 中对象活化和钝化的过程

ServletContextListener使用示例

以ServletContextListener为例,创建一个用于监听ServletContext对象的Listener,使用注解配置

package Listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class Hello_Listener implements ServletContextListener {

   @Override
   public void contextInitialized(ServletContextEvent sce) {
       System.out.println("ServletContext对象创建了!");
  }

   @Override
   public void contextDestroyed(ServletContextEvent sce) {
       System.out.println("ServletContext对象销毁了!");
  }
}

Pipeline-value

tomcat定义了Pipeline(管道)和Value(阀)。

Pipeline的主要功能是维护一个Value链表,链式调用这个Value,Pipeline的唯一实现类是StandardPipeline。在ContainerBase中便实例化了一个StandardPipeline,而Tomcat的几个容器都是继承了ContainerBase,实际上它们也都是依靠Pipeline管道来开展调用执行。

Value的核心逻辑实现在invoke方法中。ValueBase实现了Container和Value接口,是一个基础抽象类,其四个子类分别是四种容器对应的独特标准Value:

image-20250528110310173

上面说了Pipeline的实例化都是在ContainerBase中实现的,那么每个容器中的Value呢?

都是在容器方法中实例化的,如StandardHost#startInternal;如果想自定义使用也可以在配置文件中,这是解析配置文件的代码:

digester.addObjectCreate(prefix + "Context/Valve",
                        null, // MUST be specified in the element
                        "className");
digester.addSetProperties(prefix + "Context/Valve");
digester.addSetNext(prefix + "Context/Valve",
                   "addValve",
                   "org.apache.catalina.Valve");

在每个内置的Value实现类的invoke方法都会链式调用下一层value#invoke,如:

StandardEngineValue#invoke:

host.getPipeline().getFirst().invoke(request, response);

StandardHostValue#invoke:

context.getPipeline().getFirst().invoke(request, response);

……

image-20250528124823340

请求包最后走到StandardWrapperValve#invoke

通过wrapper.allocate()方法,初始化servlet。

创建ApplicationFilterChain,调用FilterChain的doFilter方法,在最后一个Filter方法执行结束后,会调用Servlet的Service方法,从而实现对Servlet的调用。

LifeStyle

在 Tomcat 中,Server、Service、容器及管道等组件都具有统一的生命周期:初始化(init)、启动(start)、停止(stop)、销毁(destroy)

LifeCycle 接口就是对这些生命周期操作进行了抽象,便于组件的统一编写和调用。

有了这个接口,Tomcat 的生命周期调用路径也更清晰。例如,初始化过程从顶层 Server 的 init() 方法开始,依次链式调用各子组件的 init()。因此,只需操作顶层 Server,即可完成所有组件的生命周期管理。

LifecycleBase是Lifecycle的抽象类,也是被Container继承的类;其init、start等方法实际上是去调用实现了的xxxInternal,如initInternal、startInternal等。

在这种操作模式下,也会引入一些新的情景:如并非所有组件都在初始化阶段立即加载,像是Servlet,可能在首次请求时才初始化。在处理请求的这个过程中,之前初始化的组件不再需要再初始化了。为了支持按需加载,Tomcat 引入了状态机模式来精细管理组件状态。

如:

当组件在STARTING_PREP、STARTING或STARTED时,调用start()方法没有任何效果
当组件在NEW状态时,调用start()方法会导致init()方法被立刻执行,随后start()方法被执行
当组件在STOPPING_PREP、STOPPING或STOPPED时,调用stop()方法没有任何效果
当一个组件在NEW状态时,调用stop()方法会将组件状态变更为STOPPED,比较典型的场景就是组件启动失败,其子组件还没有启动。当一个组件停止的时候,它将尝试停止它下面的所有子组件,即使子组件还没有启动

对于每个事件也引入了监听机制,会部署Listener监听LifeStyle,在代码中体现为如setState(LifecycleState.STARTING);有助于对功能的修改和增加。

Tomcat架构的类依赖

Tomcat整个架构的类、依赖:

image-20250526220432076

代码解析

Tomcat服务初始化和启动

LifeStyle组件初始化、启动

上面知道,所有的Service,Container,Connector组件都是LifeStyle接口实现,所以其初始化、启动都是链式调用xxx.init(),xxx.start()。

startup.sh调用了Bootstra#mainBootstra#main中先调用init方法,创建一些类加载器,以及实例化org.apache.catalina.startup.Catalina到deamon成员变量中;随后接受到.sh中的start参数,反射调用Catalina的load和start方法启动。

Catalina#load先通过parseServerXml解析server.xml,实例化其中的组件并注册到Catalina中,并调用getServer().init(),即如上文所说从Catalina#init初始化Catalina及其子部件。Catalina#start也是调用getServer().start(),启动对所有组件的初始化。其中Host及其下级组件的初始化、实例化和启动比较特殊,并没有在Engine的初始化方法中完成Host及下级组件的初始化。具体的实现也是Tomcat支持热部署Webapp的原因,稍后即讨论到。

StandardServer#initInternal -> StandardService#initInternal -> StandardEngine#initInternal -> super.initInternal()(ContainerBase#initInternal)

StandardServer#startInternal -> StandardService#startInternal -> StandardEngine#startInternal -> super.initStart()(ContainerBase#startInternal)

在Engine以上的上层组件中,初始化或者启动时都为下层组件进行了初始化操作

StandardServer#initInternal

    for (Service service : services) {
       service.init();
  }

而StandardEngine却直接调用父类的方法了,而这一步其实什么也没干。实际上HOST是在StandardHost#start中先初始化再启动的。因为需要考虑效率问题,一个Engine可能对应着多个不同HOST域名下的服务,如果一键初始化会耗费很长时间,所以需要将初始化留在各个Host中自己执行。同样,StandardHost和StandardEngine一样也没有初始化Context,初始化同样放在了Context#start中。

ContainerBase#startInternal 会调用子容器的start方法,这里便是StandardHost#start了,不会调用StandardHost#init

同样地,StandardHost#start又会去调用super.initStart()也就是ContainerBase#startInternal,调用子容器Context的start方法。

这里便有一个问题:在Server.xml中是不会写Context的配置的,这个Context是哪里实例化的?

其实,在ParseServerXml中除了解析配置文件,还会为Engine、Host配置生命周期监听器EngineConfig和HostConfig

并不需要在server.xml中配置Context,而是在ParseServerXml中由HostConfig自动扫描部署目录,以context.xml文件为基础进行解析创建(如果通过IDE启动Tomcat并部署应用,其Context配置将会被动态更新到server.xml中)。

后续的start/init流程着重讲一下关于Servlet/Linster的注册:

Servlet/Linster的注册

StandardContext#startInternal()中,解析xml后依次对Listener、Filter、Servler加载,其中创建Wrapper的关键方法是configureContext(webXml)

 private void configureContext(WebXml webxml) {
       // As far as possible, process in alphabetical order so it is easy to
       // check everything is present
       // Some validation depends on correct public ID
       context.setPublicId(webxml.getPublicId());
...   //设置StandardContext参数      
       for (ServletDef servlet : webxml.getServlets().values()) {
           //创建StandardWrapper对象
           Wrapper wrapper = context.createWrapper();
           if (servlet.getLoadOnStartup() != null) {

               //设置LoadOnStartup属性
               wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
          }
           if (servlet.getEnabled() != null) {
               wrapper.setEnabled(servlet.getEnabled().booleanValue());
          }

           //设置ServletName属性
           wrapper.setName(servlet.getServletName());
           Map<String,String> params = servlet.getParameterMap();
           for (Entry<String, String> entry : params.entrySet()) {
               wrapper.addInitParameter(entry.getKey(), entry.getValue());
          }
           wrapper.setRunAs(servlet.getRunAs());
           Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
           for (SecurityRoleRef roleRef : roleRefs) {
               wrapper.addSecurityReference(
                       roleRef.getName(), roleRef.getLink());
          }

           //设置ServletClass属性
           wrapper.setServletClass(servlet.getServletClass());
          ...
           wrapper.setOverridable(servlet.isOverridable());
           //将包装好的StandWrapper添加进ContainerBase的children属性中
           context.addChild(wrapper);
          for (Entry<String, String> entry :
               webxml.getServletMappings().entrySet()) {
           //添加路径映射
           context.addServletMappingDecoded(entry.getKey(), entry.getValue());
      }
      }
      ...
  }

调用StandradContext.createWrapper创建一个Wrapper,然后将LoadOnStartup、ServletName、ServletClass属性设置进去,最后添加进ContainerBase的children属性。

接着在StandardContext#startInternal方法通过findChildren()获取StandardWrapper

最后依次加载完Listener、Filter后,就通过loadOnStartUp()方法加载wrapper,会对Wrapper对象中loadOnStartup属性的值进行判断,只有大于0的才会被放入list进行后续的wrapper.load()加载调用。

这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序。默认值为-1,只有当Servlet被调用时才加载到内存中。

Connector的启动、初始化

这里还值得一提的是Connector组件的启动和始化,这里Connector是作为Service的一个组件,和其它一并进行初始化和启动的:

Service.init()

connector.init();

Service.start()

connector.start();

这其中具体的代码实现就不具体说明了,最终会执行serverSock.socket().bind(addr,getAcceptCount());进行绑定端口和监听,并开启一个Poller的套接字线程。

Tomcat处理请求

请求接入到Connecot开启的Poller线程后,最后走入Http11Processor#service(http协议是Http11Processor类,其它协议根据配置实现)。

其中会执行一个关键的方法是prepareRequest(),对请求url进行处理,最终对Request对象完成最后的包装,这个对于我们学安全的会重要一点。

Http11Processor#prepareRequest

这个方法干了两件事:1. 判断请求行url路径,如果为绝对路径转化为相对路径 2.判断是否存在”违规字符”

第一件事是什么意思呢?有些请求包可能是这样的:

GET http://localhost/aaa/test HTTP/1.1
Host: localhost
User-Agent: curl/8.2.1
Accept: */*
Connection: close

这种情况就要转化为/aaa/test。

第二件事对于安全来说就相对重要了:会对封装好的相对路径uriBC调用:

  for (int i = uriBC.getStart(); i < uriBC.getEnd(); i++) {
           if (!httpParser.isAbsolutePathRelaxed(uriB[i])) {
               badRequest("http11processor.request.invalidUri");
               break;
          }
      }

HttpParser#isAbsolutePathRelaxed:

  public boolean isAbsolutePathRelaxed(int c) {
       // Fast for valid user info characters, slower for some incorrect
       // ones
       try {
           return IS_ABSOLUTEPATH_RELAXED[c];
      } catch (ArrayIndexOutOfBoundsException ex) {
           return false;
      }
  }

这个IS_ABSOLUTEPATH_RELAXED是一个布尔数组,对每个ascii字符封装了相应的true false,从整体逻辑上来看,如果为false则会返回http 400报错。这里总结了一下在tomact URL中允许或者不允许的字符

0–31、127 是所有 C0 控制字符, false

! " # $ < > [ \ ] ^ \ { | } ~ 。false

所有字母、数字,以及 - . / : = ? @ _ 字符都为 true

Adapter#service

对请求进行预处理获得一个完整的Request对象,接着会调用

this.getAdapter().service

->

this.connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

由此开始了Pipline-value链的执行。

StandardEngineValue#invoke -> StandardHostValue#invoke -> StandardContextValue#invoke -> StandardWrapperValue#invoke

http://localhost/admin/login为例

StandardEngineValue#invoke :会从Request对象中取出访问的host(localhost),如果判断不存在,设置404返回包。

StandardHostValue#invoke:会从Request对象中取出访问的Context (admin),如果判断不存在,返回404返回包。

StandardContextValue#invoke:先判断Request的路径(/admin/login),是否包含/META-INF/,如果包含直接返回404返回包;同样也会取出Wrapper(login),并进行简单的校验。

StandardWrapperValue#invoke:对Wrapper进行一系列的封装,读取一系列的配置,获取了对应路径的Servlet(如果路径指向JSP文件,会返回JspServlet),最终把Servlet以及一众配置包装进ApplicationFilterChain,并且调用filterChain.doFilter(request.getRequest(), response.getResponse());

ApplicationFilterChain#doFilter

这个方法不仅执行了Filter链,在最后,还会执行servlet.service(request, response); 也就是走入相应的Servlet,执行代码。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇