0%

Chapter 6 explains the Lifecycle interface.
This interface defines the lifecycle of a Catalina component and provides an elegant way of notifying other components of events that occur in that component.
In addition, the Lifecycle interface provides an elegant mechanism for starting and stopping all the components in Catalina by one single start/stop.

Catalina 是由多个模块组成的,当 Catalina 启动时,这些模块也要一起启动,停止时也是。比如 destroy 所有的 servlet,将 session 存到二级缓存等。Catalina 通过 Lifecycle 接口管理这些事件。

实现了 Lifecycle 的组件可以出发以下事件

  • BEFORE_START_EVENT
  • START_EVENT
  • AFTER_START_EVENT
  • BEFORE_STOP_EVENT
  • STOP_EVENT
  • AFTER_STOP_EVENT

LifecycleEvent 这个接口表示上面这些事件。这些事件可以由 LifecycleListener 监听。本章会介绍上面这些类,介绍 LifecycleSupport,他可以帮助组件处理事件和监听器。

理解本章的关键点是理解如下概念

Lifecycle: 实现这个接口的 component 可以发送 event

LifecycleEvent: 代表具体的 event 事件,比如 开始,停止之类的

LifecycleListener: 监听事件的类

LifecycleSupport: Util 类,提供简化处理事件的方法. 一个实现了 Lifecycle 的 class 如果要添加 Listener 就得内部创建一些容器,比如 ArrayList 管理这些 Listener。LifecycleSupport 就是用来代替这些容器的。

The Lifecycle Interface

Catalina 允许一个 component 包含另一个 component。比如 container 可以包含 loader,manager 等。父组件需要管理子组件的起止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Lifecycle {
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";

void addLifecycleListener(LifecycleListener var1);

LifecycleListener[] findLifecycleListeners();

void removeLifecycleListener(LifecycleListener var1);

void start() throws LifecycleException;

void stop() throws LifecycleException;
}

start 和 stop 是其中的核心方法,component 提供对应的实现,parent component 可以 调用start/stop 方法。其他三个方法是用来监听事件的。

The LifecycleEvent Class

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 final class LifecycleEvent extends EventObject {

public LifecycleEvent(Lifecycle lifecycle, String type) {
this(lifecycle, type, null);
}

public LifecycleEvent(Lifecycle lifecycle, String type, Object data) {
super(lifecycle);
this.lifecycle = lifecycle;
this.type = type;
this.data = data;
}

private Object data = null;

private Lifecycle lifecycle = null;

private String type = null;

public Object getData() {
return (this.data);
}

public Lifecycle getLifecycle() {
return (this.lifecycle);
}

public String getType() {
return (this.type);
}
}

The LifecycleListener Interface

1
2
3
4
5
6
7
8
public interface LifecycleListener {
/**
* Acknowledge the occurrence of the specified event.
*
* @param event LifecycleEvent that has occurred
*/
public void lifecycleEvent(LifecycleEvent event);
}

The LifecycleSupport Class

这个 support 类内部声明了一个 Array 变量存储要操作的 Listener: private LifecycleListener listeners[] = new LifecycleListener[0];. 主就三个方法:

  • addLifecycleListener
  • findLifecycleListeners
  • fireLifecycleEvent
  • removeLifecycleListener

addLifecycleListener(LifecycleListener listener) 被调用时,老的 listener list size + 1, 老的 listener 被拷贝,最后再加上新的这个 listener。

removeLifecycleListener(LifecycleListener listener): 遍历所有的 listener 如果有则删除,最后新建一个 size - 1 的 list,将原来的 list 拷贝进去

The Application

实验环境是在 ex05 之上,删掉了额外 valves,context 实现了 Lifecycle 和 listener 接口。

启动项目可以看到如下 log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HttpConnector Opening server socket on all host IP addresses
HttpConnector[8080] Starting background thread
SimpleContextLifecycleListener's event before_start
Starting SimpleLoader
Starting Wrapper Primitive
Starting Wrapper Modern
SimpleContextLifecycleListener's event start
Starting context.
SimpleContextLifecycleListener's event after_start

SimpleContextLifecycleListener's event before_stop
SimpleContextLifecycleListener's event stop
Stopping context.
Stopping wrapper Primitive
Stopping wrapper Modern
SimpleContextLifecycleListener's event after_stop

问题

Q: SimpleContext 中 fire 的 event 是什么时候被 listener 执行的,代码在哪里

A: 在 SimpleContext 的 start() 里。但是和我臆想的事件处理不同,感觉这最多是个伪事件。我本以为他会有一个多线程之类的东西。
实际处理的时候,start() 方法一开始就通过 support 发出了一个 before 的 event,support 发送的时候会调用 listener 实现对应的逻辑。说到底还是串行操作。

Q: 这个 event 和 listener 是不是用了观察这模式啊,复习一下

A: 貌似没有,没看出来

解构

复盘一下这个 Lifecycle 的原理。Tomcat 提供了一个机制,通过套娃的方式,让所有相关的组件知道某个事件发生了,并让他可以采取相应的动作。

PS: 解构的时候才注意到,start() 和 stop() 方法中操作的 component 对象,顺序上是相反的,666

这里当 event 发生时,有两种对象需要进行操作,一种是 Component,代表 Tomcat 里的 service 组件。另一种是 Listener,我的理解是,独立于 Tomcat 之外的一些 service,比如 log 之类的东西。

Listener 说白了就是定义了一个接口,接受 event 作为参数,实现中通过 if-else 判断 event 类型并采取对应的行为

1
2
3
4
5
6
7
8
public interface LifecycleListener {
/**
* Acknowledge the occurrence of the specified event.
*
* @param event LifecycleEvent that has occurred
*/
public void lifecycleEvent(LifecycleEvent event);
}

没有理解透彻,不能很顺利的重构出这个模型,但是理解层面的话已经做到了。

What is IPv6

Internet Protocol version 6: 网际协议第六版,用于解决 IPv4 地址枯竭的问题

格式: IPv6二进位制下为128位长度,以16位为一组,每组以冒号“:”隔开,可以分为8组,每组以4位十六进制方式表示。例如:2001:0db8:86a3:08d3:1319:8a2e:0370:7344 是一个合法的IPv6地址。

同时IPv6在某些条件下可以省略:

每项数字前导的0可以省略,省略后前导数字仍是0则继续,例如下组IPv6是等价的。

  • 2001:0db8:02de:0000:0000:0000:0000:0e13
  • 2001:db8:2de:0000:0000:0000:0000:e13
  • 2001:db8:2de:000:000:000:000:e13
  • 2001:db8:2de:00:00:00:00:e13
  • 2001:db8:2de:0:0:0:0:e13

可以用双冒号“::”表示一组0或多组连续的0,但只能出现一次:如果四组数字都是零,可以被省略。遵照以上省略规则,下面这两组IPv6都是相等的。

  • 2001:db8:2de:0:0:0:0:e13
  • 2001:db8:2de::e13
  • 2001:0db8:0000:0000:0000:0000:1428:57ab
  • 2001:0db8:0000:0000:0000::1428:57ab
  • 2001:0db8:0:0:0:0:1428:57ab
  • 2001:0db8:0::0:1428:57ab
  • 2001:0db8::1428:57ab

2001::25de::cade 是非法的,因为双冒号出现了两次。它有可能是下种情形之一,造成无法推断。

  • 2001:0000:0000:0000:0000:25de:0000:cade
  • 2001:0000:0000:0000:25de:0000:0000:cade
  • 2001:0000:0000:25de:0000:0000:0000:cade
  • 2001:0000:25de:0000:0000:0000:0000:cade

如果这个地址实际上是IPv4的地址,后32位可以用10进制数表示;因此::ffff:192.168.89.9 相等于::ffff:c0a8:5909。另外,::ffff:1.2.3.4 格式叫做IPv4映射地址。

How to test

本地怎么模拟一个 IPv6 的地址做测试?

避免请求发送者和接受者的耦合,让多个对象都有可能接受请求,将这些对象连成一条链,并且沿着这条链传递请求,直到有对象处理它为止

典型应用:

  • JS 中的冒泡事件
  • tomcat 中对 Encoding 的处理
  • servlet 的 filter
  • Handler(抽象处理者): 定义一个处理请求的接口,提供对后续处理者的引用
  • ConcreteHandler(具体处理者): 抽象处理者的子类,处理用户请求,可选择将请求处理掉或者传给下家;在具体处理者中可以访问链中的下一个对象,以便请求的转发。

Handler 示例

示例描述:定义一个 handler 的抽象类以及三个对应的实现类。抽象类中定义 handleRequest() 方法作为统一的处理入口,最后创建一个 ChainClient 建立处理链并测试

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
69
70
71
72
73
74
75
76
public abstract class AbstractHandler {
private AbstractHandler handler;

public abstract void handleRequest(String condition);

public AbstractHandler getHandler() {
return handler;
}

public void setHandler(AbstractHandler handler) {
this.handler = handler;
}
}

public class ConcreteHandlerA extends AbstractHandler {
@Override
public void handleRequest(String condition) {
if (condition.equals("A")) {
System.out.println("Concrete Handler A processed...");
} else {
System.out.println("Concrete Handler A can't process, call other handler...");
getHandler().handleRequest(condition);
}
}
}

public class ConcreteHandlerB extends AbstractHandler {
@Override
public void handleRequest(String condition) {
if (condition.equals("B")) {
System.out.println("Concrete Handler B processed...");
} else {
System.out.println("Concrete Handler B can't process, call other handler...");
getHandler().handleRequest(condition);
}
}
}

public class ConcreteHandlerZ extends AbstractHandler {
@Override
public void handleRequest(String condition) {
// 一般就是最后一个处理器
System.out.println("Concrete handler z processed...");
}
}

public class ChainClient {
public static void main(String[] args) {
AbstractHandler handlerA = new ConcreteHandlerA();
AbstractHandler handlerB = new ConcreteHandlerB();
AbstractHandler handlerZ = new ConcreteHandlerZ();

// 设置链顺序
handlerA.setHandler(handlerB);
handlerB.setHandler(handlerZ);

System.out.println("--------------- handle A ---------------");
handlerA.handleRequest("A");
System.out.println("--------------- handle B ---------------");
handlerA.handleRequest("B");
System.out.println("--------------- handle Z ---------------");
handlerA.handleRequest("Z");
}
}

// 执行结果
//
// --------------- handle A ---------------
// Concrete Handler A processed...
// --------------- handle B ---------------
// Concrete Handler A can't process, call other handler...
// Concrete Handler B processed...
// --------------- handle Z ---------------
// Concrete Handler A can't process, call other handler...
// Concrete Handler B can't process, call other handler...
// Concrete handler z processed...

Log 示例

目标:实现一个 log 机制,当 log level 比当前 log 类的 level 高是,记录它

这个示例的套路和上一个很类似,但是它把处理逻辑固定了,在抽象类中做了实现,每个具体的类只定制自己的 write 行为即可,和上一个在思路上有些许的不同

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public abstract class AbstractLogger {
public static int INFO = 1;
public static int DEBUG = 2;
public static int ERROR = 3;

protected int level;

//责任链中的下一个元素
protected AbstractLogger nextLogger;

public void setNextLogger(AbstractLogger nextLogger){
this.nextLogger = nextLogger;
}

public void logMessage(int level, String message){
if(this.level <= level){
write(message);
}
if(nextLogger !=null){
nextLogger.logMessage(level, message);
}
}

abstract protected void write(String message);
}

public class ConsoleLogger extends AbstractLogger {
public ConsoleLogger(int level) {
this.level = level;
}

@Override
protected void write(String message) {
System.out.println("Standard Console::Logger: " + message);
}
}

public class ErrorLogger extends AbstractLogger {
public ErrorLogger(int level){
this.level = level;
}

@Override
protected void write(String message) {
System.out.println("Error Console::Logger: " + message);
}
}

public class FileLogger extends AbstractLogger {
public FileLogger(int level) {
this.level = level;
}

@Override
protected void write(String message) {
System.out.println("File::Logger: " + message);
}
}

public class ChainPatternDemo {
private static AbstractLogger getChainOfLoggers(){

AbstractLogger errorLogger = new ErrorLogger(AbstractLogger.ERROR);
AbstractLogger fileLogger = new FileLogger(AbstractLogger.DEBUG);
AbstractLogger consoleLogger = new ConsoleLogger(AbstractLogger.INFO);

errorLogger.setNextLogger(fileLogger);
fileLogger.setNextLogger(consoleLogger);

return errorLogger;
}

public static void main(String[] args) {
AbstractLogger loggerChain = getChainOfLoggers();

System.out.println("--------------- handle info ---------------");
loggerChain.logMessage(AbstractLogger.INFO, "This is an information.");

System.out.println("--------------- handle debug ---------------");
loggerChain.logMessage(AbstractLogger.DEBUG, "This is a debug level information.");

System.out.println("--------------- handle Error ---------------");
loggerChain.logMessage(AbstractLogger.ERROR, "This is an error information.");
}
}

// --------------- handle info ---------------
// Standard Console::Logger: This is an information.
// --------------- handle debug ---------------
// File::Logger: This is a debug level information.
// Standard Console::Logger: This is a debug level information.
// --------------- handle Error ---------------
// Error Console::Logger: This is an error information.
// File::Logger: This is an error information.
// Standard Console::Logger: This is an error information.

Chapter 5 discusses the container module.
A container is represented by the org.apache.catalina.Container interface and there are four types of containers: engine, host, context, and wrapper.
This chapter offers two applications that work with contexts and wrappers.

Tomcat 中有 4 种 container: Wrapper, Context, Engine and Host. container module 的主要作用是用来处理 request 并向 response 中填充处理结果。这节主要介绍前两种,后两种将在 13 节中介绍。

这节的主要目标是理解一下几个概念分别代表了什么

Container: 这个类注释解释的挺好的。container 表示的是可以执行 client 传过来的 request 的类。container 会将他的 invoke 委托给 pipeline 执行

A Container is an object that can execute requests received from a client, and return responses based on those requests.

Pipeline: 表示装载着 container 将会 invoke 的 tasks 的容器

Valve: 表示将会被执行的 task, 有一个特殊的 valve 叫做 basic valve, 要求最后执行。在这章的例子中,basic valve 用于处理 servlet,不知道实际应用中是不是也是这样处理的,第六章应该可以看到结果。

  • ValveContext: 由一个内部类作为实现,由此可以访问外部类的成员变量,遍历所有的 valve。

The Container Interface

container 必须实现 catalina 的 Container 接口。connector 会调用实现类的 invoke 逻辑(怎么调用的我一直没看到,估摸着应该在 Lifecycle 部分才涉及到)。按处理的业务来分,有四种类型的 Container

  • Engine: 代表整个 Catalina servlet engine - 虽然我对这里说的 engine 是个什么东西不怎么清楚
  • Host: 代表了含有 contexts 的虚拟 host
  • Context: 代表了一个 web application, 一个 application 可以包含一个或多个 wrapper
  • Wrapper: 代表一个独立的 servlet

以上四种概念的接口定义放在 org.apache.catalina 包下,对应的实现放在 org.apache.catalina.core 下

一个可用的 container 并不需要具备四种 container 类型,最简单的案例只需要一个 wrapper 即可。wrapper 是最低等级的 container,不能再包含其他 container。container 支持的常规操作

  1. addChild() - wrapper 除外
  2. removeChild()
  3. findChild()
  4. findChildren()

container 还可以包含其他辅助类,比如 Loader, Logger, Manager, Realm 和 Resources. container 还可以通过配置 server.xml 使他在启动服务器时达到动态指定的效果。这种特性是通过 pipeline 和 valves 达到的。

Pipeline Tasks

这节介绍当 container 的 invoke 方法被调用的时候会发生什么。主要涉及到四个接口 Pipeline, Valve, ValveContext 和 Contained. pipeline 包含了 container 将要执行的 tasks, valve 即将要执行的 task. container 默认有一个 valves 但是我们可以自己添加任意多个自定义的 valves. valves 也可以通过配置 server.xml 指定。

这里有个图,但是没显示,我猜是这种责任链的图

pipeline and valves

pipeline 的工作原理和 servlet 的 filter 是一样的, 使用责任链模式。pipeline 相当于链,valves 相当去 filter。当一个 valve 执行完了之后,会调用下一个 valve 继续执行。自带的那个 basic valve 总是在最后才被调用。

按上面的逻辑,你可能会用如下方式实现 pipeline

1
2
3
4
5
6
// invoke each valve added to the pipeline
for(int n=0; n<valves.length; n++) {
valves[n].invoke(...);
}
// then, invoke the basic valve
basicValve.invoke(...);

但是 Tomcat 的设计者通过引入 ValveContext 这个 interface 来解决这个问题,工作原理如下

Container 的 invoke() 方法被调用的时候,并不是 hard code 需要做的事情,而是通过调用 pipeline 的 invoke() 方法。pipeline 和 container 的 invoke() 方法定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface Pipeline {
public void invoke(Request request, Response response)
throws IOException, ServletException;
}

public interface Container {
public void invoke(Request request, Response response)
throws IOException, ServletException;
}

// container 的实现
public abstract class ContainerBase implements Container, Lifecycle, Pipeline {
protected Pipeline pipeline = new StandardPipeline(this);

public void invoke(Request request, Response response)
throws IOException, ServletException {

pipeline.invoke(request, response);

}
}

pipeline 需要保证所有被添加进来的 valves 和 basic valve 只被调用一次。pipeline 是通过 ValveContext 这个接口实现该功能的。ValveContext 是 pipeline 的一个内部类(innerClass),通过这种定义使得 ValveContext 可以访问 pipeline 中的所有对象。ValveContext 中最重要的方法是 invokeNext

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
public interface ValveContext {
public void invokeNext(Request request, Response response)
throws IOException, ServletException;
}

public class SimplePipeline implements Pipeline {
protected Valve valves[] = new Valve[0];

public void addValve(Valve valve) {...}

protected class SimplePipelineValveContext implements ValveContext {
protected int stage = 0;

public String getInfo() {
return null;
}

public void invokeNext(Request request, Response response)
throws IOException, ServletException {
// subscript: 下标 + stage 用来标记被调用的 valve
int subscript = stage;
stage = stage + 1;
// Invoke the requested Valve for the current request thread
if (subscript < valves.length) {
valves[subscript].invoke(request, response, this);
} else if ((subscript == valves.length) && (basic != null)) {
basic.invoke(request, response, this);
} else {
throw new ServletException("No valve");
}
}
}
}

ValveContxt 会调用第一个 valve 的 invoke 方法,第一个 valve 会调用第二个 valve 的 invoke 方法。Valve 的 invoke 方法的参数列表中包含 ValveContext 方便调用 invokeNext 方法。

1
2
3
public interface Valve {
public void invoke(Request request, Response response, ValveContext context) throws IOException, ServletException;
}

The Pipeline Interface

Pipeline 接口定义, container 通过调用它来处理 valves 和 basic valve.

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Pipeline {
// 操作 basic valve
public Valve getBasic();
public void setBasic(Valve valve);

public void addValve(Valve valve);
public Valve[] getValves();

public void invoke(Request request, Response response)
throws IOException, ServletException;

public void removeValve(Valve valve);
}

The Valve Interface

这个 component 用于处理一个 request,只有两个方法 invoke 和 getInfo

1
2
3
4
5
6
public interface Valve {
public String getInfo();

public void invoke(Request request, Response response, ValveContext context)
throws IOException, ServletException;
}

The ValveContext Interface

只有 invokeNext 和 getInfo 两个方法

1
2
3
4
5
6
7
public interface ValveContext {

public String getInfo();

public void invokeNext(Request request, Response response)
throws IOException, ServletException;
}

The Contained Interface

valve 可以选择性的实现 Contained 接口,这个接口表明对应的实现最多只能和一个 container 有关系

1
2
3
4
public interface Contained {
public Container getContainer();
public void setContainer(Container container);
}

The Wrapper Interface

Wrapper 表示一个独立的 servlet 定义。Wrapper 的实现类负责管理 servlet 的生命周期。比如调用 init, service 和 destroy 方法。它是最底层的 container 实现,所以不能添加 child, 添加会抛错

1
2
3
 public void addChild(Container child) {
throw new IllegalStateException(sm.getString("standardWrapper.notChild"));
}

其他一些比较重要的方法比如 allocate 和 load

1
2
3
4
public interface Wrapper extends Container {
public Servlet allocate() throws ServletException;
public void load() throws ServletException;
}

allocate 用于指定 wrapper 指代的 servlet,load 用于加载 servlet 的实例。

The Context Interface

Context 指代一个 web application. 一个 context 可以包含一个或多个 wrapper

The Wrapper Application

下面是本章的第一个例子,一个简单的 container,只由一个 wrapper 来充当 container 主体。包含七个类

  • SimpleWrapper: Wrapper 的实现类,包含一个 Pipeline, 通过一个 Loader 来加载 servlet。
  • SimplePipeline: Pipeline 的实现类,包含一个 basic valve 和两个额外 valve
  • SimpleLoader: 用于加载 servlet
  • SimpleWrapperValve: basic valve 的实现类
  • ClientIPLoggerValve, HeaderLoggerValve: 额外 valve 的实现类
  • Bootstrap1: 启动类

SimpleWrapperValve 和额外的 Valve 最大的区别是,SimpleWrapperValve 没有在调用 invkeNext, 因为规则上来说,它是最后一个需要调用的 valve 了。

Running the Application

启动服务器,访问 http://localhost:8080 终端显内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ModernServlet -- init
Client IP Logger Valve
0:0:0:0:0:0:0:1
------------------------------------
Header Logger Valve
host:localhost:8080
connection:keep-alive
sec-ch-ua:"Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile:?0
upgrade-insecure-requests:1
user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36
accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
sec-fetch-site:none
sec-fetch-mode:navigate
sec-fetch-user:?1
sec-fetch-dest:document
accept-encoding:gzip, deflate, br
accept-language:en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6
cookie:loginMethodCookieKey=PWD; bizxThemeId=2wyfwkupsp; fontstyle=null; _pk_id.1.1fff=9a13b6b396550ca6.1619008137.; Idea-12b942b0=d6d70da6-7368-4631-b3f7-a91eafcb1e9f; Idea-cead54cd=ce25569f-051c-4d11-b8f4-6b99d512503d; JSESSIONID=B3BD850D653D266A9BB2346D7D97B8A2
------------------------------------

The Context Application

当服务器需要处理多个 servlet 时,就需要用到 context 和 mapper 了。mapper 帮助父容器选择子 container 处理 request。

PS: mapper 只在 Tomcat4 中使用,到 Tomcat5 就淘汰了。

一个 container 可以使用多个 mapper 支持多种 protocols. 这个例子中只处理一种。比如一个 container 可以配置一个 mapper 处理 http 请求,配置另一个 mapper 处理 https.

1
2
3
4
5
6
7
8
9
10
11
public Container map(Request request, boolean update) {
//this method is taken from the map method in org.apache.cataline.core.ContainerBase
//the findMapper method always returns the default mapper, if any, regardless the
//request's protocol
Mapper mapper = findMapper(request.getRequest().getProtocol());
if (mapper == null)
return (null);

// Use this Mapper to perform this mapping
return (mapper.map(request, update));
}

这个例子的实现中没有做 protocol 的处理,直接返回默认的 mapper。Mapper 接口的定义如下:

1
2
3
4
5
6
7
public interface Mapper {
public Container getContainer();
public void setContainer(Container container);
public String getProtocol();
public void setProtocol(String protocol);
public Container map(Request request, boolean update);
}

Context 的例子中定义了两个 map, 一个是 url_path - servlet_name 的 map, 另一个是 servlet_name - servlet_class 的 map。就我看来有点啰嗦,这样做可能是为了实现定制 url path 的功能(个人感觉,没有细究)。

处理过程和 SimpleWrapper 一样,通过 pipeline 处理所有的 valves, 最后处理 basic valve. 这里定义的 basic valve 叫做 SimpleContextValve. 他的实现中通过调用 container.map(request, true) 拿到指定的 wrapper,之后调用 wrapper.invoke(req, resp) 完成 servlet 的调用。invoke 的实现中会调用 SimpleWrapperValve 完成 servlet 类加载,并执行 service 方法,完成功能调用。

PS: SimpleWrapper 和 SimpleWrapperValve 是强绑定的,之前找了好久,通过 debug 确定了相互关系。

过程:

  1. SimpleContext 调用 pipeline 的 invoke 方法
  2. pipeline 的 invoke 方先调用额外 valves 再调用 basic valve
  3. basic valve 的 invoke 方法会调用 map 方法找到子 wrapper,如果存在则调用其 invoke 方法

Chapter4 presents Tomcat 4’s default connector.
This connector has been deprecated in favor of a faster connector called Coyote. Nevertheless, the default connector is simpler and easier to understand.

Tomcat 的 connector 是一个独立的模块,现存比较知名的实现有 Coyote, mod_jk, mod_jk2 和 mod_webapp. Tomcat 的 connector 实现需要遵循以下标准

  • 必须实现 org.apache.catalina.Connector 接口
  • 创建的 request 必须实现 org.apache.catalina.Request 接口
  • 创建的 response 必须实现 org.apache.catalina.Response 接口

Tomcat4 默认的 connector 做的事情和第三章的没什么区别,它会一直 stand by 等待 Http request 的到来,然后创建 request 和 response 对象,并调用 org.apache.catalina.Container 实现类的 invoke 方法。

1
2
3
4
public void invoke(
org.apache.catalina.Request request,
org.apache.catalina.Response response
);

invoke 方法中,container 会加载 servlet,调用其 service 方法,同时附带管理 session,log 等资源的功能

默认的 tomcat connector 和 ex03 有点不同,它提供了 pool 机制来减小创建对象的开销,同时更多的使用 char arry 代替 string 提高效率。

PS: 这节里面的多线程操作,值得好好看一看,之前一直都没有机会接触相关的知识点 (●°u°●)​ 」- 复习了小半个月,都快看吐了

默认的 connector 实现了所有 HTTP 1.1 的特性,同时也支持老版本的 HTTP 协议,比如 0.9 和 1.0. 理解 1.1 的协议对后面理解 connector 实现原理很重要。之后我们会介绍 tomcat 自定义的 connector 接口(org.apache.catalina.Connector).

HTTP 1.1 New Features

下面介绍 HTTP 1.1 的新特性

Persistent Connections

HTTP 1.1 之前的协议,在请求完成后会关闭链接。但是现在一个网页请求中可能会包含很多资源,比如 images, applets 等。如果这些资源都通过不同的 connection 下载,那么整个过程会很慢。使用 persistent connection 之后,连接将被复用, 减小资源开销。

persistent connection 是 HTTP 1.1 的默认配置,你也可以通过 connection: keep-alive 属性显示的指定。

Chunked Encoding

persistent connection 导致的一个结果是,发送方必须在发送 request 或 response 时指定自己发送的内容的长度。但是通常情况下服务器端并不能做到这一点, 服务器发送内容的时候,是准备一点,发送一点,所以很可能发送的时候根本不知道将要发送多少内容。比如 servlet 会在一些数据准备好后就发送,并不会等到所有数据都完备再开始。

HTTP 1.0 的时候并不需要指定这个长度属性,连接会一直保持直到接收到 -1 这个结束标志符。

HTTP 1.1 通过 transfer-encoding 这个标志位表示将要发送的流长度。每个 chunk 数据发送前都会先发送一个 长度 + CR/LF 的行表示后面要发送的数据长度。在通讯结束后回发送一个 0 长度的 chunk 表示 transaction 结束。如下所示,我们以发送文字 I'm as helpless as a kitten up a tree. 为例

发送时,这段文字被分成 2 个 chunks,第一个 chunk 长度为 29 第二个 chunk 长度为 9 那么体现在实际的 request 中为如下情况

1
2
3
4
5
1D\r\n
I'm as helpless as a kitten u
9\r\n
p a tree.
0\r\n

1D 是 16 进制的 29, 表示第一个 chunk 包含 29 个 bytes. 0\r\n 表示通信结束。

Use of the 100(Continue) Status

当客户端发送的 request body 很大时,他会在 header 中包含 100-continue 属性来和服务器端确认是否接收来提高效率,避免资源浪费(传到一半被拒绝被拒绝的情况)。服务器如果接收这种 request, 则返回 HTTP/1.1 100 Continue

The Connector interface

Tomcat connector 必须实现 org.apache.catalina.Connector 接口,这个接口有很多方法,但是最主要方法有四个

  • getContainer
  • setContainer
  • createReqeust
  • createResponse

重点:Connector 和 Container 是 1 对 1 的关系,Connector 和 Processor 是 1 对多的关系

The HttpConnector Class

实现 org.apache.catalina.Connector 接口使它能和 Catalina 整合

实现 java.lang.Runnable 使他能多线程运行

Lifecycle 接口用于管理每一个 catalina component 的生命周期,具体内容第六章介绍

Creating a Server Socket

HttpConnector 的 initialize() 方法会调用 open 方法生成 serverSocket 对象。open 中通过工厂方法拿到 ServerSocket, 参见 ServerSocketFactory 和对应的实现 DefaultServerSocketFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Return the server socket factory used by this Container.
*/
public ServerSocketFactory getFactory() {

if (this.factory == null) {
synchronized (this) {
this.factory = new DefaultServerSocketFactory();
}
}
return (this.factory);

}

lazy model 的方式拿到工厂实例。然后调用 factory.createSocket(port, acceptCount) 创建 socket.

Maintaining HttpProcess Instances

HttpContainer 中声明了一个 java.io.Stack 类型的变量存储 processor 的实例,实现类似 pool 的效果。

1
private Stack processor = new Stack();

HttpConnector 中定义了两个变量(minProcessors/maxProcessors)来控制这个 stack 的大小, 在启动的时候,服务器默认创建 minProcessors 数量的 processor 备用。随着 request 的增加,这个数量也会增加知道等于 maxProcessors。如果 request 再增加,之后的 request 都会被忽略。如果你想要访问数量没有限制,可以设置 maxProcessor 为负数。

PS: HttpConnector 中通过 curProcessor 这个变量表示当前可用的 processor 数量

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 void start() throws LifecycleException {
// Validate and update our current state
if (started)
throw new LifecycleException
(sm.getString("httpConnector.alreadyStarted"));
threadName = "HttpConnector[" + port + "]";
lifecycle.fireLifecycleEvent(START_EVENT, null);
started = true;

// Start our background thread - 启动 Connector 线程,设置为守护线程
threadStart();

// Create the specified minimum number of processors
while (curProcessors < minProcessors) {
if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
break;
HttpProcessor processor = newProcessor();
recycle(processor);
}
}

void recycle(HttpProcessor processor) {
processors.push(processor);
}

processor 创建完后,通过调用 recycle 方法将 processor 回收到栈中。processor 负责解析 request 内容,他的构造函数的参数中包含 HttpConnector, 在构造的过程中,会调用 connector 中创建 request 和 response 的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public HttpProcessor(HttpConnector connector, int id) {

super();
this.connector = connector;
this.debug = connector.getDebug();
this.id = id;
this.proxyName = connector.getProxyName();
this.proxyPort = connector.getProxyPort();
this.request = (HttpRequestImpl) connector.createRequest();
this.response = (HttpResponseImpl) connector.createResponse();
this.serverPort = connector.getPort();
this.threadName = "HttpProcessor[" + connector.getPort() + "][" + id + "]";

}

Serving HTTP Requests

HttpConnector 的主要逻辑都在 run 方法中,该方法通过 while 循环等待发送过来的响应,直到服务器停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void run() {
// Loop until we receive a shutdown command
while (!stopped) {
// Accept the next incoming connection from the server socket
Socket socket = null;
try {
socket = serverSocket.accept();
// ...
}
// ...
}
// ...
}

createProcessor 工作流程

  1. 如果 stack 中有,则返回
  2. 如果没有,判断是否达到上限,没有就创建
  3. 达到上限,返回并关闭 socket
  4. 上限为 -1,创建 processor

processor 执行 assign() 方法后立即返回,后续工作由 processor 在单独的线程中完成

The HttpProcessor Class

HttpProcessor 的功能和前一章中的 processor 是一样的,本章中的实现多了 assign 之后的多线程功能。下面将具体介绍他的实现原理。

和 HttpConnector 类似 HttpProcessor 也实现了 Runnable 和 Lifecycle 接口

这里主要探究 processor 的 assign 方法是如何使用多线程来支持 tomcat 同时处理多个 request 的功能的

For each HttpProcessor instance the HttpConnector creates, its start method is called, effectively starting the “processor thread” of the HttpProcessor instance.

HttpConnector 的 start() 方法被调用时,这个方法中有一个名为 newProcessor() 的方法,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Create and return a new processor suitable for processing HTTP
* requests and returning the corresponding responses.
*/
private HttpProcessor newProcessor() {

// if (debug >= 2)
// log("newProcessor: Creating new processor");
HttpProcessor processor = new HttpProcessor(this, curProcessors++);
if (processor instanceof Lifecycle) {
try {
((Lifecycle) processor).start();
} catch (LifecycleException e) {
log("newProcessor", e);
return (null);
}
}
created.addElement(processor);
return (processor);

}

可以看到,在创建 HttpProcessor 对象之后,processor thread 立马就被启动了

processor 的 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
25
26
/**
* The background thread that listens for incoming TCP/IP connections and
* hands them off to an appropriate processor.
*/
public void run() {

// Process requests until we receive a shutdown signal
while (!stopped) {

// Wait for the next socket to be assigned
Socket socket = await();
if (socket == null)
continue;

// Process the request from this socket, omit the try-catch
process(socket);

// Finish up this request
connector.recycle(this);
}

// Tell threadStop() we have shut ourselves down successfully
synchronized (threadSync) {
threadSync.notifyAll();
}
}

当 connector 启动时 processor thread 也会一起启动,然后卡在 await 这里一直等待。当 HttpConnector 接受到 request 之后会调用 processor.assign(socket) 方法。

这里需要注意的是 assign() 方法是在 connector thread 中调用的,而 await() 方法是在 processor thread 中被调用的。这两者是怎么通信的呢?他们是通过 available flag 和 Object 自带的 wait(), notifyAll() 方法控制调度的。

PS: wait() 方法使得当前线程保持等待一直到另一个线程调用 notify() 或者 notifyAll() 方法

HttpConnector 中 会调用 processor 的 assign 方法

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
/**
* Process an incoming TCP/IP connection on the specified socket. Any
* exception that occurs during processing must be logged and swallowed.
* <b>NOTE</b>: This method is called from our Connector's thread. We
* must assign it to our own thread so that multiple simultaneous
* requests can be handled.
*
* @param socket TCP socket to process
*/
synchronized void assign(Socket socket) {

// Wait for the Processor to get the previous Socket
while (available) {
try {
wait();
} catch (InterruptedException e) {
}
}

// Store the newly available Socket and notify our thread
this.socket = socket;
available = true;
notifyAll();

if ((debug >= 1) && (socket != null))
log(" An incoming request is being assigned");

}

HttpProcessor 中 await 的实现

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
/**
* Await a newly assigned Socket from our Connector, or <code>null</code>
* if we are supposed to shut down.
*/
private synchronized Socket await() {

// Wait for the Connector to provide a new Socket
while (!available) {
try {
wait();
} catch (InterruptedException e) {
}
}

// Notify the Connector that we have received this Socket
Socket socket = this.socket;
available = false;
notifyAll();

if ((debug >= 1) && (socket != null))
log(" The incoming request has been awaited");

return (socket);

}

简单图示一下交互过程

connector processor communication

两个 thread 交互描述:

服务器启动时回执行 connector 的 start 方法,这个方法回启动 connector 线程和 processor 线程。connector 线程启动后 block 在等待 request 的地方,而 processor 线程启动后 block 在 wait()。

这时如果 connector thread 中接收到一个 request, connector 会从 stack 中取出一个可用的 processor 并调用 assign(socket) 方法。assign 方法会判断 available flag, 初始值为 false, 跳过 while, 将 socket 复刻到成员变量,设置 available 为 true, 唤醒所有等待的线程。

这时 process thread 的 await() 方法从 wait() 中被唤醒过来,跳出 while 循环将 socket 复刻到 local 变量中,并将 available 设置成 false,调用 notifyAll() 唤醒其他线程。接着跳出 await() 方法,执行其余方法,包括解析 socket,并回收重用 processor。然后继续执行 while block 在 await 中,如此循环。

