第一章:一个简单的Web服务器
ServerSocket接受http请求,将请求封装为Request,将响应封装为Response,简单返回静态资源
第2章:一个简单的Servlet容器
总的来说,一个全功能 的servlet 容器会为servlet的每个HTTP请求做下面一些工作:
1. 当第一次调用servlet的时候,加载该servlet类并调用servlet的init方法(仅仅一 次)。
2. 对每次请求,构造一个javax.servlet.ServletRequest实例和一个 javax.servlet.ServletResponse 实例。
3. 调用servlet的service方法,同时传递ServletRequest和ServletResponse对象。
4. 当servlet类被关闭的时候,调用servlet的destroy方法并卸载servlet类。
类加载器创建过程
这段代码展示了如何在 Java 中动态加载类,特别是从指定的目录(`Constants.WEB_ROOT`)加载类。以下是代码的详细解析和关键点说明:
### 代码解析
```java
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(Constants.WEB_ROOT);
String repository = (new URL("file", null,
classPath.getCanonicalPath() + File.separator)).toString();
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
```#### 1. **`URL[] urls = new URL[1];`**
- 定义了一个 `URL` 数组,用于存储类加载器的资源库(repository)路径。这里只定义了一个路径,因此数组大小为 1。#### 2. **`URLStreamHandler streamHandler = null;`**
- 定义了一个 `URLStreamHandler` 对象,用于处理 URL 的协议。这里设置为 `null`,表示使用默认的协议处理器。#### 3. **`File classPath = new File(Constants.WEB_ROOT);`**
- 创建了一个 `File` 对象,表示类加载器的根目录。`Constants.WEB_ROOT` 是一个常量,定义了类文件所在的目录路径。#### 4. **`String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();`**
- 构造了一个 `URL` 对象,表示类加载器的资源库路径。这里使用了 `file` 协议,表示本地文件系统路径。
- `classPath.getCanonicalPath()` 获取了 `Constants.WEB_ROOT` 的规范路径,确保路径是绝对的、唯一的。
- `File.separator` 确保路径分隔符与操作系统一致(Windows 使用 `\`,Unix/Linux 使用 `/`)。
- `toString()` 方法将 `URL` 对象转换为字符串形式,用于后续构造 `URLClassLoader`。#### 5. **`urls[0] = new URL(null, repository, streamHandler);`**
- 使用 `repository` 字符串构造了一个 `URL` 对象,并将其存储到 `urls` 数组的第一个位置。
- 这里再次使用了 `null` 作为协议处理器,表示使用默认的协议处理器。#### 6. **`loader = new URLClassLoader(urls);`**
- 使用 `urls` 数组构造了一个 `URLClassLoader` 对象。`URLClassLoader` 是 Java 提供的动态类加载器,用于从指定的 URL 加载类文件。
- 这里的 `urls` 数组包含了一个 `file` 协议的 URL,表示类加载器将从 `Constants.WEB_ROOT` 目录加载类文件。### 关键点说明
1. **动态加载类**
- `URLClassLoader` 是 Java 提供的动态类加载器,允许从指定的 URL 加载类文件。这在开发插件系统、热部署功能或加载第三方库时非常有用。2. **规范路径**
- 使用 `getCanonicalPath()` 确保路径是规范的,避免因路径格式问题导致的错误。例如,它会将 `./` 和 `../` 解析为实际路径。3. **默认协议处理器**
- 在构造 `URL` 和 `URLClassLoader` 时,使用了 `null` 作为协议处理器,表示使用默认的协议处理器。这在大多数情况下是足够的。4. **资源库路径**
- `repository` 字符串表示类加载器的资源库路径。这里使用了 `file` 协议,表示从本地文件系统加载类文件。### 示例:动态加载类
假设 `Constants.WEB_ROOT` 指向一个包含 `.class` 文件的目录,例如:
```
Constants.WEB_ROOT = "/path/to/classes"
```目录结构:
```
/path/to/classes/com/example/MyClass.class
```可以使用以下代码动态加载 `MyClass`:
```java
String className = "com.example.MyClass";
Class<?> clazz = Class.forName(className.replace('/', '.'), true, loader);
Object instance = clazz.getDeclaredConstructor().newInstance();
```### 总结
这段代码展示了如何使用 `URLClassLoader` 动态加载类文件。通过指定资源库路径(`Constants.WEB_ROOT`)和使用规范路径,可以确保类加载器正确加载类文件。这种机制在开发灵活的、可扩展的应用程序时非常有用。
facade类
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request,(ServletResponse) response);
}
这会危害安全性。知道这个servlet容器的内部运作的Servlet程序员可以分别把 ServletRequest和ServletResponse实例向下转换为ex02.pyrmont.Request和 ex02.pyrmont.Response,并调用他们的公共方法。拥有一个Request实例,它们就可以调用parse 方法。拥有一个Response实例,就可以调用sendStaticResource方法。 你不可以把parse和sendStaticResource方法设置为私有的,因为它们将会被其他的类调 用。不过,这两个方法是在个servlet内部是不可见的。其中一个解决办法就是让Request和 Response类拥有默认访问修饰,所以它们不能在ex02.pyrmont包的外部使用。不过,这里有一 个更优雅的解决办法:通过使用facade类。
RequestFacade实现了ServletRequest接口并通过在构造方法中传递一个引用了 ServletRequest对象的Request实例作为参数来实例化。ServletRequest接口中每个方法的实 现都调用了Request对象的相应方法。然而ServletRequest对象本身是私有的,并不能在类的 外部访问。我们构造了一个RequestFacade对象并把它传递给service方法,而不是向下转换 Request对象为ServletRequest对象并传递给service方法。Servlet程序员仍然可以向下转换 ServletRequest实例为RequestFacade,不过它们只可以访问ServletRequest接口里边的公共 方法。现在parseUri方法就是安全的了。
第3章:连接器
Catalina 中有两个主要的模块:连接器和容器。
一个符合Servlet 2.3和2.4 规 范 的 连 接 器 必 须 创 建 javax.servlet.http.HttpServletRequest 和 javax.servlet.http.HttpServletResponse,并传递给被调用的 servlet 的 service 方法。
从本章开始,每章附带的应用程序都会分成模块。这章的应用程序由三个模块组成: connector, startup和core。 startup模块只有一个类,Bootstrap,用来启动应用的。connector模块的类可以分为五组:
1. 连接器和它的支撑类(HttpConnector和HttpProcessor)。
2. 指代HTTP请求的类(HttpRequest)和它的辅助类。
3. 指代HTTP响应的类(HttpResponse)和它的辅助类。
4. Facade类(HttpRequestFacade 和HttpResponseFacade)。
5. Constant类
core 模块由两个类组成:ServletProcessor和StaticResourceProcessor。
第2章中的HttpServer类被分离为两个类:HttpConnector 和HttpProcessor,Request 被 HttpRequest 所取代,而Response被HttpResponse 所取代。
第2章中的HttpServer类的职责是等待HTTP请求并创建请求和响应对象。在本章的应用中, 等待 HTTP 请求的工作交给 HttpConnector 实例,而创建请求和响应对象的工作交给了 HttpProcessor 实例。
本章中,HTTP请求对象由实现了javax.servlet.http.HttpServletRequest的HttpRequest 类来代表。一个 HttpRequest对象将会给转换为一个HttpServletRequest实例并传递给被调用 的servlet 的 service 方法。因此,每个 HttpRequest 实例必须适当增加字段,以便 servlet 可以使用它们。值需要赋给HttpRequest对象,包括URI,查询字符串,参数,cookies和其他 的头部等等。因为连接器并不知道被调用的servlet需要哪个值,所以连接器必须从HTTP请求 中解析所有可获得的值。不过,解析一个HTTP请求牵涉昂贵的字符串和其他操作,假如只是解 析servlet 需要的值的话,连接器就能节省许多CPU周期。例如,假如servlet不 解析任何一 个请求参数(例如不调用 javax.servlet.http.HttpServletRequest 的 getParameter, getParameterMap,getParameterNames 或者 getParameterValues 方法),连接器就不需要从查询 字符串或者 HTTP 请求内容中解析这些参数。Tomcat 的默认连接器(和本章应用程序的连接器) 试图不解析参数直到servlet真正需要它的时候,通过这样来获得更高效率。
Tomcat的默认连接器和我们的连接器使用SocketInputStream类来从套接字的InputStream 中读取字节流。一个 SocketInputStream 实例对从套接字的 getInputStream 方法中返回的 java.io.InputStream 实例进行包装。 SocketInputStream 类提供了两个重要的方法: readRequestLine 和 readHeader。readRequestLine 返回一个 HTTP 请求的第一行。例如,这行 包括了URI,方法和HTTP 版本。因为从套接字的输入流中处理字节流意味着只读取一次,从第 一个字节到最后一个字节(并且不回退),因此readHeader 被调用之前,readRequestLine 必须 只被调用一次。readHeader每次被调用来获得一个头部的名/值对,并且应该被重复的调用知道 所有的头部被读取到。readRequestLine 的返回值是一个 HttpRequestLine 的实例,而 readHeader 的返回值是一个 HttpHeader 对象。
第四章:Tomcat 的默认连接器
本章中提及的“默认连接器”是指Tomcat4的默认连接器。即使默认的连机器已经被弃用, 被更快的,代号为Coyote的连接器所代替,它仍然是一个很好的学习工具。
Tomcat 连接器是一个可以插入servlet 容器的独立模块,已经存在相当多的连接器了,包 括Coyote, mod_jk, mod_jk2 和 mod_webapp。一个 Tomcat 连接器必须符合以下条件:
1. 必须实现接口org.apache.catalina.Connector。
2. 必须创建请求对象,该请求对象的类必须实现接口org.apache.catalina.Request。
3. 必须创建响应对象,该响应对象的类必须实现接口org.apache.catalina.Response。
Tomcat4 的默认连接器类似于第3章的简单连接器。它等待前来的HTTP请求,创建request 和 response 对象,然后把 request 和 response 对象传递给容器。连接器是通过调用接口 org.apache.catalina.Container 的 invoke 方法来传递 request 和 response 对象的。invoke 的方法签名如下所示:
public void invoke( org.apache.catalina.Request request, org.apache.catalina.Response response);
在invoke 方法里边,容器加载servlet,调用它的service 方法,管理会话,记录出错日 志等等。
第五章 容器
容器接口
容器是一个处理用户servlet请求并返回对象给web用户的模块。 org.apache.catalina.Container 接口定义了容器的形式,有四种容器:Engine (引擎), Host(主机), Context(上下文), 和 Wrapper(包装器)。这一 章将会介绍context和wrapper,而 Engine和Host会留到第十三章介绍。
对于Catalina的容器首先需要注意的是它一共有四种不同的容器:
Engine:表示整个Catalina的servlet引擎 ·
Host:表示一个拥有数个上下文的虚拟主机 ·
Context:表示一个Web应用,一个context包含一个或多个wrapper
Wrapper:表示一个独立的servlet
Pipelining Tasks
一个pipeline包含了该容器要唤醒的所有任务。每一个阀门表示了一个特定的 任务。一个容器的流水线有一个基本的阀门,但是你可以添加任意你想要添加的 阀门。阀门的数目定义为添加的阀门的个数(不包括基本阀门)。有趣的是,阀门可以通过编辑Tomcat的配置文件server.xml来动态的添加。
一个容器可以有一个流水线。当容器的invoke方法被调用的时候,容器将会处 理流水线中的阀门,并一个接一个的处理,直到所有的阀门都被处理完毕。可以 想象流水线的invoke方法的伪代码如下所示:
但是,Tomcat的设计者选择了一种不同的通过 org.apache.catalina.ValveContext 定义的方式来处理,这里介绍它如何工作 的:
Pipeline 是容器中Pipeline接口的一个实例。 现在,流水线必须保证说要添加给它的阀门必须被调用一次,流水线通过创建一 个ValveContext 接口的实例来实现它。ValveContext是流水线的的内部类,这 样ValveContext 就可以访问流水线中所有的成员。ValveContext中最重要的方 法是invokeNext 方法:
在创建一个ValveContext实例之后,流水线调用ValveContext的invokeNext 方法。ValveContext 会先唤醒流水线的第一个阀门,然后第一个阀门会在完成 它的任务之前(或之后)唤醒下一个阀门。ValveContext将它自己传递给每一个阀门,那 么该阀门就可以调用ValveContext的invokeNext方法。Valve接口的invoke 签名如下:
如果一个Valve先调用invokeNext,再执行自己的逻辑代码,那么这是一个后置阀门,即该Valve会在basic valve执行后再执行自己的逻辑代码。
如果一个Valve先执行自己的逻辑代码,再调用invokeNext,那么这是一个前置阀门,即该Valve会在basic valve执行前执行自己的逻辑代码。
这是使用责任链模式的优点,即valve可以通过控制invokeNext的位置,从而控制该valve是在basic valve前或者后执行自己的逻辑代码。for循环就无法提供这种功能。
总体来说pipeline中存放一个valve列表,表示所有需要执行的valve,以及一个basic valve(用来执行容器的本来任务,如调用servlet的service方法)。ValveContext负责维护一个索引用来指向当前需要执行的valve。ValveContext的invokeNext方法会执行索引指向的valve。pipeline中整个valve列表的执行由valve在其invoke方法中通过调用ValveContext的invokeNext方法驱动。具体执行流程如下:
1. 首先pipeline调用ValveContext的invokeNext方法,执行第一个valve
2. valve的invoke方法中调用ValveContext的invokeNext方法驱动着对整个valve列表的递归调用,那些前置valve的逻辑代码会被调用
3. 直到basic valve,其invoke方法不再调用ValveContext的invokeNext方法
4. 整个递归方法栈开始逐个弹出,那些后置valve开始从后向前的执行各自的逻辑代码
相关接口:
Wrapper 接口
org.apache.catalina.Wrapper 接口表示了一个包装器。一个包装器是表示一个 独立servlet定义的容器。包装器继承了Container接口,并且添加了几个方法。 包装器的实现类负责管理其下层servlet的生命周期,包括servlet的 init,service,和 destroy 方法。由于包装器是最底层的容器,所以不可以将子 容器添加给它。如果addChild方法被调用的时候会产生 IllegalArgumantException 异常。
包装器接口中重要方法有allocate和load方法。allocate方法负责定位该包 装器表示的servlet的实例。Allocate方法必须考虑一个servlet是否实现了 avax.servlet.SingleThreadModel 接口,该部分内容将会在 11章中进行讨论。 Load 方法负责load和初始化servlet的实例。它们的签名如下:
Context接口
一个context在容器中表示一个web应用。一个context通常含有一个或多个包 装器作为其子容器。 重要的方法包括addWrapper, createWrapper 等方法。该接口将会在第12章中 详细介绍。
SimpleWrapper
SimpleWrapper 实现了 Wrapper 接口。SimpleWrapper 类包括一个Pipeline和一个 Loader 类(来加载一个 servlet)。流水线包 括一个基本阀门和两个另外的阀门 ClientIPLoggerValve 和HeaderLoggerValve.该应用的类结构图如图 5.3所示:
SimpleContext
SimpleContext表示一个上下文,它使用SimpleContextMapper作为它的mapper, SimpleContextValve 作为它的基本阀门。该上下文包括两个阀门 ClientIPLoggerValve 和 HeaderLoggerValve。用 SimpleWrapper 表示的两个包装器作为该上下文的子容器被添加。包装器把SimpleWrapperValve作为它的基 本阀门,但是没有其它的阀门了。 运行流程如下:
1. 一个容器有一个流水线,容器的invoke方法会调用流水线的 invoke 方法。
2. 流水线的invoke方法会调用添加到容器中的阀门的invoke方法, 然后调用基本阀门的invoke方法。
3. 在一个包装器中,基本阀门负责加载相关的servlet类并对请求作 出相应。
4. 在一个有子容器的上下文中,基本法门使用mapper来查找负责处 理请求的子容器。如果一个子容器被找到,子容器的invoke方法会被调 用,然后返回步骤1。
第六章 生命周期
Catalina 由多个组件组成,当Catalina启动的时候,这些组件也会启动。当 Catalina 停止的时候,这些组件也必须有机会被清除。例如,当一个容器停止 工作的时候,它必须唤醒所有加载的servlet的destroy方法,而session管理 器要保存session到二级存储器中。保持组件启动和停止一致的的机制通过实现 org.apache.catalina.Lifecycle 接口来实现。
一个实现了Lifecycle接口的组件同是会触发一个或多个下列事件: BEFORE_START_EVENT, START_EVENT, AFTER_START_EVENT, BEFORE_STOP_EVENT, STOP_EVENT, and AFTER_STOP_EVENT。当组件被启动的时候前三个事件会被触发, 而组件停止的时候会触发后边三个事件。另外,如果一个组件可以触发事件,那 么必须存在相应的监听器来对触发的事件作出回应。监听器使用 org.apache.catalina.LifecycleListener 来表示。
Lifecycle 接口
Catalina 的设计允许一个组件包含其它的组件。例如一个容器可以包含一系列 的组件如加载器、管理器等。一个父组件负责启动和停止其子组件。Catalina 的设计成所有的组件被一个父组件来管理(in custody),所以启动bootstrap 类只需启动一个组件即可。这种单一的启动停止机制通过继承Lifecycle来实现。
LifecycleEvent类
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); } }
LifecycleListener 接口
LifecycleSupport 类
public final class LifecycleSupport {
public LifecycleSupport(Lifecycle lifecycle) {
super();
this.lifecycle = lifecycle;
}
private Lifecycle lifecycle = null;
private LifecycleListener listeners[] = new LifecycleListener[0];
public void addLifecycleListener(LifecycleListener listener) {
synchronized (listeners) {
LifecycleListener results[] =
new LifecycleListener[listeners.length + 1];
for (int i = 0; i < listeners.length; i++)
results[i] = listeners[i];
results[listeners.length] = listener;
listeners = results;
}
}
public LifecycleListener[] findLifecycleListeners() {
return listeners;
}
public void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);
LifecycleListener interested[] = null;
synchronized (listeners) {
interested = (LifecycleListener[]) listeners.clone();
}
for (int i = 0; i < interested.length; i++)
interested[i].lifecycleEvent(event);
}
public void removeLifecycleListener(LifecycleListener listener) {
synchronized (listeners) {
int n = -1;
for (int i = 0; i < listeners.length; i++) {
if (listeners[i] == listener) {
n = i;
break;
}
}
if (n < 0)
return;
LifecycleListener results[] =
new LifecycleListener[listeners.length - 1];
int j = 0;
for (int i = 0; i < listeners.length; i++) {
if (i != n)
results[j++] = listeners[i];
}
listeners = results;
}
}
}
SimpleContext
public void addLifecycleListener(LifecycleListener listener) {
lifecycle.addLifecycleListener(listener);
}
public LifecycleListener[] findLifecycleListeners() {
return null;
}
public void removeLifecycleListener(LifecycleListener listener) {
lifecycle.removeLifecycleListener(listener);
}
public synchronized void start() throws LifecycleException {
if (started)
throw new LifecycleException("SimpleContext has already started");
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
started = true;
try {
// Start our subordinate components, if any
if ((loader != null) && (loader instanceof Lifecycle))
((Lifecycle) loader).start();
// Start our child containers, if any
Container Children[] = findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i] instanceof Lifecycle)
((Lifecycle) children[i]).start();
}
// Start the Valves in our pipeline (including the basic),
// if any
if (pipeline instanceof Lifecycle)
((Lifecycle) pipeline).start();
// Notify our Interested LifecycleListeners
lifecycle.firelifecycleEvent(START_EVENT, null);
}
catch (Exception e) {
e.printStackTrace();
}
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
}
public void stop() throws LifecycleException {
if (!started)
throw new LifecycleException("SimpleContext has not been started");
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null);
lifecycle.fireLifecycleEvent(STOP_EVENT, null);
started = false;
try {
// Stop the Valves in our pipeline (including the basic), if any
if (pipeline instanceof Lifecycle) (
((Lifecycle) pipeline).stop();
}
// Stop our child containers, if any
Container children[] = findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i] instanceof Lifecycle)
((Lifecycle) children[i]).stop();
}
if ((loader != null) && (loader instanceof Lifecycle)) {
((Lifecycle) loader).stop();
}
}
catch (Exception e) {
e.printStackTrace();
}
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null);
}
第七章:日志系统
Logger接口
public interface Logger {
public static final int FATAL = Integer.MIN_VALUE;
public static final int ERROR = 1;
public static final int WARNING = 2;
public static final int INFORMATION = 3;
public static final int DEBUG = 4;
public Container getContainer();
public void setContainer(Container container);
public String getInfo();
public int getVerbosity();
public void setVerbosity(int verbosity);
public void addPropertyChangeListener(PropertyChangeListener
listener);
public void log(String message);
public void log(Exception exception, String msg);
public void log(String message, Throwable throwable);
public void log(String message, int verbosity);
public void log(String message, Throwable throwable, int verbosity);
public void removePropertyChangeListener(PropertyChangeListener
listener);
}
Tomcat 日志系统
第八章:加载器
。如果像前面章节中那样使用系统的加载器来加载servlet和其他需要的类, 这样servlet就可以进入Java虚拟机CLASSPATH环境下面的任何类和类库,这 会带来安全隐患。Servlet只允许访问WEB-INF/目录及其子目录下面的类以及部 署在WEB-INF/lib目录下的类库。所以一个servlet容器需要一个自己的加载器, 该加载器遵守一些特定的规则来加载类。在Catalina中,加载器使用 org.apache.catalina.Loader 接口表示。
Tomcat 需要一个自己的加载器的另一个原因是它需要支持在WEB-INF/classes 或者是WEB-INF/lib目录被改变的时候会重新加载。Tomcat的加载器实现中使 用一个单独的线程来检查servlet和支持类文件的时间戳。要支持类的自动加载 功能,一个加载器类必须实现org.apache.catalina.loader.Reloader接口。
Java 类加载器
在每次创建一个Java类的实例时候,必须先将该类加载到内存中。Java虚拟机 (JVM)使用类加载器来加载类。Java加载器在Java核心类库和CLASSPATH环 境下面的所有类中查找类。如果需要的类找不到,会抛出 java.lang.ClassNotFoundException 异常。 从J2SE1.2 开始,JVM使用了三种类加载器:bootstrap类加载器、extension 类加载器和systen类加载器。这三个加载器是父子关系,其中bootstrap类加 载器在顶端,而system加载器在结构的最底层。
其中bootstrap类加载器用于引导JVM,一旦调用java.exe程序,bootstrap 类加载器就开始工作。因此,它必须使用本地代码实现,然后加载JVM需要的类 到函数中。另外,它还负责加载所有的Java核心类,例如java.lang和java.io 包。另外bootstrap类加载器还会查找核心类库如rt.jar、i18n.jar等,这些 类库根据JVM和操作系统来查找。
extension 类加载器负责加载标准扩展目录下面的类。这样就可以使得编写程序 变得简单,只需把JAR文件拷贝到扩展目录下面即可,类加载器会自动的在下面 查找。不同的供应商提供的扩展类库是不同的,Sun公司的JVM的标准扩展目录 是/jdk/jre/lib/ext。
system 加载器是默认的加载器,它在环境变量CLASSPATH目录下面查找相应的 类。
这样,JVM使用哪个类加载器?答案在于委派模型(delegation model),这是出 于安全原因。每次一类需要加载,system 类加载器首先调用。但是,它不会马 上加载类。相反,它委派该任务给它的父类-extension 类加载器。extension 类加载器也把任务委派给它的父类bootstrap类加载器。因此,bootstrap类加 载器总是首先加载类。如果 bootstrap 类加载器不能找到所需要的类的 extension 类加载器会尝试加载类。如果扩展类加载器也失败,system类加载器 将执行任务。如果系统类加载器找不到类,一个 java.lang.ClassNotFoundException 异常。为什么需要这样的往返模式? 委派模型对于安全性是非常重要的。如你所知,可以使用安全管理器来限制访问 某个目录。现在,恶意的意图有人能写出一类叫做 java.lang.Object,可用于 访问任何在硬盘上的目录。因为JVM的信任java.lang.Object类,它不会关注 这方面的活动。因此,如果自定义java.lang.Object 被允许加载的安全管理器 将很容易瘫痪。幸运的是,这将不会发生,因为委派模型会阻止这种情况的发生。 下面是它的工作原理。当自定义java.lang.Object类在程序中被调用的时候,system类加载器将该请 求委派给extension类加载器,然后委派给bootstrap类加载器。这样bootstrap 类加载器先搜索的核心库,找到标准java.lang.Object并实例化它。这样,自 定义java.lang.Object 类永远不会被加载。
Tomcat的需求自定义自己的类加载器原因包括以下内容
1. 要制定类加载器的某些特定规则(一个context的类加载器只能加载自己目录下的类,无法加载其他应用的类)
2. 缓存以前加载的类
3. 事先加载类以预备使用
Loader 接口
在Web应用程序中加载servlet和其他类需要遵循一些规则。例如,在一个应用程序中Servlet可以使用部署到WEB-INF/classes目录和任何子目录下面的类。 然而,servlet不能访问其他应用的类,即使这些类是在运行Tomcat 所在的JVM 的CLASSPATH 中。此外,一个servlet只能访问WEB-INF/lib目录下的类库,而不能访问其他目录下面的。
一个Tomcat类加载器表示一个Web应用程序加载器,而不是一个类加载器。一 个加载器必须实现org.apache.catalina.Loader接口。加载器的实现使用定制的类加载器org.apache.catalina.loader.WebappClassLoader。可以使用 Loader 接口的getClassLoader 方法获取一个WebappClassLoader。
值得一提的是Loader接口定义了一系列方法跟库协作。Web应用程序的 WEB-INF/classes 和 WEB-INF/lib 目录作为库添加上。Loader接口的 addReposity 方法用于添加一个库,findRepositories方法用于返回一个所有库 的队列。
一个Tomcat的加载器通常跟一个上下文相关联,Loader接口的和getContainer 及setContainer 方法是建立此关联。一个加载器还可以支持重新加载,如果在 上下文中的一个或多个类已被修改。这样,一个 servlet 程序员可以重新编译 servlet 或辅助类,新类将被重新加载而不需要不重新启动 Tomcat 加载。为了 达到重新加载的目的,Loader 接口有modify方法。在加载器的实现中,如果在其 库中一个或多个类已被修改,modeify方法必须返回true,因此需要重新加载。 一个加载器不是自己进行重新加载,而是调用上下文接口的重载方法。。另外两个方法, setReloadable 和 getReloadable,用于确定加载器中是否可以使用重加载。默认情况下,在标准的上下文实现中(org.apache.catalina.core.StandardContext 类将在第 12 章讨论)重载机制并未启用。因此,要使得上下文启动重载机制,需要在server.xml文件添加一 些元素如下:
另外,一个加载器的实现可以确定是否委派给父加载器类。为了实现这一点, Loader 接口提供了getDelegate 和setDelegate方法。
public interface Loader { public ClassLoader getClassLoader(); public Container getContainer(); public void setContainer(Container container); public DefaultContext getDefaultContext(); public void setDefaultContext(DefaultContext defaultContext); public boolean getDelegate(); public void setDelegate(boolean delegate); public String getInfo(); public boolean getReloadable(); public void setReloadable(boolean reloadable); public void addPropertyChangeListener(PropertyChangeListener listener); public void addRepository(String repository); public String[] findRepositories(); public boolean modified(); public void removePropertyChangeListener(PropertyChangeListener listener); }
Catalina 提供了org.apache.catalina.loader.WebappLoader 作为Load接口的 实现。WebappLoader 对象包含一个 org.apache.catalina.loader.WebappClassLoader 类的实例,该类扩展了 Java.netURLClassLoader 类。
WebappLoader 类
org.apache.catalina.loader.WebappLoader 类是 Loader 接口的实现,它表示 一个web应用程序的加载器,负责给web应用程序加载类。WebappLoader创建 一个org.apache.catalina.loader.WebappClassLoader 类的实例作为它的类加 载器。像其他的Catalina组件一样,WebappLoader实现了 org.apache.catalina.Lifecycle 接口,可有由关联容器启动和停止。 WebappLoader 类还实现了java.lang.Runnable 接口,所以可以通过一个线程来 重复的调用modified方法,如果modified方法返回true,WebappLoader实例通知它的关联容器。类通过上下文重新加载自己,而不是WebappLoader。上下文怎么实现该功能会在第12章标准上下文中介绍。
WebappLoader 类的 start 方法被调用的时候,将会完成下面几项重要任务:
创建一个类加载器
设置库
设置类路径
设置访问权限
开启一个新线程用来进行自动重载
1. 创建类加载器:
WebappLoader 使用一个内部类加载器来加载类。可以回头看Loader接口,该接 口提供了getClassLoader方法但是并没有setClassLoader方法。因此,不能通 过传递一个WebappLoader来初始化它。这样没有默认类加载器是否意味着 WebappLoader 不够灵活的? 答案当然是否定的,WebappLoader类提供了getLoaderClass 和 setLoaderClass 方法来获得或者改变它的私有变量loaderClass的值。该变量 是一个的表示加载器类名String类型表示形式。默认的loaderClass值是 org.apahce.catalina.loader.WebappClassLoader,如果你愿意,可以创建继承 WebappClassLoader 类的自己的加载器,然后使用setLoaderClass方法来强制 WebappLoader 使用你创建的加载器。否则,当它WebappLoader启动的时候,它 会使用它的私有方法createClassLoader创建WebappClassLoader的实例
private WebappClassLoader createClassLoader() throws Exception { Class clazz = Class.forName(loaderClass); WebappClassLoader classLoader = null; if (parentClassLoader == null) { // Will cause a ClassCast if the class does not extend // WebappClassLoader, but this is on purpose (the exception will be // caught and rethrown) classLoader = (WebappClassLoader) clazz.newInstance(); // in Tomcat 5, this if block is replaced by the following: // if (parentClassLoader == null) { // parentClassLoader = // Thread.currentThread().getContextClassLoader(); // } } else { Class[] argTypes = { ClassLoader.class }; Object[] args = { parentClassLoader }; Constructor constr = clazz.getConstructor(argTypes); classLoader = (WebappClassLoader) constr.newInstance(args); } return classLoader; }
2. 设置库
WebappLoader的start方法会调用setRepositories方法来给类加载器添加一 个库。WEB-INF/classes目录传递给加载器addRepository方法,而WEB-INF/lib 传递给加载器的setJarPath方法。这样,类加载器能能从WEB-INF/classes 目 录下面和WEB-INF/lib目录下面部署的类库里加载类。
3. 设置类路径
该任务由start方法调用setClassPath方法完成,setClassPath方法会给 servlet上下文分配一个String类型属性保存Jasper JSP编译的类路径,该内 容先不予讨论。
4. 设置访问权限
如果Tomcat使用了安全管理器,setPermissions给类加载器给必要的目录添加 访问权限,例如WEB-INF/classes和WEB-INF/lib。如果不使用管理器,该方法 马上返回。
5. 开启自动重载线程
WebappLoader支持自动重载,如果WEB-INF/classes或者WEB-INF/lib目录被 重新编译过,在不重启Tomcat的情况下必须自动重新载入这些类。为了实现这 个目的,WebappLoader有一个单独的线程每个x秒会检查源的时间戳。x的值由 checkInterval变量定义,它的默认值是15,也就是每隔15秒会进行一次检查 是否需要自动重载。该类还提供了两个方法getCheckInterval和 setCheckInterval方法来访问或者设置checkInterval的值。
在Tomcat4中,WebappLoader实现了java.lang.Runnable接口来支持自动重载。
public void run() { if (debug >= 1) log("BACKGROUND THREAD Starting"); // Loop until the termination semaphore is set while (!threadDone) { // Wait for our check interval threadSleep(); if (!started) break; try { // Perform our modification check if (!classLoader.modified()) continue; } catch (Exception e) { log(sm.getString("webappLoader.failModifiedCheck"), e); continue; } // Handle a need for reloading notifyContext(); break; } if (debug >= 1) log("BACKGROUND THREAD Stopping"); } private void notifyContext() { WebappContextNotifier notifier = new WebappContextNotifier(); (new Thread(notifier)).start(); } protected class WebappContextNotifier implements Runnable { public void run() { ((Context) container).reload(); } }
WebappClassLoader 类
类org.apache.catalina.loader.WebappClassLoader 表示在一个 web 应用程序 中使用的加载器。WebappClassLoader类继承了java.net.URLClassLoader 类, 该类在前面章节中用于加载Java类。
WebappClassLoader 被可以的进行了优化和安全方面的考虑。例如它缓存了以前 加载的类以改进性能,下一次收到第一次没有找到的类的请求的时候,可以直接 抛出ClassNotFound 异常。WebappClassLoader 在源列表以及特定的JAR文件中 查找类。
处于安全性的考虑,WebappClassLoader类不允许一些特定的类被加载。这些类 被存储在一个String类型的数组中,现在仅仅有一个成员。
另外在委派给系统加载器的时候,你也不允许加载属于该包的其它类或者它的子 包:
缓存:
为了提高性能,当一个类被加载的时候会被放到缓存中,这样下次需要加载该类 的时候直接从缓存中调用即可。缓存由WebappClassLoader类实例自己管理。另 外,java.lang.ClassLoader 维护了一个Vector,可以避免前面加载过的类被当 做垃圾回收掉。在这里,缓存被该超类管理。 每一个可以被加载的类(放在 WEB-INF/classes 目录下的类文件或者 JAR 文件) 都被当做一个源。一个源被org.apache.catalina.loader.ResourceEntry类表 示。一个ResourceEntry实例保存一个byte类型的数组表示该类、最后修改的 数据或者副本等等。
public class ResourceEntry { public long lastModifled = -1; // Binary content of the resource. public byte[] binaryContent = null; public Class loadedClass = null; // URL source from where the object was loaded. public URL source = null; // URL of the codebase from where the object was loaded. public URL CodeBase = null; public Manifest manifest = null; public Certificate[] certificates = null; }
所有缓存的源被存放在一个叫做resourceEntries的HashMap中,键值为源名, 所有找不到的源都被放在一个名为notFoundResources的HashMap中。
加载类:
当加载一个类的时候,WebappClassLoader类遵循以下规则:
· 所有加载过的类都要进行缓存,所以首先需要检查本地缓存。
· 如果无法再本地缓存找到类,使用java.langClassLoader类 的findLoaderClass 方法在缓存查找类、
· 如果在两个缓存中都无法找到该类
· 如果使用了安全管理器,检查该类是否允许加载,如果该类不允许加载,则抛出ClassNotFoundException 异常。
· 如果要加载的类使用了委派标志或者该类属于trigger包中, 使用父加载器来加载类,如果父加载器为null,使用系统加载器加载。
· 从当前的源中加载类
· 如果在当前的源中找不到该类并且没有使用委派标志,使用父类加载器。如果父类加载器为null,使用系统加载器
· 如果该类仍然找不到,抛出ClassNotFoundException异常
一个应用程序加载器,简单的说就是加载器是Catalina中最重要的组件之一。 它使用一个内部的类加载器来完成加载类的工作。Tomcat使用该内部类加载器 加载应用类,它属于一个应用上下文并且遵循一系列规则。另外,该加载器还支 持缓存以及检测类修改情况的功能。
第九章:session管理
Catalina通过一个叫管理器的组件来完成session管理工作,该组件由 org.apache.catalina.Manager interface接口表示。一个管理器通常跟一个上 下文容器相关联,它负责创建、更行以及销毁session对象并能给任何请求组件 返回一个合法的session。
一个servlet可以使用getSession方法获得一个session对象,该方法在 javax.servlet.http.HttpServletRequest定义。它在默认连接器里由 org.apache.catalina.connector.HttpRequestBase类实现。这里是 HttpRequestBase类的一些相关方法。
public HttpSession getSession() { return (getSession(true)); } public HttpSession getSession(boolean create) { ... return doGetSession(create); } private HttpSession doGetSession(boolean create) { // There cannot be a session if no context has been assigned yet if (context == null) return (null); // Return the current session if it exists and is valid if ((session != null) && !session.isValid()) session = null; if (session != null) return (session.getSession()); // Return the requested session if it exists and is valid Manager manager = null; if (context != null) manager = context.getManager(); if (manager == null) return (null); // Sessions are not supported if (requestedSessionId != null) { try { session = manager.findSession(requestedSessionId); } catch (IOException e) { session = null; } if ((session != null) && !session.isValid()) session = null; if (session != null) { return (session.getSession()); } } // Create a new session if requested and the response is not // committed if (!create) return (null); ... session = manager.createSession(); if (session != null) return (session.getSession()); else return (null); }
默认情况下管理器将session对象存储在内存中,但是Tomcat也允许将session 对象存储在文件或者数据库中(通过JDBC)。Catalina在 org.apache.catalina.session包中提供了session对象和session管理的相关 类型。
Sessions
在servlet编程中,一个session对象使用javax.servlet.http.HttpSession 接口表示。该接口的标准实现是StandardSession类,该类在 org.apache.catalina.session包中。但是出于安全的原因,管理器并不会将一 个StandardSession实例传递给servlet。而是使用 org.apache.catalina.session包中的外观类StandardSessionFacade。 在内部,一 个管理器使用了另一个接口:org.apache.catalina.Session。
public interface Session { public static final String SESSION_CREATED_EVENT = "createSession"; public static final String SESSION_DESTROYED_EVENT = "destroySession"; public String getAuthType(); public void setAuthType(String authType); public long getCreationTime(); public void setCreationTime(long time); public String getId(); public void setId(String id); public String getInfo(); public long getLastAccessedTime(); public Manager getManager(); public void setManager(Manager manager); public int getMaxInactiveInterval(); public void setMaxInactiveInterval(int interval); public void setNew(boolean isNew); public Principal getPrincipal(); public void setPrincipal(Principal principal); public HttpSession getSession(); public void setValid(boolean isValid); public boolean isValid(); public void access(); public void addSessionListener(SessionListener listener); public void expire(); public Object getNote(String name); public Iterator getNoteNames(); public void recycle(); public void removeNote(String name); public void removeSessionListener(SessionListener listener); public void setNote(String name, Object value); }
由于一个Session对象常常被一个管理器持有,所以接口提供了setManager和 getManager 方法来关联一个Session对象和一个管理器。另外,一个Session 实例在跟管理器相关联的容器有一个唯一的ID。对于该ID有setId和getId方 法相关。getLastAccessedTime 方法由管理器来调用,以确定一个Session对象 是否合法。管理器调用setValid方法来重置一个session的合法性。每次一个 Session 被访问的时候,都会调用access方法更新它的最后访问时间。最后, 管理器可以调用expire方法来终止一个expire方法,使用getSession可以获得一个包装在外观内的HttpSession对象。
StandardSession 类:
StandardSession 类是 Session 接口的标准是实现。另外,实现了 javax.servlet.http.HttpSession 和 org.apache.catalina.Session 之外,它 还实现了java.lang.Serializable 接口来使得Session对象可序列化。 该类的构造器获得一个管理器实例来强制使得每个Session对象都有一个管理器。该类的构造器获得一个管理器实例来强制使得每个Session对象都有一个管理 器。接下来是几个重要的变量在存放Session状态。注意transient使得该关键字不 可序列化。一个Session对象如果在由maxInactiveInterval变量的时间内没有被访问则被终结。使用Session接口中定义的expire方法可以终结一个Session对象。
// session attributes private HashMap attributes = new HashMap(); // the authentication type used to authenticate our cached Principal, if any private transient String authType = null; private long creationTime = 0L; private transient boolean expiring = false; private transient StandardSessionFacade facade = null; private String id = null; private long lastAccessedTime = creationTime; // The session event listeners for this Session. private transient ArrayList listeners = new ArrayList(); private Manager manager = null; private int maxInactiveInterval = -1; // Flag indicating whether this session is new or not. private boolean isNew = false; private boolean isValid = false; private long thisAccessedTime = creationTime; public HttpSession getSession() { if (facade == null) facade = new StandardSessionFacade(this); return (facade); } public void expire(boolean notify) { // Mark this session as "being expired" if needed if (expiring) return; expiring = true; setValid(false); // Remove this session from our manager's active sessions if (manager != null) manager.remove(this); // Unbind any objects associated with this session String keys [] = keys(); for (int i = 0; i < keys.length; i++) removeAttribute(keys[i], notify); // Notify interested session event listeners if (notify) { fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null); } // Notify interested application event listeners // FIXME - Assumes we call listeners in reverse order Context context = (Context) manager.getContainer(); Object listeners[] = context.getApplicationListeners(); if (notify && (listeners != null)) { HttpSessionEvent event = new HttpSessionEvent(getSession()); for (int i = 0; i < listeners.length; i++) { int j = (listeners.length - 1) - i; if (!(listeners[j] instanceof HttpSessionListener)) continue; HttpSessionListener listener = (HttpSessionListener) listeners[j]; try { fireContainerEvent(context, "beforeSessionDestroyed", listener); listener.sessionDestroyed(event); fireContainerEvent(context, "afterSessionDestroyed", listener); } catch (Throwable t) { try { fireContainerEvent(context, "afterSessionDestroyed", listener); } catch (Exception e) { ; } // FIXME - should we do anything besides log these? log(sm.getString("standardSession.sessionEvent"), t); } } } // We have completed expire of this session expiring = false; if ((manager != null) && (manager instanceof ManagerBase)) { recycle(); } }
StandardSessionFacade类:
要将一个Session对象传递给一个servlet,Catalina会初始化一个 StandardSession类填充StandardSessionFacade并把StandardSessionFacade传递给servlet。。这样,servlet就不能通过将HttpSession向下转化为StandardSessionFacade类来访问StandardSession的public方法。
管理器
管理器用来管理Session对象。例如它创建Session对象并销毁它们。管理器由 org.apache.catalina.Manager 接口表示。在 Catalina 中, org.apache.catalina.session 包中类ManagerBase类提供了常用函数的基本实现。ManagerBase 类有两个直接子类:StandardManager和 PersistentManagerBase 类。 在运行的时候,StandardManager将session 对象存放在内存中。但是,当停止 的时候,它将Session对象存放到文件中。当它再次启动的时候,重新载入 Session 对象。 PersistentManagerBase 类作为一个管理器组件将Session对象存放到二级存储 器中。它有两个直接子类:PersistentManager和DistributedManager类 (DistributedManager)类只在Tomcat4 中有。
第十章:安全
有些web应用程序的内容是有限制的,只允许有权限的用户在提供正确的用户名 和密码的情况下才允许访问。Servlet通过配置部署文件web.xml来对安全性提 供技术支持。本章的主要内容是容器对于安全性限制的支持。
一个servlet通过一个叫authenticator的阀门(valve)来支持安全性限制。 当容器启动的时候,authenticator被添加到容器的流水线上。如果你忘了流水 线是如何工作的,需要重新复习下第六章的内容。
authenticator 阀门会在包装器阀门之前被调用。authenticator用于对用户进 行验证,如果用户熟人了正确的用户名和密码,authenticator阀门调用下一个 用于处理请求servlet的阀门。如果验证失败,authenticator不唤醒下一个阀 门直接返回。由于验证失败,用户并不能看到请求的servlet。
在用户验证的时候authenticator阀门调用的是上下文域(realm)内的 authenticate 方法,将用户名和密码传递给它。该容器域可以访问合法的用户 名密码。
本章首先介绍跟安全性相关的类(realms、principal、roles),然后通过一个 应用程序演示了如何在你的servlets上使用安全管理。
(域)Realm
域是用于进行用户验证的一个组件,它可以告诉你一个用户名密码对是否是合法 的。一个域跟一个上下文容器相联系,一个容器可以只有一个域。可以使用容器 的setRealm 方法来建立它们之间的联系。
一个域是如何验证一个用户的合法性的?一个域拥有所有的合法用户的密码或 者是可以访问它们。至于它们存放在哪里则取决于域的实现。在Tomcat的默认 实现里,合法用户被存储在tomcat-users.xml文件里。但是可以使用域的其它 实现来访问其它的源,如关系数据库。
在Catalina 中,一个域用接口org.apache.catalina.Realm表示。该接口最重 要的方法是四个authenticate方法:
public Principal authenticate(String username, String credentials); public Principal authenticate(String username, byte[] credentials); public Principal authenticate(String username, String digest, String nonce, String nc, String cnonce, String qop, String realm, String md5a2); public Principal authenticate(X509Certificate certs[]);
第一个方法是最常用的方法,Realm接口还有一个getRole方法,签名如下:
public boolean hasRole(Principal principal, String role);
另外,域还有getContainer和setContainer方法用于建立域与容器的联系。 一个域的基本上实现是抽象类org.apache.catalina.realm.RealmBase。 org.apache.catalina.realm包中海提供了其它一些类继承了RealmBase如: JDBCRealm, JNDIRealm, MemoryRealm,和 UserDatabaseRealm。默认情况下使用 的域是MemoryRealm。
GenericPrincipal
一个principal使用java.security.Principal接口来表示,Tomcat中该接口 的实现为org.apache.catalina.realm.GenericPrincipal接口。一个 GenericPrincipal必须跟一个域相关联。GenericPrincipal必须拥有一个用户名和一个密码,此外还可选择性的传递一 列角色。可以使用hasRole方法来检查一个principal是否有一个特定的角色, 传递的参数为角色的字符串表示形式。这里是Tomcat4中的hasRole方法:
public GenericPrincipal(Realm realm, String name, String password) { this(realm, name, password, null); } public GenericPrincipal(Realm realm, String name, String password, List roles) { super(); this.realm = realm; this.name = name; this.password = password; if (roles != null) { this.roles = new String[roles.size()]; this.roles = (String[]) roles.toArray(this.roles); if (this.roles.length > 0) Arrays.sort(this.roles); } } public boolean hasRole(String role) { if (role == null) return (false); return (Arrays.binarySearch(roles, role) >= 0); } public boolean hasRole(String role) { if ("*".equals(role)) // Special 2.4 role meaning everyone return true; if (role == null) return (false); return (Arrays.binarySearch(roles, role) >= 0); }
LoginConfig 类
封装了realm名和验证方式,getRealmName来获取realm名(这个名称通常在登录表单中显示给用户,用于提示用户他们正在登录的域),getAuthName获取验证方式。一个验证(authentication) 的名字必须是下面的之一:BASIC, DIGEST, FORM, 或者CLIENT-CERT。如果用到的是基于表单(form)的验证,该LoginConfig对象还包括登录或者错误页面 像对应的URL。
Tomcat 一个部署启动的时候,先读取web.xml。如果web.xml包括一个 login-confgi 元素,Tomcat 创建一LoginConfig 对象并相应的设置它的属性。 验证阀门调用LoginConfig的getRealmName 方法并将域名发送给浏览器显示登录表单。如果getRealmName名字返回值为null,则发送给浏览器服务器的名字和端口名。
Authenticator 类
org.apache.catalina.Authenticator 接口用来表示一个验证器。该方接口并没 有方法,只是一个组件的标志器,这样就能使用instanceof来检查一个组件是 否为验证器。 Catalina 提供了Authenticator 接口的基本实现: org.apache.catalina.authenticator.AuthenticatorBase 类。除了实现 Authenticator 接口外,AuthenticatorBase 还继承了 org.apache.catalina.valves.ValveBase 类。这就是说 AuthenticatorBase 也 是一个阀门。可以在org.apache.catalina.authenticator 包中找到该接口的几 个类:BasicAuthenticator 用于基本验证, FormAuthenticator 用于基于表单的 验证, DigestAuthentication 用于摘要(digest)验证, SSLAuthenticator 用 于SSL验证。NonLoginAuthenticator 用于Tomcat没有指定验证元素的时候。 NonLoginAuthenticator 类表示只是检查安全限制的验证器,但是不进行用户验 证。
一个验证器的主要工作是验证用户。因此,AuthenticatorBase类的invoke方 法调用了抽象方法authenticate,该方法的具体实现由子类完成。在 BasicAuthenticator 中,它 authenticate 使用基本验证器来验证用户。
在部署文件中,只能出现一个login-config元素,login-config元素包括了 auth-method 元素用于定义验证方法。这也就是说一个上下文容器只能有一个 LoginConfig 对象来使用一个authentication 的实现类。
AuthenticatorBase 的子类在上下文中被用作验证阀门,这依赖于部署文件中 auth-method 元素的值。表10.1为auth-method元素的值,可以用于确定验证 器。
第十一章:StandardWrapper
在第五章中已经说过,一共有四种容器:engine(引擎),host(主机),context (上下文)和wrapper(包装器)。在前面的章节里也介绍了如何建立自己的 context 和 wrapper。一个上下文一般包括一个或者多个包装器,每一个包装器 表示一个servlet。本章将会看到Catalina中Wrapper接口的标准实现。首先 介绍了一个HTTP请求会唤醒的一系列方法,接下来介绍了 javax.servlet.SingleThreadModel 接口。最后介绍了StandardWrapper 和 StandardWrapperValve 类。本章的应用程序说明了如何用StandardWrapper实 例来表示servlet。
方法调用序列:
对于每一个连接,连接器都会调用关联容器的invoke方法。接下来容器调用它 的所有子容器的invoke方法。例如,如果一个连接器跟一个StadardContext 实例相关联,那么连接器会调用StandardContext实例的invoke方法,该方法 会调用所有它的子容器的invoke方法。图11.1说明了一个连接器收到一个HTTP 请求的时候会做的一系列事情。
本章关注的是一个servlet被调用的时候发生的细节。因此我们需要自习看 StandardWrapper 和 StandarWrapperValve 类。在学习它们之前,我们需要首先 关注下javax.servlet.SingleThreadModel。理解该接口对于理解一个包装器是 如何工作的是非常重要的。
SingleThreadModel:
一个servlet可以实现javax.servlet.SingleThreadModel 接口,实现此接口的 一个 servlet 通俗称为 SingleThreadModel(STM)的程序组件。根据 Servlet 规范,实现此接口的目的是保证servlet一次只能有一个请求。Servlet 2.4规 范的第SRV.14.2.24节(Servlet 2.3的有SingleThreadModel 接口上的类似说 明)如果一个Servlet实现此接口,将保证不会有两个线程同是使用servlet 的service 方法。 servlet容器可以保证同步进入一个servlet的一个实例,或维持的Servlet 实例池和处理每个新请求。该接口并不能避免同步而产生的问题,如访问静态 类变量或该servlet以外的类或变量。很多程序员并没有仔细阅读它,只是认为实现了SIngleThreadModel就能保证它 们的servlet是线程安全的。单显然并非如此,重新阅读上面的引文。一个servlet实现了SIngelThreadModel之后确实能保证它的service方法不会 被两个线程同时使用。为了提高servlet容器的性能,可以创建STM servlet 的多个实例。该SingleThreadModel 接口在Servlet 2.4 中 已经废弃,因为它使Servlet程序员产生虚假 的安全感,认为它是线程安全的。然而,无论 Servlet 2.3 和 Servlet 2.4 的容器仍然必须 支持此接口
StandardWrapper:
一个StandardWrapper 对象的主要职责是:加载它表示的servlet并分配它的一 个实例。该StandardWrapper不会调用servlet的service方法。这个任务留给 StandardWrapperValve 对象,在 StandardWrapper 实例的基本阀门管道。 StandardWrapperValve 对象通过调用 StandardWrapper 的 allocate 方法获得 Servlet 实例。在获得Servlet实例之后的StandardWrapperValve调用servlet 的service 方法
在servlet 第一次被请求的时候,StandardWrapper加载servlet类。它是动态 的加载servlet,所以需要知道servlet类的完全限定名称。通过 StandardWrapper 类的 setServletClass 方法将 servlet 的类名传递给 StandardWrapper。另外,使用setName方法也可以传递servlet名。
考虑到StandardWrapper 负责在StandardWrapperValve 请求的时候分配一个 servlet 实例,它必须考虑一个servlet是否实现了SingleThreadModel 接口。 如果一个servlet没有实现SingleThreadModel接口,StandardWrapper加载该 servlet 一次,对于以后的请求返回相同的实例即可。StandardWrapper假设 servlet 的 service 方法是现场安全的,所以并没有创建servlet的多个实例。 如果需要的话,由程序员自己解决资源同步问题。
对于一个STM servlet,情况就有所不同了。StandardWrapper必须保证不能同 时有两个线程提交STM servlet的service方法。如果StandardWrapper维持一 个STM servlet 的实例,下面是它如何调用servlet的service方法:
Servlet instance = <get an instance of the servlet>; if ((servlet implementing SingleThreadModel>) { synchronized (instance) { instance.service(request, response); } } else { instance.service(request, response); }
但是,为了性能起见,StandardWrapper维护了一个STM servlet实例池。 一个包装器还负责准备一个javax.servlet.ServletConfig的实例,这可以在 servlet 内部完成,接下来两小节讨论如何分配和加载servlet。
Allocating the Servlet:
在本节开始的时候介绍到StandardWrapperValve的invoke方法调用了包装器的 allocate 方法来获得一个请求servlet的实例。因此StandardWrapper类必须 实现该接口。该方法的签名如下:
public javax.servlet.Servlet allocate() throws ServletException;
注意allocate方法返回的是请求servlet的一个实例。 由于要支持STM servlet,这使得该方法更复杂了一点。实际上,该方法有两部 分组成,一部分负责非STM servlet的工作,另一部分负责STM servlet。第一 部分的结构如下:
if (!singleThreadModel) { // returns a non-STM servlet instance }
布尔变量singleThreadModel 负责标志一个servlet是否是STM servlet。它的 初始值是false,loadServlet方法会检测加载的servlet是否是STM的,如果 是则将它的值该为true。loadServlet方法在下面会介绍到。
该方法的第二部分处理singleThreadModel为true的情况,第二部分的框架如 下: synchronized (instancepool) { // returns an instance of the servlet from the pool }
对于非STM servlet,StandardWrapper 定义一个java.servlet.Servlet 类型的 实例 private Servlet instance = null; 方法allocate检查该实例是否为null,如果是调用loadServlet方法来加载 servlet。然后增加contAllocated整型并返回该实例。
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(" Returninq non-STM instance"); countAllocated++; return (instance); } }
如果StandardWrapper表示的是一个STM servlet,方法allocate尝试返回池 中的一个实例,变量intancePool是一个java.util.Stack类型的STM servlet 实例池。 private Stack instancePool = null;该变量在loadServlet方法内初始化,该部分在接下来的小节进行讨论。 方法allocate负责分配STMservlet实例,前提是实例的数目不超过最大数目, 该数目由maxInstances整型定义,默认值是20.
private int maxInstances = 20;
StandardWrapper提供了nInstances整型变量来定义当前STM 实例的个数。
private int nInstances = 0;
这里是allocate方法的第二部分
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(); }
上面的代码使用一个while循环等待直到nInstances的数目达到countAllocated。在循环里,allocate方法检查 nInstance的值,如果低于maxInstances的值,调用loadServlet方法并将该 实例添加到池中,增加nInstances的值。如果nInstances的值等于或大于 maxInstances的值,它调用实例池堆栈的wait方法,直到一个实例被返回。
Loading the Servlet:
StandardWrapper实现了Wrapper接口的load方法,load方法调用loadServlet 方法来加载一个servlet类,并调用该servlet的init方法,传递一个 javax.servlet.ServletConfig实例。这里是loadServlet是如何工作的。 方法loadServlet首先检查StandardWrapper是否表示一个STM servlet。如果 不是并且该实例不是null(即以前已经加载过),直接返回该实例:
如果该实例是null或者是一个STM servlet,继续该方法的其它部分: