0%

Tomcat 中用到这个技术,但是很陌生,手机一下资料,整理如下。

JMX - java management extension, 简单理解就是 Java 提供了一套规范,可以在 JVM 运行时,操作对象。

示例

新建一个类并在 MBeanServer 中注册,等程序运行时,我们可以通过 jconsole 对运行时的对象进行操作

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public interface HelloMBean {
public String getName();

public void setName(String name);

public String getAge();

public void setAge(String age);

public void helloWorld();

public void helloWorld(String str);

public void getTelephone();
}

public class Hello implements HelloMBean {
private String name;
private String age;

public void getTelephone() {
System.out.println("get Telephone");
}

public void helloWorld() {
System.out.println("hello world");
}

public void helloWorld(String str) {
System.out.println("helloWorld:" + str);
}

public String getName() {
System.out.println("get name 123");
return name;
}

public void setName(String name) {
System.out.println("set name 123");
this.name = name;
}

public String getAge() {
System.out.println("get age 123");
return age;
}

public void setAge(String age) {
System.out.println("set age 123");
this.age = age;
}
}

import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;

public class main {
public static void main(String[] args) throws Exception {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName helloName = new ObjectName("jmxBean:name=hello");
//create mbean and register mbean
server.registerMBean(new Hello(), helloName);
Thread.sleep(60 * 60 * 1000);
}
}

ObjectName("jmxBean:name=hello"); 中括号中的内容类似 entrypoint 可以确定我们要监测的节点。下一句就是注册 bean 到 MBeanServer。运行程序后 thread 会 hold.

这时启动 jdk 的 bin 目录下的 jconsole 客户端,选择 main 这个 thread, 链接上去

connection

选中 MBean tab,展开 jmxBean 可以看到我们定义的 MBean

show

其他链接方式

JMX 是一个规范,除了 jconsole 这中修改方式,还是通过 web, client 等,只需要实现了对应的接口就行。暂时没用到,略。

这篇博客说的很详细,有需要可以参考一下 cnblog

官方文档:Manager - howto

长见识了,读这本书之前,完全不知道,原来 tomcat 还有集成这种功能,厉害了。看了 How To 说明。这个 Manager 给了维护人员一个快捷的通道,通过他你可以管理 Tomcat 下的各个 app 的状态,可以随时开启,停止,重新 deploy 而且你不需要问了这个目的重启整个 Tomcat。此外还可以监测各种环境数据,比如 JVM 信息,系统参数,Server 状态等。

开启

Manager 功能默认是不开启的,当你本地启动项目时,可以试着访问一下 /manager 这个路径,会给 403 err

403

提示很清楚,你要在 /conf 下配置 tomcat-user.xml 才能开启这个功能。给了 manager-gui 之后就不需要 script 和 status 权限了,这是处于安全考虑。为了测试方便也可以全部加上

1
<user username="tomcat" password="s3cret" roles="manager-gui,manager-script,manager-jmx,manager-status"/>

重启 Tomcat,访问 http://localhost:8080/manager 即可看到管理页面

status

能管理的项目 UI 展示都很直接,不细说了

Manager Commands

Manager 还支持通过 request 进行控制,你需要按照前面说的给 manager-status 这个 permission 之后才能开启。开启后,访问 http://localhost:8080/manager/text/list 就能看到效果,其实就是 UI 功能的 request 版本,细节参考开头的文档。

此外 Tomcat 还支持 Ant 脚本跑这些命令,用不到,暂时不看了。

Chapter 18 presents the deployer, the component responsible for deploying and installing web applications.

Chapter 17 discusses the starting and stopping of Tomcat through the use of batch files and shell scripts.

Tomcat 中通过两个 class 管理容器的启动分别是 Catalina 和 Bootstrap。Catalina 用来管理服务器的开启和停止,同时负责解析 server.xml 配置文件。 Bootstrap 用来创建 Catalina 实体并运行。理论上来说,这两个类可以合并,但是为了支持多模式运行,还是分开了。

这章先介绍了 Catalina 和 Bootstrap,然后介绍了管理 Tomcat 的 dot 和 shell 文件的编写

The Catalina Class

管理主入口为 process 方法,通过 Bootstrap 管理,但其实它本身就包含一个 main() 函数,调用方式和 Bootstrap 一样

1
2
3
4
5
6
7
8
9
10
11
public void process(String args[]) {

setCatalinaHome();
setCatalinaBase();
try {
if (arguments(args))
execute();
} catch (Exception e) {
e.printStackTrace(System.out);
}
}

The start Mehtod

start 方法中,会做如下事情

  1. 创建对应的 digester 对象处理 server.xml
  2. 调用解析得到的 server 容器的 start 方法
  3. 注册 shutdown hook
  4. 卡在 await 等待结束信号
  5. 正常结束时会先移掉 hook 避免重复执行

The stop Mehtod

执行原理和逻辑和 start 一致,跳过。

Start/Stop Digester

逻辑一致,创建了一个 Digester 对象,主要的逻辑都在 rule set 那一部分,很直观

The Bootstrap Class

Tomcat 通过 Bootstrap 调用 Catalina,我们平时启动用的 dot/shll 文件也是直接调用的 Bootstrap 类。其中的 main 方法创建了三个 class loader 并对 Catalina 做了实例话,最后调用了他的 process 方法启动服务器。

PS: 这部分后面可以结合第八章仔细看看,应该挺有意思

Running Tomcat on Windows/Linux

Chapter 16 explains the shutdown hook that Tomcat uses to always get a chance to do clean-up regardless how the user stops it (i.e. either appropriately by sending a shutdown command or inappropriately by simply closing the console.)

本章介绍了 Tomcat 如何优雅的执行 stop 方法,前面先通过两个例子介绍实现原理,后面分析 Tomcat 的实现方式

java 中,虚拟机可以通过两种方式 shut down

  • 调用 System.exit 结束
  • 非正常结束,比如 CTRL+C,直接关闭终端等

当虚拟机结束时,会一次执行下面两个步骤

  • jvm 会执行所有注册的 shutdown hook. 这些 hook 被 attach 在 Runtime 对象中,结束时并行执行
  • jvm 执行所有没有被调用的 finalizers 方法

本章中的例子都是针对第一点做实现的

创建一个 shutdown hook 的方式很简单,分一下几步

  • 创建 class 继承 Thread
  • run 方法中实现定制的逻辑
  • 在目标应用中,实例话你的 hook class
  • 注册 hook 到 Runtime

下面是一个示例程序, 不管怎么结束程序,ShutdownHook 的 run() 方法都会被调用到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ShutdownHookDemo {
public void start() {
System.out.println("Demo");
ShutdownHook shutdownHook = new ShutdownHook();
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
public static void main(String[] args) {
ShutdownHookDemo demo = new ShutdownHookDemo();
demo.start();
try {
System.in.read();
}catch (Exception e) {}
}
}

class ShutdownHook extends Thread {
@Override
public void run() {
System.out.println("Shutting down...");
}
}

// Demo
// Shutting down...

A Shutdown Hook Example

这里通过一个 UI 组件做例子,应用启动时会创建一个临时文件,我们的目标是,推出后,这个文件必须被删除。原始实现如下,当点击 Exit 推出时,文件可以被删除,但是如果通过点击 x 按钮推出,则文件还会保留。

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
public class MySwingApp extends JFrame {
JButton exitButton = new JButton();
JTextArea jTextArea1 = new JTextArea();
String dir = System.getProperty("user.dir");
String filename = "temp.txt";

public MySwingApp() {
exitButton.setText("Exit");
exitButton.setBounds(new Rectangle(304, 248, 76, 37));
exitButton.addActionListener(e -> exitButton_actionPerformed(e));
this.getContentPane().setLayout(null);
jTextArea1.setText("Click the Exit button to quit");
jTextArea1.setBounds(new Rectangle(9, 7, 371, 235));
this.getContentPane().add(exitButton, null);
this.getContentPane().add(jTextArea1, null);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setBounds(0, 0, 400, 330);
this.setVisible(true);
initialize();
}

private void initialize() {
// create a temp file
File file = new File(dir, filename);
try {
System.out.println("Creating temporary file");
file.createNewFile();
} catch (IOException e) {
System.out.println("Failed creating temporary file.");
}
}

private void shutdown() {
// delete the temp file
File file = new File(dir, filename);
if (file.exists()) {
System.out.println("Deleting temporary file.");
file.delete();
}
}

void exitButton_actionPerformed(ActionEvent e) {
shutdown();
System.exit(0);
}

public static void main(String[] args) {
MySwingApp mySwingApp = new MySwingApp();
}
}

改进方案是,将 shutdown() 的内容包装到单独的内部类中并实现 Thread 接口。在应用启动时,在 initialize() 方法中通过 hook 的方式注册到 Runtime。这样不管怎么退出,都能保证 shutdown() 方法会被执行

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
51
52
53
54
55
56
57
58
59
60
public class MySwingAppWithShutdownHook extends JFrame {

JButton exitButton = new JButton();
JTextArea jTextArea1 = new JTextArea();
String dir = System.getProperty("user.dir");
String filename = "temp.txt";

public MySwingAppWithShutdownHook() {
exitButton.setText("Exit");
exitButton.setBounds(new Rectangle(304, 248, 76, 37));
exitButton.addActionListener(e -> exitButton_actionPerformed(e));
this.getContentPane().setLayout(null);
jTextArea1.setText("Click the Exit button to quit");
jTextArea1.setBounds(new Rectangle(9, 7, 371, 235));
this.getContentPane().add(exitButton, null);
this.getContentPane().add(jTextArea1, null);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setBounds(0, 0, 400, 330);
this.setVisible(true);
initialize();
}

private void initialize() {
// add shutdown hook
MyShutdownHook shutdownHook = new MyShutdownHook();
Runtime.getRuntime().addShutdownHook(shutdownHook);
// create a temp file
File file = new File(dir, filename);
try {
System.out.println("Creating temporary file");
file.createNewFile();
} catch (IOException e) {
System.out.println("Failed creating temporary file.");
}
}

private void shutdown() {
// delete the temp file
File file = new File(dir, filename);
if (file.exists()) {
System.out.println("Deleting temporary file.");
file.delete();
}
}

void exitButton_actionPerformed(ActionEvent e) {
shutdown();
System.exit(0);
}

public static void main(String[] args) {
MySwingAppWithShutdownHook mySwingApp = new MySwingAppWithShutdownHook();
}

private class MyShutdownHook extends Thread {
public void run() {
shutdown();
}
}
}

Shutdown Hook in Tomcat

Tomcat 也通过同样的原理,在 Catalina 这个类中定义了一个 hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected class CatalinaShutdownHook extends Thread {

public void run() {

if (server != null) {
try {
((Lifecycle) server).stop();
} catch (LifecycleException e) {
System.out.println("Catalina.stop: " + e);
e.printStackTrace(System.out);
if (e.getThrowable() != null) {
System.out.println("----- Root Cause -----");
e.getThrowable().printStackTrace(System.out);
}
}
}

}
}

在 Catalina 运行 start() 方法的时候 attach 到 Runtime 对象中去

1
2
3
Thread shutdownHook = new CatalinaShutdownHook();
//...
Runtime.getRuntime().addShutdownHook(shutdownHook);

Chapter 15 explains the configuration of a web application through Digester, an exciting open source project from the Apache Software Foundation. For those not initiated, this chapter presents a section that gently introduces the digester library
and how to use it to convert the nodes in an XML document to Java objects. It then explains the ContextConfig object that configures a StandardContext instance.

PS: 创建完 project 后将 lib 添加到项目的 classpath 中, 不然会缺少依赖

本章先介绍一个 xml 解析工具包 Digester,它是给予 SAX 的解析工具,然后介绍了 Tomcat 中解析配置文件的类 StandardContext 和 ContextConfig。最后实验部分,可以看到主函数中我们并没有声明 StandardWrapper 但是通过解析 xml 程序还是能正常运行。

Digester

Digester 是 Apache 的 Jakarta 项目的一个子项目,是对 SAX 的封装和抽象,使用起来挺简单的,下面是几个小例子.

比如我们有一下 xml 文件并切想要将它解析成 employee 对象

1
2
3
<?xml version="1.0" encoding="ISO-8859-1"?>
<employee firstName="Brian" lastName="May">
</employee>

我们可以使用如下代码, 先定义好 employee 的结构

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
public class Employee {
private String firstName;
private String lastName;

public Employee() {
System.out.println("Creating Employee");
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
System.out.println("Setting firstName : " + firstName);
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
System.out.println("Setting lastName : " + lastName);
this.lastName = lastName;
}

public void printName() {
System.out.println("My name is " + firstName + " " + lastName);
}
}

然后调用 Digester 方法解析,主要方法解释如下

  • addObjectCreate: 添加一条创建对象的 rule,应该就是创建指定 element 的对象的意思
  • addSetProperties: 添加一条 set properties 的 rule,应该就是将属性 set 到对象里的意思
  • addCallMethod: 添加一条调用方法的 rule
  • parse: 使用定制好的 rule 解析文件流
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
public class Test01 {

public static void main(String[] args) {
String path = System.getProperty("user.dir") + File.separator + "etc";
File file = new File(path, "employee1.xml");
Digester digester = new Digester();
// add rules
digester.addObjectCreate("employee", "com.jzheng.digestertest.Employee");
digester.addSetProperties("employee");
digester.addCallMethod("employee", "printName");

try {
Employee employee = (Employee) digester.parse(file);
System.out.println("First name : " + employee.getFirstName());
System.out.println("Last name : " + employee.getLastName());
}
catch(Exception e) {
e.printStackTrace();
}
}
}

// Creating Employee
// Setting firstName : Brian
// Setting lastName : May
// My name is Brian May
// First name : Brian
// Last name : May

上面的例子是单个 node 的情况,如果 xml 是嵌套的话,可以如下处理

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="ISO-8859-1"?>
<employee firstName="Freddie" lastName="Mercury">
<office description="Headquarters">
<address streetName="Wellington Avenue" streetNumber="223"/>
</office>
<office description="Client site">
<address streetName="Downing Street" streetNumber="10"/>
</office>
</employee>

根据实际情况,将对应的 Office 和 address 的定义写出来

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
51
52
public class Office {
private Address address;
private String description;
public Office() {
System.out.println("..Creating Office");
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
System.out.println("..Setting office description : " + description);
this.description = description;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
System.out.println("..Setting office address : " + address);
this.address = address;
}
}

public class Address {
private String streetName;
private String streetNumber;

public Address() {
System.out.println("....Creating Address");
}

public String getStreetName() {
return streetName;
}

public void setStreetName(String streetName) {
System.out.println("....Setting streetName : " + streetName);
this.streetName = streetName;
}

public String getStreetNumber() {
return streetNumber;
}

public void setStreetNumber(String streetNumber) {
System.out.println("....Setting streetNumber : " + streetNumber);
this.streetNumber = streetNumber;
}

public String toString() {
return "...." + streetNumber + " " + streetName;
}
}

主体部分唯一的区别就是在嵌套对象的表示和 addSetNext 方法。当元素是嵌套在内部的元素时,通过斜杠(/)表示自元素

  • addSetNext(String pattern, String method): pattern 表示处理的对象类型,method 表示触发的方法。如 digester.addSetNext("employee/office", "addOffice"); 就表示当遇到 addOffice(Office office) 时,将两个对象关联起来
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
51
52
53
54
55
56
57
58
59
60
public class Test02 {

public static void main(String[] args) {
String path = System.getProperty("user.dir") + File.separator + "etc";
File file = new File(path, "employee2.xml");
Digester digester = new Digester();
// add rules
digester.addObjectCreate("employee", "com.jzheng.digestertest.Employee");
digester.addSetProperties("employee");
digester.addObjectCreate("employee/office", "com.jzheng.digestertest.Office");
digester.addSetProperties("employee/office");
digester.addSetNext("employee/office", "addOffice");
digester.addObjectCreate("employee/office/address", "com.jzheng.digestertest.Address");
digester.addSetProperties("employee/office/address");
digester.addSetNext("employee/office/address", "setAddress");
try {
Employee employee = (Employee) digester.parse(file);
ArrayList offices = employee.getOffices();
Iterator iterator = offices.iterator();
System.out.println("-------------------------------------------------");
while (iterator.hasNext()) {
Office office = (Office) iterator.next();
Address address = office.getAddress();
System.out.println(office.getDescription());
System.out.println("Address : " +
address.getStreetNumber() + " " + address.getStreetName());
System.out.println("--------------------------------");
}

}
catch(Exception e) {
e.printStackTrace();
}
}
}

// Creating Employee
// Setting firstName : Freddie
// Setting lastName : Mercury
// ..Creating Office
// ..Setting office description : Headquarters
// ....Creating Address
// ....Setting streetName : Wellington Avenue
// ....Setting streetNumber : 223
// ..Setting office address : ....223 Wellington Avenue
// Adding Office to this employee
// ..Creating Office
// ..Setting office description : Client site
// ....Creating Address
// ....Setting streetName : Downing Street
// ....Setting streetNumber : 10
// ..Setting office address : ....10 Downing Street
// Adding Office to this employee
// -------------------------------------------------
// Headquarters
// Address : 223 Wellington Avenue
// --------------------------------
// Client site
// Address : 10 Downing Street
// --------------------------------

当 rule 很多时,还可以将这些 rule 封装到一个类中

1
2
3
4
5
6
7
8
9
10
11
12
13
public class EmployeeRuleSet extends RuleSetBase {
public void addRuleInstances(Digester digester) {
// add rules
digester.addObjectCreate("employee", "com.jzheng.digestertest.Employee");
digester.addSetProperties("employee");
digester.addObjectCreate("employee/office", "com.jzheng.digestertest.Office");
digester.addSetProperties("employee/office");
digester.addSetNext("employee/office", "addOffice");
digester.addObjectCreate("employee/office/address", "com.jzheng.digestertest.Address");
digester.addSetProperties("employee/office/address");
digester.addSetNext("employee/office/address", "setAddress");
}
}

对应的主体简化为

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
public class Test03 {

public static void main(String[] args) {
String path = System.getProperty("user.dir") + File.separator + "etc";
File file = new File(path, "employee2.xml");
Digester digester = new Digester();
digester.addRuleSet(new EmployeeRuleSet());
try {
Employee employee = (Employee) digester.parse(file);
ArrayList offices = employee.getOffices();
Iterator iterator = offices.iterator();
System.out.println("-------------------------------------------------");
while (iterator.hasNext()) {
Office office = (Office) iterator.next();
Address address = office.getAddress();
System.out.println(office.getDescription());
System.out.println("Address : " + address.getStreetNumber() + " " + address.getStreetName());
System.out.println("--------------------------------");
}

}
catch(Exception e) {
e.printStackTrace();
}
}
}

ContextConfig

StandardContext 必须要设置一个 ContextConfig 才能正常工作,它只要干几件事,设置 flag,设置 valve,解析 xml. ContextConfig 是以 listener 的形式出现的,监听 start/stop 事件。加载 xml 的逻辑在 defaultConig() 和 applicationConfig() 中。

defaultConfig

解析 conf/web.xml 的内容

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
51
52
53
private void defaultConfig() {

// Open the default web.xml file, if it exists
File file = new File(Constants.DefaultWebXml);
if (!file.isAbsolute())
file = new File(System.getProperty("catalina.base"),
Constants.DefaultWebXml);
FileInputStream stream = null;
try {
stream = new FileInputStream(file.getCanonicalPath());
stream.close();
stream = null;
} catch (FileNotFoundException e) {
log(sm.getString("contextConfig.defaultMissing"));
return;
} catch (IOException e) {
log(sm.getString("contextConfig.defaultMissing"), e);
return;
}

// Process the default web.xml file
synchronized (webDigester) {
try {
InputSource is =
new InputSource("file://" + file.getAbsolutePath());
stream = new FileInputStream(file);
is.setByteStream(stream);
webDigester.setDebug(getDebug());
if (context instanceof StandardContext)
((StandardContext) context).setReplaceWelcomeFiles(true);
webDigester.clear();
webDigester.push(context);
webDigester.parse(is);
} catch (SAXParseException e) {
log(sm.getString("contextConfig.defaultParse"), e);
log(sm.getString("contextConfig.defaultPosition",
"" + e.getLineNumber(),
"" + e.getColumnNumber()));
ok = false;
} catch (Exception e) {
log(sm.getString("contextConfig.defaultParse"), e);
ok = false;
} finally {
try {
if (stream != null) {
stream.close();
}
} catch (IOException e) {
log(sm.getString("contextConfig.defaultClose"), e);
}
}
}
}

The applicationConfig Method

applicationConfig 的逻辑和 defaultConfig 基本一致,只是将解析的路径改了, 解析的路径为 /WEB-INF/web.xml

Creating Web Digester

创建解析用的 digester

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static Digester createWebDigester() {

URL url = null;
Digester webDigester = new Digester();
webDigester.setValidating(true);
url = ContextConfig.class.getResource(Constants.WebDtdResourcePath_22);
webDigester.register(Constants.WebDtdPublicId_22,
url.toString());
url = ContextConfig.class.getResource(Constants.WebDtdResourcePath_23);
webDigester.register(Constants.WebDtdPublicId_23,
url.toString());
webDigester.addRuleSet(new WebRuleSet());
return (webDigester);

}

WebRuleSet 中就是解析的规则,比如下面这些是用来解析 filter 的规则

1
2
digester.addObjectCreate(prefix + "web-app/filter", "org.apache.catalina.deploy.FilterDef");
digester.addSetNext(prefix + "web-app/filter", "addFilterDef", "org.apache.catalina.deploy.FilterDef");

整个程序段应该是解析之前,通过 digester.push(context) 将要解析的对象塞进去,然后通过 parse(is) 关联起来的

PS: Digester 和 SAX 的整合关系分析,有兴趣可以做一做

Chapter 14 offers the server and service components. A server provides an elegant start and stop mechanism for the whole servlet container, a service serves as a holder for a container and one or more connectors. The application accompanying this chapter shows how to use a server and a service.

前面的实验中,一个 container 只能和一个 connector 关联,并且启动和停止时通过多条指令完成的,Server + Service 可以帮助你更优雅的管理这些服务。

Server & Implementation

Server 表示的是 Catalina servlet container 以及所属的所有子 component。它提供了一种优雅的方式管理所有服务的开启和停止。

官方定义如下:A Server element represents the entire Catalina servlet container.

对应的是现实 org.apache.catalina.core.StandardServer,同时还实现了 Lifecycle 接口(start/stop 方法),以 initialize 方法为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Invoke a pre-startup initialization. This is used to allow connectors
* to bind to restricted ports under Unix operating environments.
*/
public void initialize()
throws LifecycleException {
if (initialized)
throw new LifecycleException (
sm.getString("standardServer.initialize.initialized"));
initialized = true;

// Initialize our defined Services
for (int i = 0; i < services.length; i++) {
services[i].initialize();
}
}

类似的还有 start, stop 方法,处理的逻辑都是类似的,先发送对应的 event,然后 for 循环调用 service 对应的方法

The await Method

这个方法用来监听停止信号,当条件达成时跳出循环,这个方法是用来代替原来例子中的 System.in.read() 方法的

Service & Implementation

A Service is a group of one or more Connectors that share a single Container to process their incoming requests.

Service 可以持有多个 connector 和一个 container。多个 connector 可以用来匹配多种 protocol,比如 http 和 https。实现类为 org.apache.catalina.core.StandardService.

1
2
private Connector connectors[] = new Connector[0];
private Container container = null;

service 在 setContainer 时会调用 container 的 start() 方法,并将它关联到 connector

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
public void setContainer(Container container) {

Container oldContainer = this.container;
if ((oldContainer != null) && (oldContainer instanceof Engine))
((Engine) oldContainer).setService(null);
this.container = container;
if ((this.container != null) && (this.container instanceof Engine))
((Engine) this.container).setService(this);
if (started && (this.container != null) &&
(this.container instanceof Lifecycle)) {
try {
((Lifecycle) this.container).start();
} catch (LifecycleException e) {
;
}
}
synchronized (connectors) {
for (int i = 0; i < connectors.length; i++)
connectors[i].setContainer(this.container);
}
if (started && (oldContainer != null) &&
(oldContainer instanceof Lifecycle)) {
try {
((Lifecycle) oldContainer).stop();
} catch (LifecycleException e) {
;
}
}

// Report this property change to interested listeners
support.firePropertyChange("container", oldContainer, this.container);
}

同样的,在 addConnector() 的时候,会将它和 container 关联起来

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
public void addConnector(Connector connector) {

synchronized (connectors) {
connector.setContainer(this.container);
connector.setService(this);
Connector results[] = new Connector[connectors.length + 1];
System.arraycopy(connectors, 0, results, 0, connectors.length);
results[connectors.length] = connector;
connectors = results;

if (initialized) {
try {
connector.initialize();
} catch (LifecycleException e) {
e.printStackTrace(System.err);
}
}

if (started && (connector instanceof Lifecycle)) {
try {
((Lifecycle) connector).start();
} catch (LifecycleException e) {
;
}
}

// Report this property change to interested listeners
support.firePropertyChange("connector", null, connector);
}

}

Service 也实现了 Lifecycle 接口,在 initialize() 和 start() 方法中会调用 connector 和 container 对应的方法。

The Application

Bootstrap 实现和前面基本一致,最大的区别是在 Engine 声明之后,声明了这节介绍的 Server 和 Service 作为管理 Engine 的容器

1
2
3
4
5
Service service = new StandardService();
service.setName("Stand-alone Service");
Server server = new StandardServer();
server.addService(service);
service.addConnector(connector);

并且使用 await 代替 System.in.read();

Chapter 13 presents the two other containers: host and engine. You can also find the standard implementation of these two containers: org.apache.catalina.core.StandardHost and org.apache.catalina.core.StandardEngine.

介绍了另外两个 Container 概念:engine 和 host。如果你的系统需要有一个以上的 context,那你就需要 host 了。如果只有一个 context,理论上可以不用 host。

The Host Interface

Container 的一个接口实现,最重要的方法为 map

1
2
3
4
5
6
7
/**
* Return the Context that would be used to process the specified
* host-relative request URI, if any; otherwise return <code>null</code>.
*
* @param uri Request URI to be mapped
*/
public Context map(String uri);

StandardHost

Host 的具体实现,也没什么新鲜的,还是 Container + pipeline + valve 三件套。

1
2
3
4
public StandardHost() {
super();
pipeline.setBasic(new StandardHostValve());
}

在它的 start() 方法中还会添加两个新的 valve 进 pipeline, 一个是 ErrorReportValve 另一个是 ErrorDispatcherValve

StandardHost 的 map() 用来寻找匹配的 context。

PS: Tomcat 5 已经不用 Mapper 机制的,直接从 request 中找到正确的 context

StandardHostMapper

StandardHostValve

里面有涉及 Session 的操作,挺有意思

Why You Cannot Live without a Host

如果你的应用使用了默认的 ContextConfig 作为配置的对象,那你必须创建 Host。因为它的加载配置文件的方法 applicationConfig() 的实现如下

1
2
3
4
5
6
URL url = servletContext.getResource(Constants.ApplicationWebXml);

InputSource is = new InputSource(url.toExternalForm());
is.setByteStream(stream);
//...
webDigester.parse(is);

servletContext 的实现如下

1
2
3
4
5
6
7
8
public URL getResource(String path) throws MalformedURLException {
DirContext resources = context.getResources();
if (resources != null) {
String fullPath = context.getName() + path;

// this is the problem. Host must not be null
String hostName = context.getParent().getName();
//...

最后一行表示,当使用 ContextConfig 时,你必须有一个 Host 类型的 parent

Bootstrap1

使用 Host 的案例

The Engine Interface

Engin 表示整个 Catalina servlet engine, 让你想要你的应用有多个 Host 的时候可以使用它。

对应的实现是 org.apache.catalina.core.StandardEngine。和其他 Container 实现相比,StandardEngine 要单薄的多。套路还是一样,结合 valve 使用。在 addChild() 时,如果不是 Host 类型的 container 会抛异常。 setParent() 时,由于它是顶层 Container,setParent() 会抛异常。

问题

按理来说,学到这里应该就可以设置一个网站,多个域名了,怎么实现?

Chapter 12 covers the org.apache.catalina.core.StandardContext class that represents a web application. In particular this chapter discusses how a StandardContext object is configured, what happens in it for each incoming HTTP request, how it supports automatic reloading, and how Tomcat 5 shares a thread that executes periodic tasks in its associated components.

本章沿用 11 章的代码,主要介绍一下内容

  • StanadradContextMapper 和 ContextConfig
  • Http request 接收到之后的调用链
  • StandardContext 中的重要属性
  • Tomcat 5 中的 backgroundProcess

StandardContext Configuration

创建完 StandardContext 的实例后必须调用一下他的 start() 方法,这个过程中他会做以下事情

  • 置位 available flag
  • 读取 CATALINA_HOME/conf 路径下的配置文件
  • 在 listener 中进行 context 的配置

细节在 15 章中再讲

StandardContext Class’s Constructor

构造函数,设置默认的 valve

1
2
3
4
5
public StandardContext() {
super();
pipeline.setBasic(new StandardContextValve());
namingResources.setContainer(this);
}

Starting StandardContext

只要做了 flag 置位和 listener 的处理,代码很清楚,主要做了如下事情

  • fire BEFORE_START event
  • availability flag 置位
  • configured flag 置位
  • set resources
  • set manager
  • init character set manager
  • 启动 context 相关联的其他 component
  • 启动子 container
  • 启动 pipeline
  • 启动 manager
  • fire START event, listener(ContextConfig) 会进行一些配置,成功了之后 configured flag 置位
  • 如果 configured 为 true,进行一些其他配置工作
  • fire AFTER_START event

PS: Tomcat 5 中逻辑基本一致,此外还增加了 JMX 相关的代码

StandardContextMapper

StandardContext 的 invoke 方法调用时,会调用自己的 StandardContextValve 的 invoke 方法,它做的第一件事是拿到处理 request 匹配的 wrapper。

ContainerBase 类中有 addDefaultMapper() 方法,实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void addDefaultMapper(String mapperClass) {
// Do we need a default Mapper?
if (mapperClass == null)
return;
if (mappers.size() >= 1)
return;

// Instantiate and add a default Mapper
try {
Class clazz = Class.forName(mapperClass);
Mapper mapper = (Mapper) clazz.newInstance();
mapper.setProtocol("http");
addMapper(mapper);
} catch (Exception e) {
log(sm.getString("containerBase.addDefaultMapper", mapperClass),
e);
}
}

StandardContext 中定义了对应的 mapperClass 类

1
private String mapperClass = "org.apache.catalina.core.StandardContextMapper"

Mapper 中最核心的方法为 map 方法 public Container map(Request request, boolean update) 返回 request 对应的 wrapper。mapper 中会一次通过四种筛选条件过滤出目标 wrapper

  • 精确匹配
  • 前缀匹配
  • 后缀匹配
  • 默认匹配

如果还是没找到,就返回 null. Tomcat 5 中做了改进,直接从 request 中可以拿到对应的 wrapper

1
Wrapper wrapper = request.getWrapper();

Support for Reloading

StandardContext 中又一个 reloadable 属性,当 web.xml 改变或者 WEB-INF/classes 文件夹下文件发生改变时,这个 flag 会置位

StandardContext 的 loader - WebappLoader 在执行 setContainer 时会启动一个线程,当上述目录的文件发生改变,loader 会重新加载 application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Set the Container with which this Logger has been associated.
*
* @param container The associated Container
*/
public void setContainer(Container container) {

// Deregister from the old Container (if any)
if ((this.container != null) && (this.container instanceof Context))
((Context) this.container).removePropertyChangeListener(this);

// Process this property change
Container oldContainer = this.container;
this.container = container;
support.firePropertyChange("container", oldContainer, this.container);

// Register with the new Container (if any)
if ((this.container != null) && (this.container instanceof Context)) {
setReloadable( ((Context) this.container).getReloadable() );
((Context) this.container).addPropertyChangeListener(this);
}

}

最后一段的 setReloadable() 方法实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Set the reloadable flag for this Loader.
*
* @param reloadable The new reloadable flag
*/
public void setReloadable(boolean reloadable) {

// Process this property change
boolean oldReloadable = this.reloadable;
this.reloadable = reloadable;
support.firePropertyChange("reloadable",
new Boolean(oldReloadable),
new Boolean(this.reloadable));

// Start or stop our background thread if required
if (!started)
return;
if (!oldReloadable && this.reloadable)
threadStart();
else if (oldReloadable && !this.reloadable)
threadStop();

}

threadStart() 会启动一个线程,持续监测 WEB-INF 文件夹下文件的时间戳,threadStop() 则用来停止这个线程

PS:Tomcat 5 中这个监测过程使用专门的 backgroundProcess 来完成

The backgroundProcess Method

PS:这本书看完之后,为了巩固他,可以看看 Stackoverflow 上 Tomcat 相关的问题,找找灵感

Chapter 11 explains in detail the org.apache.catalina.core.StandardWrapper class that represents a servlet in a web application. In particular, this chapter explains how filters and a servlet’s service method are invoked. The application accompanying this chapter uses StandardWrapper instances to represents servlets.

本章主要介绍 Tomcat 的默认 Wrapper 实现,已经相关的接口 SingleThreadModel

Sequence of Methods Invocation

当 connector 收到一个 HTTP request 之后,调用链如下

method invocation

主要步骤:

  • connector 创建 request/response 实例
  • connector 调用 StandardContext 的 invoke 方法
  • StandardContext 调用 StandardContextValve 的 invoke 方法
  • StandardContextValve 找到对应的 wrapper 并调用其 invoke 方法
  • wrapper 调用对应的 StandardWrapperValve 的 invoke 方法
  • StandardWrapperValve 调用 wrapper 的 allocate() 方法加载 servlet 实例
  • allocate 调用 load 方法加载 servlet
  • load 方法调用 servlet 的 init 方法
  • StandardWrapperValve 调用 servlet 的 service 方法

SingleThreadModel

servlet 可以选择实现 javax.servlet.SingleThreadMode 接口,实现了该接口的 class 称为 SingleThreadModel (STM) servlet。

Servlet 2.4 specification 中关于这个借口的描述如下:

If a servlet implements this interface, you are guaranteed that no two threads will execute concurrently in a servlet’s service method. The servlet container can guarantee this by synchronizing access to a single instance of the servlet,
or by maintaining a pool of servlet instances and dispatching each new request to a free servlet. This interface does not prevent synchronization problems that result from servlets accessing shared resources such as static class variables or classes outside the scope of the servlet.

实现这个接口可以保证再一个时间点上,只有一个 servlet 被处理。实现方式可能是 synchronized 或者 pool,但这并不意味着保证线程安全。比如一些静态变量,或者共享资源的调用还是有可能会长生多线程问题的。

It is true that by implementing SingleThreadModel no two threads will execute a servlet’s service method at the same time. However, to enhance performance the servlet container can create multiple instances of an STM servlet. That means, the STM servlet’s service method can be executed concurrently in different instances. This will introduce synchronization problems if the servlet need to access static class variables or other resources outside the class.

StandardWrapper

StandardWrapper 的职责是加载对应的 servlet 并创建实例,调用 servlet 的 service 方法并不在它的职责范围内,是由 StandardWrapperValve 完成的。具体的调用点是在 ApplicationFilterChain 类中。

加载在 StandardWrapper#loadServlet 中完成,过程的后段会置位 singleThreadModel flag

1
2
3
4
5
6
// Register our newly initialized instance
singleThreadModel = servlet instanceof SingleThreadModel;
if (singleThreadModel) {
if (instancePool == null)
instancePool = new Stack();
}

StandardWrapper#allocate 实现如下, 主要逻辑如下

  1. 加载目标 servlet
  2. 置位 singleThreadModel flag
  3. 根据 flag 选择对应的维护 servlet 的形式,单例模式/Pool
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public Servlet allocate() throws ServletException {

if (debug >= 1)
log("Allocating an instance");

// If we are currently unloading this servlet, throw an exception
if (unloading)
throw new ServletException
(sm.getString("standardWrapper.unloading", getName()));

// If not SingleThreadedModel, return the same instance every time
if (!singleThreadModel) {

// Load and initialize our instance if necessary
if (instance == null) {
synchronized (this) {
if (instance == null) {
try {
instance = loadServlet();
} catch (ServletException e) {
throw e;
} catch (Throwable e) {
throw new ServletException
(sm.getString("standardWrapper.allocate"), e);
}
}
}
}

if (!singleThreadModel) {
if (debug >= 2)
log(" Returning non-STM instance");
countAllocated++;
return (instance);
}

}

synchronized (instancePool) {

while (countAllocated >= nInstances) {
// Allocate a new instance if possible, or else wait
if (nInstances < maxInstances) {
try {
instancePool.push(loadServlet());
nInstances++;
} catch (ServletException e) {
throw e;
} catch (Throwable e) {
throw new ServletException
(sm.getString("standardWrapper.allocate"), e);
}
} else {
try {
instancePool.wait();
} catch (InterruptedException e) {
;
}
}
}
if (debug >= 2)
log(" Returning allocated STM instance");
countAllocated++;
return (Servlet) instancePool.pop();

}

}

StandardWrapperValve

StandardWrapperValve 是 StandardWrapper 的默认的 valve 实现,这个设置在 StandardWrapper 的构造函数中

1
2
3
4
public StandardWrapper() {
super();
pipeline.setBasic(new StandardWrapperValve());
}

主要干两件事

  • 执行 servlet 相关的所有 filters
  • 调用 sender 的 service 方法

在这个 valve 的 invoke 中具体做了如下事情

  • 调用 StandardWrapper 的 allocate 方法,拿到 servlet 的实例
  • 调用 createFilterChain 创建 filter chain
  • 调用 chain 的 doFilter 方法,过程中会调用 servlet 的 service 方法
  • 释放 filter chain
  • 调用 wrapper 的 deallocate 方法
  • 如果 servlet 永远不可用了,调用 wrapper 的 unload 方法

FilterDef

org.apache.catalina.deploy.FilterDef 表示配置文件中的 filter 定义,他的每一个 property 都代表文件中该元素的一个可配置项。

1
2
3
4
5
6
7
8
9
public final class FilterDef {
private String description = null;
// getter and setter
private String displayName = null;
// getter and setter
private String filterClass = null;
// getter and setter
//...
}

ApplicationFilterConfig

org.apache.catalina.core.ApplicationFilterConfig 实现了 javax.servlet.FilterConfig 接口,管理 web application 启动时创建的 filter 实例。构造函数如下

1
2
3
public ApplicationFilterConfig(Context context, FilterDef filterDef)
throws ClassCastException, ClassNotFoundException, IllegalAccessException, InstantiationException, ServletException {
}

context 代表 web application, FilterDef 代表 filter 的定义。他有一个 getFilter() 方法可以返回 javax.servlet.Filter 对象实例

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
Filter getFilter() throws ClassCastException, ClassNotFoundException,
IllegalAccessException, InstantiationException, ServletException {

// Return the existing filter instance, if any
if (this.filter != null)
return (this.filter);

// Identify the class loader we will be using
String filterClass = filterDef.getFilterClass();
ClassLoader classLoader = null;
if (filterClass.startsWith("org.apache.catalina."))
classLoader = this.getClass().getClassLoader();
else
classLoader = context.getLoader().getClassLoader();

ClassLoader oldCtxClassLoader =
Thread.currentThread().getContextClassLoader();

// Instantiate a new instance of this filter and return it
Class clazz = classLoader.loadClass(filterClass);
this.filter = (Filter) clazz.newInstance();
filter.init(this);
return (this.filter);

}

ApplicationFilterChain

org.apache.catalina.core.ApplicationFilterChain class 实现了 javax.servlet.FilterChain 接口,StandardWrapperValve 创建这个 chain 的实例并调用 doFilter 方法,doFilter 签名如下

1
void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException;

原理和 Valve 一致也用了 责任链 模式,下面是一个实现的例子

1
2
3
4
5
6
public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain) throws IOException, ServletException {
// do something here
...
chain.doFilter(request, response);
}

The Application

和之前的章节基本一致,最大的区别是在 Bootstrap 中使用了默认的 StandardWrapper 作为 wrapper 的实现

1
2
3
4
5
6
Wrapper wrapper1 = new StandardWrapper();
wrapper1.setName("Primitive");
wrapper1.setServletClass("PrimitiveServlet");
Wrapper wrapper2 = new StandardWrapper();
wrapper2.setName("Modern");
wrapper2.setServletClass("ModernServlet")

Issue

setup 项目之后,访问 URL,抛异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
StandardWrapperValve[Primitive]: Allocate exception for servlet Primitive
javax.servlet.ServletException: Error allocating a servlet instance
javax.servlet.ServletException: Error allocating a servlet instance
at org.apache.catalina.core.StandardWrapper.allocate(StandardWrapper.java:654)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:137)
at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:642)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:479)
at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:993)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:185)
at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:642)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:479)
at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:993)
at org.apache.catalina.core.StandardContext.invoke(StandardContext.java:2377)
at org.apache.catalina.connector.http.HttpProcessor.process(HttpProcessor.java:972)
at org.apache.catalina.connector.http.HttpProcessor.run(HttpProcessor.java:1085)
at java.lang.Thread.run(Thread.java:748)

可以将 StandardWrapper 的 loadServlet() 中 SystemLogHandler 相关的方法注释掉即可。这个应该是 log 相关的操作,功能上没什么影响

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
SystemLogHandler.startCapture();
// ...
// } finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
if (getServletContext() != null) {
getServletContext().log(log);
} else {
out.println(log);
}
}
// }

思考

最后 Chain 相关的章节,在返回 chain 实例的时候用的是 servlet 包下 filter 的实例,这部分配合的部分可以深入探究一下

Filter Vs Valve: 在 tomcat 中效果是一样的,但是 Filter 更像是一个行业标准,Jetty 中也可以用,Valve 更像是一个 Tomcat 实现,其他框架中是没有的。