问题:
Q:为什么 await 要使用 local variable 类型的 socket 而不是直接返回传入的 socket
A: 如果没有用 local 的 socket, 那么 socket 还是 connector 中的那个 socket,我们用 local 的复刻之后返回,这个 socket 就可以用来处理下一个 request 了。
Q: 为什么 await 需要调用 notifyAll() 方法
A: 书上给的答案是防止这个时候 processor 再次收到一个 socket,此时 assign() 中 available 为 true,会进入到 wait 方法,需要主动唤醒。但是从我的理解来看,这种情况压根不会发生才对啊,同一个 processor 此时应该接受不到其他 socket 了才对。不知道是不是我理解有问题。

Request Objects / Response Objects

default connector 的 request 实现采用 org.apache.catalina.Request 接口. 对应的实现基础类是 RequestBase,他的子类是 HttpRequest. 最终实现类是 HttpRequestImpl. 这些类也有各自的 Facade 类。 UML 示例如下

response 的关系图和 request 基本一致

Process Requests

这节主要介绍 HttpProcessor 的 process 方法,它主要做了下面几件事

  • parseConnection - 获取地址并塞到 request 中
  • parseRequest - 同上节
  • parseHeader - 解析 header 并塞到 request 中
  • recycle response + request - 复用对象,相比于上一节完善了很多

process 定义了一些 flag,比如 ok 表示处理过程中没有出现异常,finishResponse 表示 finishResponse 方法要被调用。

1
2
boolean ok = true;
boolean finishResponse = true;
  • keepAlive - 持久链接
  • stopped - HttpProcess instance has been stopped by connector
  • http11 - request 是从支持 http11 的 client 发出来的

在 Tomcat 的 default connector 实现中,用户和 HttpProcessor 是隔离的,但是用户可以通过设置 connector 的 buffer size 间接设置 processor 的 buffer size.

1
2
3
4
5
6
7
8
9
10
SocketInputStream input = null;
OutputStream output = null;

// Construct and initialize the objects we will need
try {
input = new SocketInputStream(socket.getInputStream(), connector.getBufferSize());
} catch (Exception e) {
log("process.create", e);
ok = false;
}

接下来是一个 while 循环读取 inputStream 中的数据

1
2
keepAlive = true;
while (!stopped && ok && keepAlive) {...}

解析过程中,一开始设置 finishResponse 的值,并做一些 request 和 response 的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
finishResponse = true;

try {
request.setStream(input);
request.setResponse(response);
output = socket.getOutputStream();
response.setStream(output);
response.setRequest(request);
((HttpServletResponse) response.getResponse()).setHeader
("Server", SERVER_INFO);
} catch (Exception e) {
log("process.create", e);
ok = false;
}

之后开始 parse connection,request 和 headers

