tomcat的简单实现

为了能够深入理解tomcat,所以我找了一本叫《深入剖析Tomcat》的书籍,这篇相当于类似读书笔记。

一些说明:

  • 该书使用的tomcat版本为tomcat4和tomcat5,而我在读这本书的时候自己使用的tomcat版本为9.0,所以会有一些出入,且文中说的“最新版本”其实指的就是tomcat 9.0。
  • tomcat也是面向接口编程的,基本就是一个接口X,然后有一个XBase的抽象类来实现这个接口,接下来就是一个StandardX来继承这个抽象类。

全书脉络

  • 第一章介绍了一下最简单的web知识,并且手动实现了一个web server。
  • 第二章展示了什么是servlet容器
  • 第三章实现了最简单的连接器
  • 第四章详细讲解了连接器
  • 第五章简单介绍了容器
  • 第六章介绍了Lifecycle,几乎所有的类都实现了这个接口
  • 第七章介绍了日志记录器,非常非常过时
  • 第八章对载入器进行了介绍,比较重要的一个模块
  • 第九章介绍了session管理器,也比较重要
  • 第十章介绍了安全机制,不过似乎我没见到过实际中存在这类机制
  • 第十一章详细介绍了Wrapper
  • 第十二章详细介绍了Context
  • 第十三章介绍了Host和Engine
  • 第十四章介绍了服务器组件和服务组件
  • 第十五章介绍了如何使用Digester来从配置文件中生成所需要的对象而不是硬编码
  • 第十六章介绍了关闭钩子
  • 第十七章介绍如何启动和关闭tomcat,还额外介绍了一下bash和bat的部分知识
  • 第十八章介绍了部署器
  • 第十九章介绍了如何使用一个特殊的servlet来对tomcat进行处理
  • 第二十章介绍了JMX来对tomcat进行管理

第一章

开篇需要理解一下http协议中最简单的包的结构,即http请求和响应的结构。

然后是手动实现一个http server,能够实现最最简单的功能。除了server类之外,我们还需要抽象两个概念,一个是request,另外一个是response,用来作为接受请求和对请求做出相应。

默认情况下,服务器只需要一启动就会永远监听8080端口,只有当满足特定的条件(这个之后再讨论)才会退出。然后底层是通过服务器socket的accept方法返回一个和客户端进行通信的socket,并且通过基础的io流进行通信,有了这些,就可以得到下面的代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SimpleHttpServer {

/**
* 服务器端的socket
*/
private ServerSocket serverSocket;

/**
* 服务器端口号
*/
private Integer port = 8080;

/**
* web root,默认就就在当前项目下
*/
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";

public void start() {
try {
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
// 如果在这里都异常了,那就不需要继续了,直接退出
e.printStackTrace();
System.exit(1);
}

while (true) {
// 此服务器一次只能处理一个请求
Socket socket = null;
InputStream input = null;
OutputStream output = null;

try {
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

接下来我们先去抽象出我们需要的request和response,方便我们之后使用。

我们都知道http请求头部形如这样:GET /index.html HTTP/1.1,而在第一章,我们也仅仅对这个感兴趣,只要获取了第一行,就可以知道这个请求需要的文件地址了,就可以去对应的地方找到这个文件并读取了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Slf4j
public class Request {
/**
* 从socket中获取的inputStream
*/
private InputStream input;

/**
* Buffer size
*/
private final int BUFFER_SIZE = 1024;

private String uri;

public Request(InputStream input) {
this.input = input;
}

public void parse() {
byte[] buffer = new byte[BUFFER_SIZE];
StringBuilder sb = new StringBuilder();
int len = -1;
try {
len = input.read(buffer);
} catch (IOException e) {
e.printStackTrace();
len = -1;
}

for (int i = 0; i < len; i++) {
sb.append((char) buffer[i]);
}

log.info("request = \n{}", sb.toString());
uri = parseUri(sb.toString());
log.debug("uri = {}", uri);
}

private String parseUri(String requestString) {
String[] strings = requestString.split(" ");
if (strings.length >= 3) {
return strings[1];
}
return null;
}

public String getUri() {
return this.uri;
}
}

其实上面的代码就一个目的:获取uri的值。其余都是附加的。

接下来是response的代码,response原书里面是没有HTTP/1.1 200 OK这段的,实测不加这段的chrome是无法解析的,所以我就加上了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Slf4j
public class Response {

private Request request;
private OutputStream outputStream;

private static final int BUFFER_SIZE = 1024;

public Response(Request request, OutputStream outputStream) {
this.request = request;
this.outputStream = outputStream;
}

public void sendStaticResource() throws IOException {
FileInputStream fis = null;
byte[] buffer = new byte[BUFFER_SIZE];
File file = new File(SimpleHttpServer.WEB_ROOT, request.getUri());
try {
if (file.exists()) {
// 读取指定文件
log.info("file exists");
fis = new FileInputStream(file);
outputStream.write("HTTP/1.1 200 OK\r\n".getBytes());
outputStream.write("\r\n".getBytes());
int read = fis.read(buffer, 0, BUFFER_SIZE);
while (-1 != read) {
outputStream.write(buffer, 0, read);
outputStream.flush();
read = fis.read(buffer, 0, BUFFER_SIZE);
}
} else {
log.info("file not exists");
String errorMsg = "HTTP/1.1 404 File Not Found\r\n"
+ "Content-Type: text/html\r\n"
+ "Content-Length: 23\r\n"
+ "\r\n"
+ "<h1>File not Found</h1>";
outputStream.write(errorMsg.getBytes());
}
} catch (Exception e) {
log.error(e.toString());
} finally {
if (fis != null) {
fis.close();
}
}
}
}

也是很简单的代码,只需要获取了request想要的资源的信息,就可以通过output发回去了。

第二章

Servlet

在这一部分,将添加servlet的部分。servlet本身有一个java的接口,所以我们只需要实现它们就可以了。

这个接口本身有五个方法,我们基本上只需要实现service方法即可。

Servlet容器

然后是站在servlet容器(也就是Tomcat的角度)来思考,我们需要为servlet做些什么?

  1. 创建该servlet,并且调用它的init方法。
  2. 每次收到请求,都需要创建出对应的request和response实例,这部分之后会独立出去
  3. 然后调用servlet的service方法,并将创建出来的request和response实例传过去。
  4. 最后调用destory方法,并且释放内存。

接下来分别对各个部分加以改造,使其支持上面说的功能。

HttpServer

server需要根据获取得到的包的第一行,来判断究竟应该调用谁来完成任务。这里先规定,假设uri里面以/servlet/作为开头的就调用servlet,否则就和之前一样认为是静态资源。

Request

之前说了servlet的service()方法中就会有request和response对象传入,所以我们需要在之前的request类的基础上,让它继承自javax.servlet.ServletRequest。当然这些方法我们目前并不需要实现,留空即可。

Response

和request同理,新增一个接口的实现,但是需要注意这里需要多实现一个方法(主要是为了能够让servlet调用这个方法来进行输出,便于显示结果):

1
2
3
4
5
6
private PrintWriter writer;
@Override
public PrintWriter getWriter() throws IOException {
writer = new PrintWriter(outputStream, true);
return writer;
}

ServletProcesser

现在唯一缺的,就是我们如何对servlet进行处理了。思路也是非常简单,我们仅仅需要知道servlet被放在服务器的哪个位置,然后使用类加载器去加载它,并且将其强转为servlet,并调用它的service方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Slf4j
public class ServletProcessor {

public void process(Request request, Response response) {
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
log.debug("servletName = {}", servletName);
URLClassLoader loader = null;
try {
URLStreamHandler streamHandler = null;
File classPath = new File(Constant.WEB_ROOT);

// 构造出协议号+地址,为了方便之后构造URLClassLoader
String repository = String.valueOf(new URL("file", null, classPath.getCanonicalPath() + File.separator));
log.debug("repository = {}", repository);
loader = new URLClassLoader(new URL[]{new URL(null, repository, streamHandler)});
} catch (IOException e) {
e.printStackTrace();
}
Class c = null;
try {
c = loader.loadClass(servletName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

Servlet servlet = null;
if (c != null) {
try {
servlet = (Servlet) c.newInstance();
servlet.service(request, response);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException | ServletException | IOException e) {
e.printStackTrace();
}
}
}
}

这里遇到和书里不一样的地方导致我类加载不了,因为这里我的类是直接在当前路径下,然后需要类的全限定类名。

还有一个问题就是,我这里server发送response似乎是需要断开之后(或者超时之后)才会将响应送达到浏览器,这里的解决方法(可能不一定完美)就是当发送结束之后,立刻断开input和output。

第三章

这一章主要是写连接器。连接器的主要功能是,为每个收到的HTTP请求创建request对象和response对象,并交给容器。从这里开始,我们的tomcat正式分成了两个部分——连接器和servlet容器。

HttpRequest

这个类就是用来读取请求,所以可以大致归为这五个部分:

  1. 读取套接字的输入流
  2. 解析请求行
  3. 解析请求头
  4. 解析cookie(cookie也可以算在请求头里)
  5. 获取参数

下面分部分来实现。

读取套接字

书中使用SocketInputStream,其实用原生的就行。

解析请求行

这里指的注意的是,如果禁用了cookie,那么jsessionid也可以通过请求参数传递过来,这里可以获取到。

解析请求头

请求头可以抽象一个HttpHeader来代替它。本质上就是通过一个hashmap来对其进行操作。

解析cookie

cookie本身就是请求头的一项,只不过由于它比较重要,所以这里可以抽象一个Cookie类来对其进行操作。

获取参数

如果是get请求,那么只需在请求行里获取即可。如果是POST请求,则需要去请求体里面看看。

HttpResponse

和之前的一样,判断一下是静态的资源还是servlet即可。

第四章

这一章将详细讲述连接器的作用。tomcat的连接器作为一个独立的模块,所以实际中我们可以进行更换。连接器必须要满足以下几个要求:

  1. 实现了org.apache.catalina.Connector接口
  2. 能够创建实现了org.apache.catalina.Request接口的request对象
  3. 能够创建实现了org.apache.catalina.Response接口的response对象

官方实现的连接器进行了一些优化,比如使用对象池技术来避免频繁创建对象的性能损耗,使用字符数组来代替字符串提高效率。

HTTP 1.1新特性

  1. 持久连接。除了我们请求的资源,服务器发回的响应体里面往往还会有一些图片啦、js文件等,需要我们再去找服务器索要,如果使用传统的每次都建立一个http连接,那么开销过于巨大。所以有了这个持久连接的特性,只需要在请求头部加上connection:keep-alive即可。
  2. 块编码。有的时候不知道响应块的大小,就需要用到这种方法来分块传送响应内容。
  3. 状态码100.当客户端需要发送一个超大的请求的时候,由于不确定服务器是否会接受,所以可以发送一个请求头Expect:100-continue,服务器如果愿意接收,则可以发送HTTP/1.1 100 Continue给浏览器。

Connector接口

该接口位于org.apache.catalina.Connector,里面最重要的方法是(我在最新的版本找过了,已经没有下面这些方法了):

  • setContainer() 将连接器与某个servlet容器关联。
  • getContainer() 获取当前连接器相关联的某个servlet容器。
  • createRequest() 和 createResponse() 见名知意。

HttpConnector类

该类实现了Connector接口,并且实现了Lifecycle接口,使得可以调用一些方法来完成生命周期的管理,它的主要功能是:

  • 创建服务器套接字
  • 维护一个HttpProcessor的实例池,这样就可以同时处理多个连接了。所以其实是把socket传给HttpProcessor,让其进行加工。

HttpProcessor

之前实现的HttpConnector,运行在自己的线程里,而且它必须等待当前的HTTP请求返回,才可以处理下一个HTTP请求,这样效率显然非常的低下。

所以,HttpProcessor本身实现了Runnable接口,这样,每次当Connector给Processor派送任务的时候,任务就可以在Processor新的线程里面被执行了,而这些线程就叫“处理器线程”。这么说起来可能有点拗口,其实就是一个多线程的服务器……

下面是连接器线程的assign()方法的伪代码:

1
2
3
4
5
6
7
8
public void assign(){
while(available){
wait();
}
this.socket = socket;
available = true;
notifyAll();
}

下面是Processor的await()方法的伪代码:

1
2
3
4
5
6
7
8
9
public void await(){
while(!available){
wait();
}
Socket socket = this.socket;
available = false;
notifyAll();
return socket;
}

具体流程是这样的:当Connector刚刚启动的时候,available这个值是false,这里就意味着,Processor里面的await方法必须等待。而连接器一旦有了新的socket,就会调用assign方法,此时该方法并不会进入到while循环内,而是会得到这个socket,并且将available置为true,同时调用notifyAll()来唤醒Processor线程,这样Processor就可以得到这个socket,并且

第五章

tomcat中,一共有4种servlet容器,分别是Engine、Host、Context和Wrapper,它们按照范围从大到小排列的。

Container接口

其下分别有四个接口,Engine、Host、Context和Wrapper,而这些接口可以由大到小去包含,比如一个Context可以包含0个或者多个Wrapper。所有的servlet容器必须实现这个接口。

管道任务

类似过滤器一样(注意!只是机制类似,它和过滤器完完全全不是一个东西),每当一个request需要传递过去的时候,需要经过一根管道,而管道里面则有一个一个的阀(valve),每一个阀代表了一个任务,最后一个阀是基础阀。

当连接器调用了容器的invoke方法之后,容器会接着调用管道的invoke方法。也就是说现在执行的大任落到了管道身上,管道通过其内部类——ValveContext的invokeNext方法,来一次调用每一个阀,并且将自身传递给下一个阀,让下一个阀继续调用。

Wrapper

作为最低级的容器,wrapper的功能就是调用servlet的init()、service()、destory()方法。

应用程序实现

已知Wrapper接口继承了Container接口,这里创建一个SimpleWrapper类来实现Wrapper接口,并且在这个SimpleWrapper类同时也实现了Pipeline,用来实现上面说的管道任务。

由于是servlet容器,所以必然需要一个类加载器来加载servlet。而且上面也说了Wrapper是最低级的servlet容器,所以它还有一个成员变量parent(当然可以是Null),然后在获取类加载的时候,先判断自己有没有,然后再判断自己的parent有没有。

接下来可以通过实现Valve接口来自己定义一个又一个的阀。

最后可以通过简单的包装使用,来实现一个只有一个servlet的服务器(最简单的tomcat):

1
2
3
4
5
6
HttpConnector httpConnector = new HttpConnector();
Wrapper wrapper = new Wrapper();
Loader loader = new Loader;
Valve valve1 = new 自定义的valve1();
Valve valve2 = new 自定义的valve2();
// 剩下的将它们进行组合即可。

Context

上面已经学会了仅有一个servlet的wrapper是如何工作的,但是实际中我们几乎不可能仅仅使用一个servlet来完成工作的,大部分都需要多个servlet,所以实际中最常见的是context。

而之前也提到过,context可以包含多个wrapper,所以我们要做的就是组合这些wrapper,然后抽象出一个新的组件——Mapper,针对获得的不同的request,来匹配应该用哪个servlet——路径匹配就这么诞生了。

第六章

Lifecycle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public interface Lifecycle {
String BEFORE_INIT_EVENT = "before_init";
String AFTER_INIT_EVENT = "after_init";
String START_EVENT = "start";
String BEFORE_START_EVENT = "before_start";
String AFTER_START_EVENT = "after_start";
String STOP_EVENT = "stop";
String BEFORE_STOP_EVENT = "before_stop";
String AFTER_STOP_EVENT = "after_stop";
String AFTER_DESTROY_EVENT = "after_destroy";
String BEFORE_DESTROY_EVENT = "before_destroy";
String PERIODIC_EVENT = "periodic";
String CONFIGURE_START_EVENT = "configure_start";
String CONFIGURE_STOP_EVENT = "configure_stop";

void addLifecycleListener(LifecycleListener var1);

LifecycleListener[] findLifecycleListeners();

void removeLifecycleListener(LifecycleListener var1);

void init() throws LifecycleException;

void start() throws LifecycleException;

void stop() throws LifecycleException;

void destroy() throws LifecycleException;

LifecycleState getState();

String getStateName();

public interface SingleUse {
}
}

生命周期除了上面这个,还有实现事件的LifecycleEvent类、相关的监听器接口和LifecycleSupport类。

只要我们的组件实现了这个接口,就可以有良好的生命周期管理。

第七章

日志器,没啥好说的。而且相关代码超级老旧……这里建议直接跳过。

第八章

这里开始介绍载入器,它就是一个类加载器,但是之前实现的类加载器,可以加载任意的类,显然安全性不高,这里就自己动手写一个类加载器,原因如下:

  1. 为了在载入类中指定某些规则。
  2. 为了缓存已经载入的类。
  3. 为了实现类的预载入。

Loader

tomcat有一个Loader接口来规定,有一个WebappLoader的默认实现类。下面是核心代码:

1
2
3
4
5
6
7
8
9
10
this.classLoader = this.createClassLoader();
this.classLoader.setResources(this.context.getResources());
this.classLoader.setDelegate(this.delegate);
this.setClassPath();
this.setPermissions();
this.classLoader.start();
String contextName = this.context.getName();
if (!contextName.startsWith("/")) {
contextName = "/" + contextName;
}

从上面的代码中可以发现,第一行在创建类加载器,该方法本质上是通过loaderClass这个String类型的成员变量来控制的。当然并不是随意指定一个String就可以用来当做类加载器的,最后还需要经过检查(强制类型转换)的。

第二行是在指定这个“类加载器”能够去哪里获取到这些类。

之后就是设置类路径和设置访问权限了。

第九章

这一章介绍Session管理器,Session管理器需要且必须要和一个Context容器进行关联,它负责创建、更新和销毁session。

Session对象

在servlet中,Session对象通过javax.servlet.HttpSession接口来表示,而在catalina中则是位于org.apache.catalina.session包下面,StandardSession类,则是既实现了servlet中的session接口,又实现了catalina包下的session接口。

SessionManager

所有的session都是通过SessionManager来进行管理的,比如session的过期、session的存储和加载等。

对于每一个context,都会有一个session manager,而对于request,也可以通过getContext的方法来获取这个context,相当于request就可以直接获取到session manager,自然而然也可以获取到session。

饶了这么一大个弯就是说,request -> context -> session manager -> session,然后其实request本身就贴心的封装好了,只需要request.getSession即可。

分布式

这里不得不提一句,tomcat中是有一个叫DistributedManager类来进行多台tomcat服务器之间session管理的,但是每一台服务器都必须存储所有的session,庞大臃肿速度慢,现在没人用这个,还不如忘记tomcat支持分布式的session存储。

第十章

有不少内容是需要用户登录了之后才能查看的,在Tomcat中,是通过一个叫“验证器”的阀来支持的。但是说实话,用到现在都没看到过这种登录方式的….

领域

领域对象是用来对用户进行身份认证的,通常一个领域对象和一个context相关联,而一个context只能有一个领域对象。在tomcat中,领域对象是一个Realm接口的实例,其下有很多实现类,默认是MemoryRealm类。

GenericPrincipal类

该类用来保存用户的账号和密码。

接下来的略,真的完全没用到过……

第十一章

从这里开始我们详细深入了解一下StandardWrapper。

调用链

对于每一个http请求,连接器都会调用相关联的servlet容器的invoke()方法,然后servlet容器则会调用子容器的invoke()方法,接下来就是对应的管道和管道里面的阀调用。详细的过程如下:

  1. 连接器会接受用户发来的请求,并创建request和response
  2. 连接器调用了StandardContext的invoke()方法,其实是去调用父类ContainerBase的invoke()方法,但是这里认为是StandardContext的invoke()方法也没问题。
  3. servlet容器会调用相关管道对象的invoke()方法,管道中必然存在一个基础阀,所以基础阀的invoke()方法会被调用
  4. 基础阀的invoke方法会调用对应的Wrapper实例的invoke()方法
  5. 同样的Wrapper也有管道,也有基础阀。基础阀里面会有一个allocate()能够获取到servlet实例。
  6. allocate()能够调用load()方法来载入对应的servlet类,如果存在则不需要重复载入。
  7. load()方法会调用servlet的init()方法
  8. 最后就是servlet的service()方法被调用

SingleThreadModel

servlet可以实现javax.servlet.SingleThreadModel接口,该接口从名字中也可以看出就是为了保证servlet一次只处理一个请求。可以保证绝对不会有两个线程同时执行该类的service()方法。该接口已经被弃用。

StandardWrapper

从上面调用链可知,StandardWrapper的主要任务就是载入对应的servlet类,进行实例化。注意它并不能调用servlet的service()方法,该方法是由对应的阀来完成的。实例化之后,StandardWrapperValve的对象就能够通过allocate()来获取servlet实例,并调用service()方法。

如果类没有实现上面的SingleThreadModel接口,则StandardWrapper就只会载入一次,之后对于它的任何请求都返回同一个实例(单例模式),线程安全由程序员来进行负责。而如果实现了接口,那么就需要来位于一个实例池。

下面是allocate()的逻辑伪代码:

1
2
3
4
5
6
7
if(!singleThreadModel){
return a nor-STM-instance
}else{
synchronized(pool){
pool.push(createNewServlet());
}
}

接下来就是通过loadServlet()方法来载入servlet并执行init()方法了,主要逻辑就是先判断下当前的类名,判断下当前是不是JSP页面的servlet类。然后就是获取载入器,并且获得类加载器,有了类加载器就可以载入类并对该类进行实例化操作了。有了实例之后,就可以调用它的init()方法了。

StandardWrapperValve

这个阀主要完成两个任务:执行与servlet关联的全部过滤器(看好了过滤器是在这里才开始的)和调用servlet的service()方法。为了实现这两个方法,会执行以下步骤:

  1. 获取servlet实例(就是上一步中通过allocate()方法得到的)
  2. 调用私有方法createFilterChain()来创建过滤器链
  3. 调用过滤器链的doFilter()方法,其中包含了调用servlet的service()方法(最后一个被调用)
  4. 释放过滤器链
  5. 调用Wrapper的deallocate()方法
  6. 如果该servlet不会再被用到,则调用unload()方法

为了能够了解过滤器链,首先需要介绍一下过滤器定义类——FilterDef类,我在这里稍微抽象了一下它有哪些属性:

1
2
3
4
5
private String description; // 过滤器的描述信息
private String displayName; // 该过滤器对外展示的名字
private String filterClass; // 全限定类名
private String filterName; // 过滤器真正的名字,必须唯一
private Map parameters; // 属性

有了这个类之后,可以通过ApplicationFilterConfig类来构建一个过滤器。

第十二章

这里开始详细聊一聊StandardContext类。

构造函数里面最重要的一点:this.pipeline.setBasic(new StandardContextValve());为当前的管道设置一个基本阀。

StandardContext类有两个对象属性,分别是available(目前最新版是在state这个枚举类里面,不过原理是一模一样的,姑且认为有这么一个成员变量吧)和configured(最新版还在)。其中的available指的是当前的context是否可用,而configured则指的是当前的context是否配置完成。

context的启动流程如下所示:

  1. 触发lifecycle的BEFORE_START事件。
  2. 将available设置成false
  3. 将configured设置成false
  4. 配置相关资源
  5. 设置loader
  6. 设置session manager
  7. 初始化字符集映射器
  8. 启动与context相关联的组件
  9. 启动子容器(Wrapper)
  10. 启动管道对象
  11. 启动session manager
  12. 触发START事件。在这里会执行一些配置操作,成功则会将configured置为true
  13. 如果configured为true,则说明上一步成功,那么调用postWelcomePages()方法,载入Wrapper,并将available设置成true;如果configured为false,那么调用stop()方法结束。
  14. 触发AFTER_START事件

StandardContextMapper

我们都知道context作为一个容器,必然是实现了Container接口的,而其实实际上是去继承一个抽象类ContainerBase,在这个ContainerBase里面有一个addDefaultMapper的方法(目前已经没得了),通过一个String类型的全限定类名来构造一个mapper并将其加入到当前的类中,而这个String,默认就是”org.apache.catalina.core.StandardContextMapper”(目前也已经消失了)。

这个mapper最重要的功能,就是通过传入一个request来判断应该使用哪一个container来处理它,这个功能就是map()方法。更具体的说,当StandardContextValve需要对每一个HTTP请求作出判断的时候,它会使用容器(map方法实际定义在ContainerBase中)的map()方法来找到需要的container。当然有相应的规则来匹配找到对应的wrapper,而这些规则其实就是我们自己之前定义好并且传递给容器的。

目前这个mapper已经灰飞烟灭了,实际功能被转移到了StandardContextValve中,当调用它的invoke()方法的时候,里面有一句:Wrapper wrapper = request.getWrapper();,即只需要request的信息就可以得到wrapper了。

backgroundProcess()

从名字就知道这个方法是新开一个线程并且在后台运行的,用来做一些诸如周期性检查的事务等,比如session管理器需要检查session是否过期等,而如果为了每个后台都需要开启一个线程,那么会比较浪费,所以在tomcat 5就把它们聚合在了一起,而这一方法被定义在了Container接口中,并且真正的实现在ContainerBase中。

然后在ContainerBase中还有一个内部类是ContainerBackgroundProcessor,该类实现了Runnable接口,并且在内部的run方法中,它会周期调用processChildren()方法,该方法调用自己和子容器的backgroundProcess()方法。

重载功能

这里的重载说的是当某些文件被重新编译的时候,应用程序可以及时感知到并且更新应用。这个功能当然应该交给Loader来实现了。

第十三章

之前提到了容器一共有四类,分别是Engine、Host、Context和Wrapper,之前已经详细了解了Context和wrapper,这里开始聊聊Host和Engine。

StandardHost

该类作为Host接口的标准实现,和之前的那两个标准容器一样,该类的构造函数也会添加一个默认的基础阀;与它们不同的是,当调用start()方法的时候,会额外引入两个阀,分别是ErrorReportValve(新版本里还在)和ErrorDispatcherValve(新版本里面已经没得了),至于invoke()方法,则是和之前的流程一样。

其它的流程与context几乎一模一样,这里就不多做介绍了。

Host是用来干嘛的

从上面的说明我们可以知道,host和context几乎是一模一样的东西,我们为什么还需要搞出host这个东西呢。之前说到context的时候提到,一个context是需要ContextConfig的对象来进行设置,而这个ContextConfig对象需要知道web.xml文件的位置并且读取它里面的内容。也就是如果需要使用ContextConfig对象来设置context的话,外部必须要有一个Host容器,除非你自己实现了ContextConfig。(书这里我想是不是翻译的问题啊,这理由很牵强,我个人的理解就是需要有一个抽象的概念来代表一台服务器)

Engine

Engine是一个顶层的接口,如果希望tomcat能够支持多个虚拟机的话,就需要使用engine容器。engine容器可以设置一个默认host容器或者是一个默认的context容器。engine容器作为顶层容器,并不能给它在设置父容器了,与此同时,engine容器的子容器如果存在,则必须是Host容器。

第十四章

服务器组件Server

Server接口,表示catalina的整个servlet引擎,当启动服务器组件的时候,它会启动所有的组件,并且开始等待关闭命令;如果想要关闭系统,那么就可以向指定的端口发送一条命令,这样服务器组件就会关闭所有的组件,它的存在为我们提供了一种优雅的方式来启动和关闭tomcat

而服务器组件使用了服务组件(Service)来包含其他的组件。一个服务器组件可以有0个或者多个服务组件。标准实现类是StandardServer类,该类有四个生命周期,分别是initialize(),start(),stop()和await()方法。

initialize()方法新版本更名为initInternal(),最主要的实现就是对于每一个服务组件,都调用它们的init()方法。

而start()方法则更名为startInternal(),用来调用所有的service的start方法。同理stop就不赘述了。

await方法没有改名,而且它的代码我们非常熟悉,就是之前的连接器的功能,创建一个serversocket,并且监听对应的端口(该端口默认情况下是正常关闭端口加一个偏移,正常关闭端口是8005,偏移是0,所以最后默认监听的关闭服务器端口就是8005),如果收到对应的关闭命令,就关闭。

服务组件Service

一个服务组件可以有一个servlet容器和多个连接器,可以自由的把连接器放进去,然后连接器就会自动和这个servlet容器发生关系了。由于有多个连接器,这也就意味着可以同时处理http请求和https请求。同样它也有对应的生命周期方法,包括initialize(),start(),stop()。

initialize方法就是执行所有连接器的initialize方法;start方法需要启动连接器和servlet容器,注意要先启动servlet然后启动连接器;stop方法则会停止所有的连接器和servlet容器,先关闭连接器,再关闭servlet容器。

第十五章

可以发现之前的代码中,我们都是通过硬编码来完成这些组件之间的耦合关系的,这样子不够优雅,实际中我们是通过配置文件来对Tomcat进行配置的,而Tomcat能够实现这一功能的秘诀就在于,它使用了Digester这个开源库来完成将xml对象转换成java对象这一艰巨的任务的。

这个第三方库最重要的就是Digester类,对于XML文档中的每一个元素,它都会检查它是否要做预先定义好的事件,然后在遇到对应的XML元素的时候,就执行相对应的动作。

稍微介绍一下,根元素的模式就是它自己的名字,然后根下面的一级元素的模式是根元素+"/"+子元素。然后是规则,规则就是找到了对应的元素之后应该进行的动作,规则的接口是Rule,而存储这些规则和模式的,则是一个叫Rules的东西。另外,Rule会有begin和end方法,就是当匹配到的时候触发begin方法,而结束的时候触发end方法。

当然已经有不少现成的规则供我们直接拿来使用。下面简单介绍几种:

  1. 当遇到特定的模式的时候,需要创建对象,则调用addObjectCreate()方法。比如你想当遇到<employee>这个标签的时候(这里假设模式和标签名相同),那么就可以用digester.addObjectCreate("employee",cn.clp.Employee.class);,创建的这个对象会被压入到一个内部的栈中。
  2. 当创建为类之后,我们一般还会在xml中标注好对应的属性值,可以通过addSetProperties来自动读取xml的值并且为创建好的对象进行属性赋值。类的属性名字和xml中的属性名字必须要能对的上。
  3. 我们还可以通过规则自动调用一个类的方法,通过addCallMethod可以调用内部栈最顶端对象的某个方法。
  4. 当然我们创建的对象之间也可能会有关系,所以可以用addSetNext()方法来调用相关的方法并且将对象传入。

当调用上面的这些方法的时候,都会间接地调用Digester类的addRule()方法,该方法会把Rule和对应的模式加入到Rules集合中。

ContextConfig

StandardContext实例必须要有一个监听器,这个监听器会配置好StandardContext实例,并将configured置为true。之前的ContextConfig仅仅是简简单单的将configured置为true,好让程序可以继续跑下去,但是实际上它应该要去读取web.xml文件(在CATALINE_HOME的conf目录下面),并且利用上面说过的digester将对应的xml元素变成java对象。

web.xml大概长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

可以看到有了这个可以轻松构造出一个默认的servlet,并且将下面的两个参数传递给它。这样就可以创建出对应的StandardWrapper类。所以,在启动类中,我们需要创建好这个Config类,然后将它作为监听器添加到Context中。这样只要StandContext触发了什么事件,就可以通过相应的方法来进行处理了。

第十六章

有的时候,或者说大多数时候我们其实不是按照正常流程那样,给指定端口发送关闭命令来关闭tomcat的(其实我甚至都不知道关闭端口是啥…),而是直接简单的退出。而这个时候就需要通过关闭钩子来生效。

在java中,当用户调用System.exit()方法或者最后一个非守护进程的线程退出时,应用程序正常退出,此时按照tomcat的逻辑肯定已经都释放资源、关闭一切了;也可能是通过用户强制中断java虚拟机(最常见的就是ctrl+C)来退出。但是就算你是用关闭jvm的方式来进行关闭,也要经历两个过程:

  1. 虚拟机启动所有已经注册的关闭钩子,所有的关闭钩子都会执行。
  2. 虚拟机根据情况调用所有没有被调用过的终结器(finalizer)。

而我们这里显然把关注点放到了第一阶段,通过创建关闭钩子来确保安全的关闭。方法很简单,关闭钩子本身也是一个线程,所以只需要继承一下Thread,重写run方法,并且通过Runtime.getRuntime().addShutdownHook(你的钩子);即可。

tomcat中的钩子

emmm 就是一个叫CatalinaShutdownHook的内部类,然后它会确保Catalina.this.stop();这句话被执行,然后就没了….

第十七章

这一章主要是为了讲述启动Tomcat的。Tomcat有启动的时候会用到两个类,分别是Bootstrap类和Catalina类。Catalina类是用来启动和关闭server对象,并且解析web.xml文件的。而Bootstrap则是为了创建Catalina的,所以其实两者可以合二为一,但是为了更好的扩展性和更多的运行方式,还是分开了。而且Tomcat还提供了脚本,我们仅仅需要运行这些脚本就可以执行了。

catalina本身持有一个Digester对象,能够方便对web.xml进行解析,同时有一个Server对象。

接下来开始流程分析。

在用户传入参数的时候,它会通过protected boolean arguments(String args[])来判断是否能够处理用户的命令,如果通过则返回true。

然后是通过创建一个Digester对象来解析server.xml文件,解析完成之后,server变量就能够引用一个Server对象,并且调用server的initialize和start方法,接下来就让server进入await方法,等着shutdown命令的到来。

Bootstrap

这个类里面有main方法,所以它才是真正的入口,脚本其实就是调用了它的main方法了实现的。

这个类本身有三个类加载器,分别是:

1
2
3
ClassLoader commonLoader = null;			// 对应common/classes目录下,common/endorsed目录下,common/lib目录下
ClassLoader catalinaLoader = null; // 对应server/classes目录,server/lib目录以及上面的所有
ClassLoader sharedLoader = null; // 对应shared/classes目录和shared、lib目录

这里指的注意的是,与context相关联的类载入器的父类都是sharedLoader,但是这些类载入器并不能访问catalina的内部类以及CLASSPATH环境变量中的类(为了安全考虑)。而在最新版的代码中是这个样子的:

1
2
3
4
5
6
7
8
try {
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader = this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);

可以看到和上面的描述是一致的。搞定了这三个类加载器之后,就会通过catalinaLoader加载一个新的catalina,并且将这个新的catalina的父类加载器置成sharedLoader,然后执行这个对象的process()方法。

接下来介绍了Windows平台下和linux平台下启动脚本,这里我跳过了。

第十八章

实际部署的时候,我们通常会将我们的web项目打包成war文件,放到Webapp目录下面进行部署。

而在tomcat中,则是使用一个部署器的玩意儿来进行部署。部署器的接口是Deployer,且部署器与Host实例相关联,部署器会启动一个StandardContext实例,并将该实例放入到Host中。

在实际中,会有一个HostConfig类型的生命周期监听器,当我们调用StandardHost的start()方法的时候,HostConfig会进行响应,并且它自身有一个start()方法,该方法就可以逐个部署并且安装所有Web程序。

在Host实例发出START事件之后,HostConfig首先会进行下面的操作:

1
2
3
4
setCopyXML(((StandardHost) host).isCopyXML());
setDeployXML(((StandardHost) host).isDeployXML());
setUnpackWARs(((StandardHost) host).isUnpackWARs());
setContextClass(((StandardHost) host).getContextClass());

经过了上面的检查之后,就会执行该类的start()方法,抽象一下代码可以得出其实就是一句函数:deployApps();,也就是最关键的部署其实奥秘就在这个函数里,下面来看看这个函数长什么样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void deployApps() {

File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);

}

可以看到这个方法又调用了其他三个方法,只要了解这三个方法,我们就可以了解tomcat的部署过程了。

deployDescriptors:一个Context可以使用一个xml文件来进行描述。该方法用来部署好Context所对应的xml文件。

deployWARs:部署war文件。

deployDirectories:部署目录,就是把整个目录复制到webapps目录下面。

Deployer接口

很遗憾,在tomcat9.0版本中已经不存在了,所以下面的只能是全靠书中怎么讲了。

StandardHost类本身就实现了Deployer接口,所以它本身也是一个部署器。然后它将接受到的任务委托给StandardHostDeployer类来进行处理。

安装一个描述符

这个本身是非常简单的,因为只需要通过文件的输入输出流读取到对应的文件,并且使用Digester对象来解析这个文件即可。

安装一个war

根据war的不同,可以得到它的url,然后就可以对其进行载入操作,利用Class.forName()等操作。

第十九章

1
2
3
4
5
6
7
8
9
10
11
12
<servlet>
<servlet-name>Manager</servlet-name>
<servlet-class>org.apache.catalina.manager.ManagerServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>2</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Manager</servlet-name>
<url-pattern>/text/*</url-pattern>
</servlet-mapping>

这就是本章索要描述的对象,Manager对象,上面的代码应该很熟悉,就是当你访问http://localhost:8080/text/*的时候,其实会去调用ManagerServlet这个类来完成相应的工作。

ManagerServlet

一个servlet对象通常由一个StandardWrapper实例来表示,在我们第一次调用servlet的时候,会先调用StandardWrapper的loadServlet方法,加载完成之后才是对应servlet的init()方法。而这个servlet则有那么一丝丝特殊,在加载它的时候,会执行setWrapper方法,相当于它可以持有对Wrapper的引用。

由于它持有了Wrapper,导致它可以调用一系列的方法,所以这也就是它Manager的命名的由来,我们可以通过访问它来执行一些特殊的命令,比如localhost:8080/manager/html,输入账号密码之后就可以查看了。更多的功能可自行探索。

第二十章

上一章介绍了使用ManagerServlet类来作为tomcat的管理器,接下来介绍Java Management Extensions(JMX)来进行管理tomcat。

JMX

JMX提供了比上面一章的servlet更加详细的管理方式。可以看到类名中带MBean的基本上都是可以被JMX管理的对象,而这些MBean则可以用来管理对应的java对象。

MBean统一被MBean Server所管理,接下来介绍这些不同种类的MBean。

JMX API

MBean Server可以由对应的工厂来进行创建,同时具有一个注册MBean的方法,也有查询指定MBean,或者是执行特定MBean的方法,亦或是为MBean设置属性的方法。

由于每一个MBean都注册到了MBean Server中,而MBean Server则是通过一个叫ObjectName的东西来唯一区别这些MBean的。

标准MBean

这是最为简单的一种MBean,可以通过以下步骤来创建一个标准MBean来管理对象:

  1. 为你想要控制的java对象创建一个接口,名字规范是你要管理的类+MBean,比如你要管理的类是Car,那么就请取名为CarMBean
  2. 让之前的Car类实现新创建的CarMBean接口
  3. 创建一个代理,代理中必须包含一个MBeanServer——其实就是一个普通的类,然后该类有一个成员变量是MBeanServer,且该MBean必须要有工厂来进行创建。
  4. 给MBean创建ObjectName实例(为了能够唯一区分)
  5. 实例化MBeanServer并将MBean注册到其中。

显然最大的问题就是在第二步,要是我们自己的类其实还好说,但是如果是他人已经实现的类呢?那就无法进行修改了呀。而且是现有类,然后才有接口,接口中还需要写好你想要管理该类的方法(当然一般推荐是全写上)

模型MBean

上面标准MBean的缺点是缺少灵活性,所以有了这个MBean。而所有的MBean的任务就是用来管理,所以又有了ModelMBeanInfo这个对象来完成对托管资源的属性/方法暴露给代理。

  1. 实例化一个RequiredMBean类,并且创建出对应的代理类,代理类里有对应的MBeanServer。
  2. 在代理类里面编写好我们需要管理的类(Car)所需要的各种信息,比如各类方法,属性等。
  3. 直接组合即可。

第三方库

显然我们不想把精力都花在编写需要管理类的信息上面,就有了Commons Modeler这个库。而这个库的作用就是把上面那些繁琐的属性抽象成了一个文件,我们只需要去对应的文件(mbean.xml,最新的里面叫mbeans.descriptors.xml)中写好对应的xml语句,即可自动创建出对应的ModelMBeanInfo对象了。长得像这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<mbean name="StandardEngine"
type="org.apache.catalina.core.StandardEngine"
description="Standard Engine Component"
domain="Catalina"
group="Engine"
className="org.apache.catalina.mbeans.ContainerMBean">

<attribute name="backgroundProcessorDelay"
description="The processor delay for this component."
type="int"/>

<!-- 省略了很多attribute -->

<operation name="addChild"
description="Add a virtual host"
impact="ACTION"
returnType="void">
<parameter name="type"
description="Type(classname) of the new child to be added"
type="java.lang.String"/>
<parameter name="name"
description="Name of the child to be added"
type="java.lang.String"/>
</operation>

<!-- 省略了很多operation -->

也就是一个mbean标签,下面带了很多组attribute和operation,然后operation元素里面又有很多的parameter。

有了这个描述文件,就可以通过Registry这个类来加载xml文件,并进行MBean的创建了。

重要的MBean

知道了如何创建这些mbean,这里就需要介绍一下在catalina中很重要的三个MBean,了解了它们可以很好的了解MBean的工作机制,同时也能够掌握如何管理tomcat。

ClassNameMBean

见名知意,获取托管资源的类名。

StandardServerMBean

用来管理StandardServer,是一种模型MBean。

MBeanFactory

用来创建tomcat中所有的MBean。

最后实现了一个能够查看所有MBean的MBean,有兴趣的可以自己去实现下。


全书完。