要完整跟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结构最全的一张图:
Tomcat三大逻辑组件
对于Tomcat Server提供web服务的流程,可以简单抽象为三大组件:Service、Connector、Container(四种实现:Engine、Host、Context、Wrapper),可以用一张图表现它们之间的关系:
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。
Connector的主要实现是ProtocolHandler。
ProtocolHandler
包含三个部件:Endpoint
、Processor
、Adapter
。
Endpoint
用来处理底层Socket的网络连接,Processor
用于将Endpoint
接收到的Socket封装成Request,Adapter
用于将Request交给Container进行具体的处理。Endpoint
由于是处理底层的Socket网络连接,因此Endpoint
是用来实现TCP/IP协议
的,而Processor
用来实现HTTP协议
的,Adapter
将请求适配到Servlet容器进行具体的处理。Endpoint
的抽象实现类AbstractEndpoint里面定义了Acceptor
和AsyncTimeout
两个内部类和一个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接口的五个方法:
实现了服务逻辑的方法是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注册到一个FilterChain中,然后在请求和返回响应时链式调用Filter.doFilter()。其触发顺序:
- 基于注解配置:按照类名的字符串比较规则比较,值小的先执行
- 使用web.xml配置:根据对应的Mapping的顺序组织,谁定义在上边谁就在前
FilterConfig
和Servlet类似,由于Filter也有可能访问Servlet,所以Servlet 规范将代表 ServletContext 对象和 Filter 的配置参数信息都封装到一个称为 FilterConfig 的对象中。
FilterConfig接口则用于定义FilterConfig对象应该对外提供的方法,以便在 Filter的doFilter()方法中可以调用这些方法来获取 ServletContext 对象,以及获取在 web.xml 文件中的一些初始化参数。
Listener
Listener是一个实现了特定接口的Java程序,用于监听一个方法或者属性,当被监听的方法被调用或者属性改变时,就会自动执行某个方法。
下面有几个与Listener相关的概念
- 事件:某个方法被调用,或者属性的改变
- 事件源:被监听的对象(如ServletContext、requset、方法等)
- 监听器:用于监听事件源,当发生事件时会触发监听器
监听器有类似几种接口:都继承自EventListener
事件源 | 监听器 | 描述 |
---|---|---|
ServletContext | ServletContextListener | 用于监听 ServletContext 对象的创建与销毁 |
HttpSession | HttpSessionListener | 用于监听 HttpSession 对象的创建和销毁 |
ServletRequest | ServletRequestListener | 用于监听 ServletRequest 对象的创建和销毁 |
Livecycle | LivecycleListener | 用于监听生命周期事件的发生 |
ServletContext | ServletContextAttributeListener | 用于监听 ServletContext 对象的属性新增、移除和替换 |
HttpSession | HttpSessionAttributeListener | 用于监听 HttpSession 对象的属性新增、移除和替换 |
ServletRequest | ServletRequestAttributeListener | 用于监听 HttpServletRequest 对象的属性新增、移除和替换 |
HttpSession | HttpSessionBindingListener | 用于监听 JavaBean 对象绑定到 HttpSession 对象和从 HttpSession 对象解绑的事件 |
HttpSession | HttpSessionActivationListener | 用于监听 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:
上面说了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);
……
请求包最后走到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整个架构的类、依赖:
代码解析
Tomcat服务初始化和启动
LifeStyle组件初始化、启动
上面知道,所有的Service,Container,Connector组件都是LifeStyle接口实现,所以其初始化、启动都是链式调用xxx.init(),xxx.start()。
startup.sh调用了Bootstra#main
。Bootstra#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,执行代码。