1
2
3
4
5
6
7
8
// Parse the incoming request
try {
if (ok) {
parseConnection(socket);
parseRequest(input, output);
if (!request.getRequest().getProtocol()
.startsWith("HTTP/0"))
parseHeaders(input);

parseConnection 可以获取 protocol 信息,这个值可能是 0.9, 1.0 或者 1.1. 如果是 1.0 则 keepAlive 会设置成 false。如果 request 头中包含 100-contiue 则会在 parseHeaders 中将 sendAck 设置成 true。

如果是 1.1 的协议,它也会相应 100-continue 并且会检查是否允许 chunking

1
2
3
4
5
6
7
8
if (http11) {
// Sending a request acknowledge back to the client if
// requested.
ackRequest(output);
// If the protocol is HTTP/1.1, chunking is allowed.
if (connector.isChunkingAllowed())
response.setAllowChunking(true);
}

ackRequest 会检测 sendAck 的值,如果为 true 则返回 HTTP/1.1 100 Continue /r/n/r/n. parse 过程中如果有异常,则 ok 和 finishResponse 会被置位。parse 结束后 request 和 response 会传给 container 调用

1
2
3
4
5
6
7
// Ask our Container to process this request
try {
((HttpServletResponse) response).setHeader("Date", FastHttpDateFormat.getCurrentDate());
if (ok) {
connector.getContainer().invoke(request, response);
}
}

如果此时 finishResponse 还是 true 则调用 requeset/response 的 finishResponse 方法, flush 流

1
2
3
4
5
6
7
// Finish up the handling of the request
if (finishResponse) {
response.finishResponse();
request.finishRequest();
output.flush();
ok = false;
}

最后检查 Connection 的值并置位,回收 request 和 response

1
2
3
4
5
6
7
8
9
10
11
12
13
// We have to check if the connection closure has been requested
// by the application or the response stream (in case of HTTP/1.0
// and keep-alive).
if ( "close".equals(response.getHeader("Connection")) ) {
keepAlive = false;
}

// End of request processing
status = Constants.PROCESSOR_IDLE;

// Recycling the request and the response objects
request.recycle();
response.recycle();

然后重复 while 或者结束 socket 通信

1
2
3
4
try {
shutdownInput(input);
socket.close();
}

Parsing the Connection

parseConnection 会获取 address 和 port 在 request 中赋值

1
2
3
4
5
6
7
8
private void parseConnection(Socket socket) throws IOException, ServletException {
((HttpRequestImpl) request).setInet(socket.getInetAddress());
if (proxyPort != 0)
request.setServerPort(proxyPort);
else
request.setServerPort(serverPort);
request.setSocket(socket);
}

Parsing the Request

和前一章一样的实现

Parsing Headers

通过 character arrays 操作而非 String 来提高效率

The Simple Container Application

这里的 container 是一个简易版本,实现了 catalina 中的 Container 接口以配合 connector 使用。只实现了 invoke 接口,里面的功能是加载 servlet class 并执行

Chapter3 presents a simplified version of Tomcat 4’s default connector.
The application built in this chapter serves as a learning tool to understand the connector discussed in Chapter4

PS: 这个 project 有点老了,其中用到的 Catalina 包比较老, 找了半天

1
2
3
4
5
<dependency>
<groupId>tomcat</groupId>
<artifactId>catalina</artifactId>
<version>4.0.4</version>
</dependency>

相比于 ex02 这章节实现的服务器多了如下功能

  • connector parse request headers
  • servlet can obtain headers, cookies parameter name/values, etc
  • enhance response’s getWriter

完成上述功能后,这个就是简化版的 Tomcat4 的 connector 了。Tomcat 的默认 connector 在 Tomcat4 时被 deprecated 了,不过还是有参考价值的。

StringManager

开篇先介绍了一个用于做类似国际化的类 org.apache.catalina.util.StringManager. 原理很简单,就是这个类通过单例模式生成唯一对象,加载预先定义好的 properties,通过 getString 方法拿到对应语言的翻译。

StringManager 底层使用两个 Java 基础类做实现,一个是 ResourceBundle 另一个是 MessageFormat. ResourceBundle 可以通过 properties 加载多语言支持,MessageFormat 则用于格式化打印信息。

为了节省资源,StringManager 内部通过 Hashtable 存储多语言,并通过单例模式创建这个 field

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static Hashtable managers = new Hashtable();

/**
* Get the StringManager for a particular package. If a manager for
* a package already exists, it will be reused, else a new
* StringManager will be created and returned.
*
* @param packageName
*/

public synchronized static StringManager getManager(String packageName) {
StringManager mgr = (StringManager)managers.get(packageName);
if (mgr == null) {
mgr = new StringManager(packageName);
managers.put(packageName, mgr);
}
return mgr;
}

PS: 它这里用的是饿汉式的声明,类加载的时候就创建了对象,调用 getManager() 的时候通过 synchronized 加锁保证线程安全。每一个 package 下的 LocalStrings 都会创建一个对象存储多语言信息。

The Application

相比之前的 project,这章开始,代码开始分包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.
├── ServletProcessor.java
├── StaticResourceProcessor.java
├── connector
│ ├── RequestStream.java
│ ├── ResponseStream.java
│ ├── ResponseWriter.java
│ └── http
│ ├── Constants.java
│ ├── HttpConnector.java
│ ├── HttpHeader.java
│ ├── HttpProcessor.java
│ ├── HttpRequest.java
│ ├── HttpRequestFacade.java
│ ├── HttpRequestLine.java 拆出来一个单独的类代表 request 的第一行,包括请求类型,URI,协议等信息
│ ├── HttpResponse.java
│ ├── HttpResponseFacade.java
│ ├── LocalStrings.properties
│ ├── LocalStrings_es.properties
│ ├── LocalStrings_ja.properties
│ └── SocketInputStream.java
└── startup
└── Bootstrap.java 启动类,实例化 HttpConnector 并调用 start() 方法

Bootstrap.java 为启动类,内容很简单,就是 new 一个 connector, 然后执行 start 方法,让 connector 常驻。

connector 下的类可以分为五类

  • connect 及该类的辅助类(HttpConnector + HttpProcessor)
  • 代表 Http Request 的类(HttpRequest)及其辅助类
  • 代表 Http Response 的类(HttpResponse)及其辅助类
  • Facade 类(HttpRequestFacade + HttpResponseFacade)
  • Constant 常量类

类关系图

和 ex02 比,这里将 HttpServer 拆成了 HttpConnector 和 HttpProcessor 两个类。HttpConnector 等待 request, HttpProcessor 负责 request/response 的生成和处理。

为了提高 connector 的效率,设计的时候将 request 中的 parse 的行为尽可能的延后了(比如有些 servlet 根本不需要 request 中的参数,这样 parse 就显得很多余,白白浪费了时间)。

The Connector

HttpConnector 表示 connector 的实体类,他负责创建 server socket 并等待 Http request 的到来。HttpConnector 实现 runnable 接口,当 start() 被调用时,HttpConnector 被创建并运行。

connector 运行时会做如下几件事情

  • 等待 HTTP requests
  • 为每个 request 创建 HttpProcessor
  • 调用 processor 的 process 方法

HttpProcessor 的 process 方法在拿到 socket 后,会做如下事情

  • Create an HttpRequest/HttpResponse object
  • Parse request first line and headers, populate to HttpRequest object
  • Pass HttpRequest, HttpResponse to Processor(servlet process/static processor)

Create an HttpRequest Object

HttpRequest 的继承关系图如下

本章的 HttpRequest 实现的很多方法都留空了,下一章会有具体实现。但是 header,cookies 等主要属性的提取已经实现了。由于 HttpRequst 的解析比较复杂,下面会分几个小节介绍

Reading the Socket’s input Stream

SocketInputStream 的实现是直接从 Tomcat 4 的实现中 copy 过来的,他负责解析从 socket 中获取的 inputStream。

Parsing the Request Line

processor 中处理 socket 的过程如下

request line 就是 inputStream 中的第一行内容,下面是示例

GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1

各部分称谓如下

  • GET - method
  • /myApp/ModernServlet - URI
  • userName=tarzan&password=pwd - query string
  • parameters - userName/tarzan;password/pwd 成对出现

servlet/JSP 程序中通过 JsessionId 指代 session。 session 标识符通常通过 cookies 存储,如果客户端没有 enable cookie 还需要将它 append 到 URL 中

HttpProcessor 的 process 方法会将上面提到的对象从 inputStream 中提取出来并塞到对应的对象中

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
69
70
71
72
73
74
75
private void parseRequest(SocketInputStream input, OutputStream output) throws IOException, ServletException {
// Parse the incoming request line
input.readRequestLine(requestLine);
String method = new String(requestLine.method, 0, requestLine.methodEnd);
String uri = null;
String protocol = new String(requestLine.protocol, 0,
requestLine.protocolEnd);

// Validate the incoming request line
if (method.length() < 1) {
throw new ServletException("Missing HTTP request method");
} else if (requestLine.uriEnd < 1) {
throw new ServletException("Missing HTTP request URI");
}
// Parse any query parameters out of the request URI
int question = requestLine.indexOf("?");
if (question >= 0) {
request.setQueryString(new String(requestLine.uri, question + 1,
requestLine.uriEnd - question - 1));
uri = new String(requestLine.uri, 0, question);
} else {
request.setQueryString(null);
uri = new String(requestLine.uri, 0, requestLine.uriEnd);
}

// Checking for an absolute URI (with the HTTP protocol)
if (!uri.startsWith("/")) {
int pos = uri.indexOf("://");
// Parsing out protocol and host name
if (pos != -1) {
pos = uri.indexOf('/', pos + 3);
if (pos == -1) {
uri = "";
} else {
uri = uri.substring(pos);
}
}
}

// Parse any requested session ID out of the request URI
String match = ";jsessionid=";
int semicolon = uri.indexOf(match);
if (semicolon >= 0) {
String rest = uri.substring(semicolon + match.length());
int semicolon2 = rest.indexOf(';');
if (semicolon2 >= 0) {
request.setRequestedSessionId(rest.substring(0, semicolon2));
rest = rest.substring(semicolon2);
} else {
request.setRequestedSessionId(rest);
rest = "";
}
request.setRequestedSessionURL(true);
uri = uri.substring(0, semicolon) + rest;
} else {
request.setRequestedSessionId(null);
request.setRequestedSessionURL(false);
}

// Normalize URI (using String operations at the moment)
String normalizedUri = normalize(uri);

// Set the corresponding request properties
((HttpRequest) request).setMethod(method);
request.setProtocol(protocol);
if (normalizedUri != null) {
((HttpRequest) request).setRequestURI(normalizedUri);
} else {
((HttpRequest) request).setRequestURI(uri);
}

if (normalizedUri == null) {
throw new ServletException("Invalid URI: " + uri + "'");
}
}

UML 图示如下

Request line 的类实现为 HttpRequestLine, 它的实现比较有意思,它为这个类中的各个部分声明了一个存储的 char 数组,并标识了结束地址 char[] method, int methodEnd

我们通过处理 SocketInputStream 可以得到 request line 的信息用以填充 HttpRequestLine,主要涉及的方法

  • readRequestLine(HttpRequestLine) 填充 line 对象的方法入口
  • fill() 使用 buffer 的方式读取输入流中的内容,这个过程中会初始化 pos 和 count 的值。pos 表示当前位置,count 表示流中内容长度
  • read() 放回 pos 位置上的内容

SocketInputStream 的 read() 方法有一个很有意思的处理方式

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Read byte.
*/
public int read()
throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return buf[pos++] & 0xff;
}

可以看到最后的处理方式是返回 buf[n] & 0xff 0xff 即 0000 0000 0000 1111 做与操作可以将前面的值置零

readRequestLine 中用了三个 while 循环通过判断空格和行结束符将首行的信息提取出来。很雷同的还有一个叫 readHeader() 的方法处理解析 request 中的 headers.

Parsing Headers

request 的 header 部分由 HttpHeader 这个类表示。将在第四章介绍具体实现,目前只需要了解一下几点

  • 可以使用无参数构造器创建实例
  • 通过调用 readHeader 方法 SocketInputStream 中的 header 部分解析并填充进指定的 HttpHeader 对象
  • 通过 String name = new String(header.name, 0, header.nameEnd) 拿到 header 的 name, 同理获取 value

由于一个 request 中可能包含多个 header,所以通过 while 循环解析,解析完后通过 addHeader 塞入 request 中

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
private void parseHeaders(SocketInputStream input) throws IOException, ServletException {
while (true) {
HttpHeader header = new HttpHeader();
;

// Read the next header
input.readHeader(header);
if (header.nameEnd == 0) {
if (header.valueEnd == 0) {
return;
} else {
throw new ServletException(
sm.getString("httpProcessor.parseHeaders.colon"));
}
}

String name = new String(header.name, 0, header.nameEnd);
String value = new String(header.value, 0, header.valueEnd);
request.addHeader(name, value);
// do something for some headers, ignore others.
if (name.equals("cookie")) {
Cookie cookies[] = RequestUtil.parseCookieHeader(value);
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals("jsessionid")) {
// Override anything requested in the URL
if (!request.isRequestedSessionIdFromCookie()) {
// Accept only the first session id cookie
request.setRequestedSessionId(cookies[i].getValue());
request.setRequestedSessionCookie(true);
request.setRequestedSessionURL(false);
}
}
request.addCookie(cookies[i]);
}
} else if (name.equals("content-length")) {
int n = -1;
try {
n = Integer.parseInt(value);
} catch (Exception e) {
throw new ServletException(
sm.getString("httpProcessor.parseHeaders.contentLength"));
}
request.setContentLength(n);
} else if (name.equals("content-type")) {
request.setContentType(value);
}
} // end while
}

Parsing Cookies

随便访问了一下网页,下面是一个 cookie 的例子

1
txtcookie: fontstyle=null; loginMethodCookieKey=PWD; bizxThemeId=lightGrayPlacematBlueAccentNoTexture; route=133abdfd8b5240fdc3330810e535ae4c79433a08; zsessionid=45641c6c-9dff-4d67-8893-b0764636ee1f; JSESSIONID=D8477F13FD4A9257B98731F666694D91.mo-bce0c171c

在前面的 parseHeaders 方法中,处理 cookie 的部分,通过 RequestUtil.parseCookieHeader(value) 解析 cookie

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
/**
* Parse a cookie header into an array of cookies according to RFC 2109.
*
* @param header Value of an HTTP "Cookie" header
*/
public static Cookie[] parseCookieHeader(String header) {

if ((header == null) || (header.length() < 1))
return (new Cookie[0]);

ArrayList cookies = new ArrayList();
while (header.length() > 0) {
int semicolon = header.indexOf(';');
if (semicolon < 0)
semicolon = header.length();
if (semicolon == 0)
break;
String token = header.substring(0, semicolon);
if (semicolon < header.length())
header = header.substring(semicolon + 1);
else
header = "";
try {
int equals = token.indexOf('=');
if (equals > 0) {
String name = token.substring(0, equals).trim();
String value = token.substring(equals+1).trim();
cookies.add(new Cookie(name, value));
}
} catch (Throwable e) {
;
}
}

return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()]));

}

Obtaining Parameters

解析 parameter 的动作放在 HttpRequest 的 parseParameters 方法中。在调用 parameter 相关的方法,比如 getParameterMap, getParameterNames 等时,会先调用 parseParameters 方法解析他,而且只需要解析一次即可,再次调用时,使用之前解析的结果。

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
69
/**
* Parse the parameters of this request, if it has not already occurred.
* If parameters are present in both the query string and the request
* content, they are merged.
*/
protected void parseParameters() {
if (parsed)
return;
ParameterMap results = parameters;
if (results == null)
results = new ParameterMap();
results.setLocked(false);
String encoding = getCharacterEncoding();
if (encoding == null)
encoding = "ISO-8859-1";

// Parse any parameters specified in the query string
String queryString = getQueryString();
try {
RequestUtil.parseParameters(results, queryString, encoding);
}
catch (UnsupportedEncodingException e) {
;
}

// Parse any parameters specified in the input stream
String contentType = getContentType();
if (contentType == null)
contentType = "";
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
}
else {
contentType = contentType.trim();
}
if ("POST".equals(getMethod()) && (getContentLength() > 0)
&& "application/x-www-form-urlencoded".equals(contentType)) {
try {
int max = getContentLength();
int len = 0;
byte buf[] = new byte[getContentLength()];
ServletInputStream is = getInputStream();
while (len < max) {
int next = is.read(buf, len, max - len);
if (next < 0 ) {
break;
}
len += next;
}
is.close();
if (len < max) {
throw new RuntimeException("Content length mismatch");
}
RequestUtil.parseParameters(results, buf, encoding);
}
catch (UnsupportedEncodingException ue) {
;
}
catch (IOException e) {
throw new RuntimeException("Content read fail");
}
}

// Store the final results
results.setLocked(true);
parsed = true;
parameters = results;
}

在 GET 类型的 request 中,所有的 parameter 都是存在 URL 中的,在POST 类型的 request,parameter 是存在 body 中的。解析的 parameter 会存在特殊的 Map 中,这个 map 不允许改变存放的 parameter 的值。对应的实现是 org.apache.catalina.util.ParameterMap. 看了一下具体的实现类代码,其实就是一个 HashMap, 最大的特点是他新加了一个 locked 的 boolean 属性,在增删改的时候都会先检查一下这个 flag 如果此时 flag 为 false 则抛异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class ParameterMap extends HashMap {
// ...
private boolean locked = false;

// ...

public void clear() {

if (locked)
throw new IllegalStateException(sm.getString("parameterMap.locked"));
super.clear();

}

// ...
}

Creating a HttpResponse Object

HttpReponse 类图

通过设置 PrintWriter 的 auto flush 功能,之前打印的 behavior 才修复了,不然只会打印第一句话。为了了解这里说的东西,你需要查一下 Writer 相关的知识点。

问题

server 启动后访问 URL 抛异常 Exception in thread "Thread-0" java.util.MissingResourceException: Can't find bundle for base name com.jzheng.connector.http.LocalStrings, locale en_US

查看了一下编译后的 target 文件加,其中咩有 properties 文件,怀疑是一些类型的文件编译时没有同步过去,试着在 pom 中添加以前项目中用过的 build 代码,问题解决

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
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
<!-- 在 build 的时候将工程中的配置文件也一并 copy 到编译文件中,即 target 文件夹下 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>

Chapter2 explains how simple servlet containers work.
This chapter comes with two servlet container applications that can service requests for static resources as well as very simple servlets.
In particular, you will learn how you can create request and response objects and pass them to the requested servlet’s service method.
There is also a servlet that can be run inside the servlet containers and that you can invoke from a web browser.

servlet container 会为 servlet 做如下事情

  • 第一次调用 servlet 时,加载 servlet 并执行 init 方法
  • 接收 server 创建的 ServletRequest 和 ServletResponse 对象
  • 调用 service 方法,传入前面声明的两个对象
  • servlet 生命周期结束时,调用 destroy 方法

练习01

ex02 的第一个 demo 并不是完整功能的 server,没有实现 init 和 destroy 的功能,它主要关注以下几个方面

  • 等待请求
  • 构建 ServletRequest 和 ServletResponse 对象
  • 如果请求静态资源,调用 StaticResourceProcessor 相关方法
  • 如果请求 servlet, 加载 servlet 并调用 service 方法

Figure 2.1

主要类介绍

HttpServer1 即服务器主体类,通过 while 死循环等待连接

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 HttpServer1 {
public static void main(String[] args) {
HttpServer1 server = new HttpServer1();
server.await();
}

public void await() {
ServerSocket serverSocket = null;
int port = 8080;
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));

// Loop waiting for a request
while (!shutdown) {
Socket socket = serverSocket.accept();
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();

// create Request object and parse
Request request = new Request(input);
request.parse();

// create Response object
Response response = new Response(output);
response.setRequest(request);

// check if this is a request for a servlet or a static resource
// a request for a servlet begins with "/servlet/"
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor1 processor = new ServletProcessor1();
processor.process(request, response);
} else {
StaticResourceProcessor processor = new StaticResourceProcessor();
processor.process(request, response);
}

// Close the socket
socket.close();
//check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}
}
}

Request 和 Response 与 ex01 唯一的不同是,它们分别实现了 servlet 包中定义的 ServletReqeust 和 ServletResponse 的接口,需要实现很多额外的方法,但是本次练习中基本都是使用默认实现,不做介绍

Response 稍微多了一点逻辑,作者将处理静态页面的逻辑放到了 response 中,我觉得这个不好,应该写到 StaticResourceProcessor 中去的,和 ServletProcessor1 相对应,职责更分明才好

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
// Response
public class Response implements ServletResponse {
// constructor ...

/* This method is used to serve a static page */
public void sendStaticResource() throws IOException {
// write response header
String header = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html\r\n" +
"\r\n";
output.write(header.getBytes());

byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try {
/* request.getUri has been replaced by request.getRequestURI */
File file = new File(Constants.WEB_ROOT, request.getUri());
fis = new FileInputStream(file);
/*
HTTP Response = Status-Line
*(( general-header | response-header | entity-header ) CRLF)
CRLF
[ message-body ]
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
*/
int ch = fis.read(bytes, 0, BUFFER_SIZE);
while (ch != -1) {
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
} catch (FileNotFoundException e) {
String errorMessage = "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>";
output.write(errorMessage.getBytes());
} finally {
if (fis != null)
fis.close();
}
}
// other required methods defined in ServletResponse
}

StaticResourceProcessor 很简单的一个类,其实就是提供了一个 process 方法,但是实现只是调用了 response 中定义的方法

1
2
3
4
5
public class StaticResourceProcessor {
public void process(Request request, Response response) {
response.sendStaticResource();
}
}

ServletProcessor1 即处理 servlet 的类,作者参考了 tomcat 中的处理方式,通过 url 解析出来请求的 servlet 名字,再通过 URLClassLoader 做类加载,最后生成实体类并调用 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
public class ServletProcessor1 {
public void process(Request request, Response response) {
// 解析 servlet 名字
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);

// create a URLClassLoader
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(Constants.WEB_ROOT);

// the forming of repository is taken from the createClassLoader
// method in org.apache.catalina.startup.ClassLoaderFactory
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();

// the code for forming the URL is taken from the addRepository
// method in org.apache.catalina.loader.StandardClassLoader class.
urls[0] = new URL(null, repository, streamHandler);
URLClassLoader loader = new URLClassLoader(urls);


Class<?> myClass = myClass = loader.loadClass(servletName);
Servlet servlet = (Servlet) myClass.newInstance();
servlet.service(request, response);
}
}

启动 server 通过终端访问测试地址. 现在的浏览器对 response 格式要求都挺严格的,默认的代码访问会因为 header 不全被 block,通过终端访问则没这么多问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
curl 'http://localhost:8080/servlet/PrimitiveServlet'
Hello. Roses are red.

curl 'http://localhost:8080/index.html'
<html>
<head>
<title>Welcome to BrainySoftware</title>
</head>
<body>
<img src="images/logo.gif">
<br>
Welcome to BrainySoftware.
</body>
</html>

练习 02

这一节练习主要解决一个安全问题-恶意强转。上面的例子中,在 ServletProcessor1 中,我们直接将 request/response 对象作为参数传入 service 方法中,如果使用方知道我们对应的实现,就可以做对象强转并调用 public 方法,比如调用解析静态资源的 sendStaticResource() 方法,这是不安全的,所以我们通过 Facade 的模式,将 request/response 作为内部私有变量持有并调用,以达到隐藏实现的目的。

Facade 和 request/response 的关系

ServletProcessor1 和 ServletProcessor2 最大的区别只有下面这些

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
} catch (Exception e) {
System.out.println(e.toString());
} catch (Throwable e) {
System.out.println(e.toString());
}
// ...

思考题

Servlet 到底是什么?

先看这个英文单词的意思:小服务程序。Javaweb 三个组建之一(其二为 Listener 和 Filter),定义了一类特殊的 Java class 可以在访问特定的地址时执行特定的业务逻辑。我觉得理解到这里就足够了。

遇到的问题

启动 server 之后访问 URL 报错 Exception in thread "main" java.lang.NoClassDefFoundError: javax/servlet/ServletRequest。检查了 pom.xml 什么都正常,Google 了一下,发现是 pom 中包的 scope 有问题.

maven 网站上 copy 时,scope 是 provided 需要改成 compile

  • compile,缺省值,适用于所有阶段,会随着项目一起发布。
  • provided,类似compile,期望JDK、容器或使用者会提供这个依赖。
  • runtime,只在运行时使用,如JDBC驱动,适用运行和测试阶段。
  • test,只在测试时使用,用于编译和运行测试代码。不会随项目发布。
  • system,类似provided,需要显式提供包含依赖的jar,Maven不会在Repository中查找它。

PrimitiveServlet 访问异常

idea 抄过来的会带包名的,所以会出问题。从成功项目 copy 过来的不带,所以能 work

Servlet 中的内容 browser 访问出问题

和之前静态页面的问题一样,不过 servlet 文件我拿到的源码就是 class 的不好改

问题:

  • 父 JSP 页面中声明了一个变量,子 JSPF 文件中不显示的声明能直接使用这个变量吗
  • 上面的情况如果是子 JSP 又如何

实验

新建一个 servlet 在 request 中传入 name 属性,然后 forward 到 parent.jsp 页面中。parent 页面包含三个子页面,分别是 sub.jspf, sub2.jsp 和 sub3.jsp. 前两个通过 <%@ include file="xxx" %> 引入,sub3.jsp 通过 <jsp:include page="xxx"/> 引入。目录结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── java
│ └── com
│ └── jzheng
│ └── servlet
│ └── ParentServlet.java
└── webapp
├── WEB-INF
│ └── web.xml
├── parent.jsp
├── sub.jspf
├── sub2.jsp
└── sub3.jsp

代码实现如下

1
2
3
4
5
6
7
public class ParentServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("name", "jack");
getServletContext().getRequestDispatcher("/parent.jsp").forward(req, resp);
}
}

parent.jsp 页面取得 request 中的属性,并重命名为 myname

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>

<%
String myname = (String)request.getAttribute("name");
%>

<hr>
<h1> include jspf file </h1>
<%@ include file="sub.jspf" %>

<hr>
<h1> include jsp file </h1>
<%@ include file="sub2.jsp" %>

<hr>
<h1> jsp:include jsp file </h1>
<jsp:include page="sub3.jsp"/>

</body>
</html>

sub 页面内容如下

1
2
3
4
5
6
7
8
9
10
11
<!-- sub.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<h2> Name: <%= myname%> <h2/>

<!-- sub2.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<h2> Name: <%=myname%> </h2>

<!-- sub3.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<h2>Name: ${name}</h2>

PS: 这里插入一个语法点,<%= myname%> 这种语法是结合 String myname = (String)request.getAttribute("name"); 只有 jsp 中声明的变量可以这么用,其实这种写法有点累赘,可以通过 EL 表示式 ${name} 直接从内置对象中取值。

配置 web.xml

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>ParentServlet</servlet-name>
<servlet-class>com.jzheng.servlet.ParentServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ParentServlet</servlet-name>
<url-pattern>/parent</url-pattern>
</servlet-mapping>

启动服务器访问 /parent 可以看到如下结果, 几种方式都能 work.

parent

深入理解

查看编译生成的 JSP 文件,可以看到 parent.jsp 和 sub3.jsp 被编译成了 java/class, sub.jsp 和 sub2.jsp 没有。原因是 <%@ include file="xxx" %> 会将页面直接整合到父页面中,而 <jsp:include page="xxx"/> 则是通过 request 转发达到这个效果的。查看 parent_jsp.java 文件可以更清晰一点. sub 页面处理部分已表明。所以 <%@ include file="xxx" %> 中直接使用父页面定义的变量是可以,这种做法更像是定义了一写通用脚本做包含。但是这些变量都会爆红,非常的不爽

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
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;

out.write("\n");
out.write("\n");
out.write("<html>\n");
out.write("<head>\n");
out.write(" <title>Title</title>\n");
out.write("</head>\n");
out.write("<body>\n");
out.write("\n");

String myname = (String)request.getAttribute("name");

out.write("\n");
out.write("\n");
out.write("<hr>\n");
out.write("<h1> include jspf file </h1>\n"); // <---------- sub.jsp part
out.write('\n');
out.write('\n');
out.write("\n");
out.write("<h2> Name: ");
out.print( myname);
out.write(" <h2/>");
out.write("\n");
out.write("\n");
out.write("<hr>\n");
out.write("<h1> include jsp file </h1>\n"); // <---------- sub2.jsp part
out.write("\n");
out.write("<h2> Name: ");
out.print(myname);
out.write(" </h2>\n");
out.write("\n");
out.write("\n");
out.write("<hr>\n");
out.write("<h1> jsp:include jsp file </h1>\n"); // <---------- sub3.jsp part
org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response, "sub3.jsp", out, false);
out.write("\n");
out.write("\n");
out.write("</body>\n");
out.write("</html>\n");

问题:

  • jsp 中如果 include 了其他的 jsp 页面时 request 属性是否能传递到 include 的页面中

测试

新建一个 IndexServlet 为 request 设置 name 属性并 forward 到 index.jsp. index.jsp 包含一个子页面 mypage.jsp 这个页面会拿尝试那 index.jsp 中的 request 属性并显示

1
2
3
4
5
6
7
8
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("name", "jack");
RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
dispatcher.forward(req, resp);
}
}

jsp 页面代码如下

1
2
3
4
5
6
7
8
9
10
11
12
<!-- index.jsp -->
<html>
<body>
<h2>Hello World!</h2>

<jsp:include page="mypage.jsp"/>
</body>
</html>

<!-- mypage.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<h2>Name: <%=request.getAttribute("name")%></h2>

配置 web.xml 指定路由

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>IndexServlet</servlet-name>
<servlet-class>com.jzheng.servlet.IndexServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>IndexServlet</servlet-name>
<url-pattern>/indexServlet</url-pattern>
</servlet-mapping>

启动 server 时跳出来的首页,name 为 null, 访问 /indexServlet 显示的首页 name 属性可以被拿到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
curl 'http://localhost:8080/'
<html>
<body>
<h2>Hello World!</h2>


<h2>Name: null</h2>

</body>
</html>

curl 'http://localhost:8080/indexServlet'
<html>
<body>
<h2>Hello World!</h2>


<h2>Name: jack</h2>

</body>
</html>

探究

Mac 环境下配置的 Tomcat 服务,jsp 编译的页面会放在 /Users/jack/Library/Caches/JetBrains/IntelliJIdea2021.1/tomcat/a0a1bd57-0f17-404d-9ff8-9f185e6d4d97/work/Catalina/localhost/ROOT/org/apache/jsp 这个路径下

1
2
3
4
5
.
├── index_jsp.class
├── index_jsp.java
├── mypage_jsp.class
└── mypage_jsp.java

index_jsp.java 和 mypage_jsp.java 核心代码显示如下

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
// index_jsp.java
response.setContentType("text/html");
pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;

out.write("<html>\n");
out.write("<body>\n");
out.write("<h2>Hello World!</h2>\n");
out.write("\n");
org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response, "mypage.jsp", out, false);
out.write("\n");
out.write("</body>\n");
out.write("</html>\n");

// mypage_jsp.java
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;

out.write("\n");
out.write("<h2>Name: ");
out.print(request.getAttribute("name"));
out.write("</h2>\n");

可以看到父页面中通过 org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response, "mypage.jsp", out, false); 包含页面,这个过程中是会把 request 当成参数传递的,自然在自页面中是能访问到这个对象的。

PS: 在配置 Tomcat 服务器时有两种 type, Tomcat 和 TomEE 之前选错了 TomEE 起不来 server (; ̄ェ ̄)

问题:

  • forward 和 redirect 时 request 传递的区别
  • forward 和 redirect 时 url 传递的区别

问: forward 和 redirect 时 request 传递的区别

forward 可以将 request 传递下去,而 redirect 不能。其实写了 code 之后才意识到,forward 的传递性是因为它直接把之前的 request 当参数传递了,当然是一致的。而 redirect 是不带 request 参数的。

新建一个 ForwardServlet 在这个 servlet 中我们向 request 中设置 name 属性,然后 forward 到 ForwardedServlet. 在 ForwardServlet 中打印处之前设置的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ForwardServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("name", "jack");
RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/forwarded");
dispatcher.forward(req, resp);
}
}

public class ForwardedServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("Name in request: " + req.getAttribute("name"));
}
}

在 web.xml 中配置 mapping 关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<servlet>
<servlet-name>Forward</servlet-name>
<servlet-class>com.jzheng.servlet.ForwardServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Forward</servlet-name>
<url-pattern>/forward</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>Forwarded</servlet-name>
<servlet-class>com.jzheng.servlet.ForwardedServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Forwarded</servlet-name>
<url-pattern>/forwarded</url-pattern>
</servlet-mapping>

name 在终端正确显示。其实光看 dispatch 部分的代码应该就有数了 dispatcher.forward(req, resp); 会将 request 传递下去,能拿到也不奇怪

同样的思路我们设计一个 redirect 的测试。新建 RedirectServlet 并在 request 对象中设置 name 属性,接着 redirect 到 RedirectedServlet 并打印 name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
req.setAttribute("name", "jack");
System.out.println("context path: " + req.getContextPath());
resp.sendRedirect(req.getContextPath() + "/redirected");
}
}

public class RedirectedServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("Name in request: " + req.getAttribute("name"));
}
}

name 为 null. 原因也很简单 resp.sendRedirect(req.getContextPath() + "/redirected"); 并没有传递 request 参数。

问: forward 和 redirect 时 url 传递的区别

forward 方式不会改变 URL,redirect 会变。上面的实验中,我们访问 /redirect 时,最终浏览器显示的是 /redirected