编辑注: 在这个系列中, 本文和上一篇, "How Web Servers Work," 是Tomcat 内部工作原理的指南书籍 How Tomcat Works 的节选. 如果你还没有读过上一篇, 那么赶紧先去读读吧; 那篇文章告诉了你一些有用的背景信息. 在本文里, 你会学习到怎样去创建2个 servlet 容器. 本书附带的程序可以下载. 如果你有兴趣的话, 在限定的时间内, 作者的网站上允许下载本书的其它部分.
本文讲解了一个简单的 servlet 容器是怎样工作的. 将会给您展示2个 servlet 容器应用程序; 第一个尽可能简单, 第二个则在第一个基础上做了美化. 我不想把第一个容器做的完美的唯一原因是让它尽可能保持简单. 更多复杂的 servlet 容器, 包括 Tomcat 4 和 5, 则在 How Tomcat Works 的其它章节讨论.
servlet container 既能处理简单的 servlet, 也能处理静态资源. 你可以使用 PrimitiveServlet
(位于
webroot/ 目录下)测试这个容器. 更复杂的servlet已经超出了这个容器的能力, 但你可以从 How Tomcat Works 这本书中学习到怎样建立更完善的 servlet container.
这些程序的类都是 ex02.pyrmont
包中的一部分. 要想了解程序是如何工作的, 你必须要熟悉 javax.servlet.Servlet
接口. 为了重新温习一下这方面的知识, 本文第一部分将讨论这个接口. 然后, 你会学习到究竟一个 servlet container 需要做些什么工作才能处理 servlet.
The javax.servlet.Servlet
Interface
Servlet 编程需要用到2个包中的类和接口: javax.servlet
和 javax.servlet.http
. 在这些类和接口中, javax.servlet.Servlet
接口是最重要的接口. 所有servlet 都必须实现这个接口或者继承一个实现了这个接口的类.
Servlet
接口有5个方法, 定义如下:
-
public void init(ServletConfig config) throws ServletException
-
public void service(ServletRequest request, ServletResponse response)
throws ServletException, java.io.IOException -
public void destroy()
-
public ServletConfig getServletConfig()
-
public java.lang.String getServletInfo()
init
, service
, 和 destroy
方法是 servlet 的生命周期方法. init
方法只在servlet类被初始化后被容器调用一次,指出这个 servlet 已经处于可提供服务的状态. 在servlet能够接收任何请求之前,init
方法的调用必须完全成功. Servlet 程序员可以覆盖这个方法,实现一些需要只运行一次的初始化代码, 像装载 database driver, 初始化值, 等. 其它情况下, 这个方法通常是空白的.
service
方法会被容器调用让这个servlet响应一个请求. servlet container 传递一个 javax.servlet.ServletRequest
对象和一个 javax.servlet.ServletResponse
对象. ServletRequest
对象包含客户端 HTTP 请求信息,ServletResponse
封装了 servlet 的响应. 这2个对象让你能够写一些自己的代码决定这个servlet怎样为这个客户端请求提供服务.
servlet container 在移除一个servlet实例之前会调用 destroy
方法. 这通常发生在当 servlet container 要关闭或者当 servlet container 需要更多 free memory 的时候. 这个方法只会在这个servlet的service
方法中的所有线程都已经退出来或者超时时间已过之后
. 在 servlet container 调用了 destroy
之后
, 它就不会再调用这个servlet上的 service
方法. destroy
方法给了servlet一个机会去清理该servlet正在把持的任何资源 (例如, 内存, 文件句柄, 和线程) 和确认任何持久性状态信息都已经与内存中这个servlet的当前状态进行了同步.
Listing 2.1 包含
了
PrimitiveServlet
的代码
, 这是一个非常简单的 servlet,你可以用来测试本文这个 servlet container 程序. PrimitiveServlet
类实现了 javax.servlet.Servlet
(所有servlet 都必须实现) 并且为所有5个servlet 方法都提供了实现. 它做的事情非常简单: 每次 init
, service
, 或者 destroy
方法中的任意一个被调用, servlet 就向控制台打印方法名. service
方法中的代码也会从ServletResponse
对象
获得一个 java.io.PrintWriter
对象向浏览器发送字符串.
Listing 2.1. PrimitiveServlet.java
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
public class PrimitiveServlet implements Servlet {
public void init(ServletConfig config) throws ServletException {
System.out.println("init");
}
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
System.out.println("from service");
PrintWriter out = response.getWriter();
out.println("Hello. Roses are red.");
out.print("Violets are blue.");
}
public void destroy() {
System.out.println("destroy");
}
public String getServletInfo() {
return null;
}
public ServletConfig getServletConfig() {
return null;
}
}
Application 1
现在, 让我们从servlet container 的视角来看 servlet 编程. 简而言之, 一个功能完备的 servlet container 需为每个servlet的HTTP 请求完成如下工作:
-
当 servlet 当 servlet 第一次被调用的时候, 加载这个 servlet 类并调用它的
init
方法 (只调用一次). -
对于每个请求, 构造一个
javax.servlet.ServletRequest
实例和javax.servlet.ServletResponse
实例
. -
调用 servlet 的
service
方法, 传给它ServletRequest
和ServletResponse
对象. -
当 servlet 类被关掉的时候, 调用 servlet 的
destroy
方法并卸载 servlet 类.
在一个真正的 servlet container 里需要做的比这些复杂的多. 不过, 这个简单的 servlet container 功能并不完备. 因此, 它只能运行非常简单的 servlet,而且也不调用 servlet 的 init
和 destroy
方法. 取而代之, 它做了如下工作:
-
等待 HTTP 请求.
-
构造一个
ServletRequest
对象和一个ServletResponse
对象. -
如果请求的是静态资源, 调用
StaticResourceProcessor
实例的
process
方法, 并传给它ServletRequest
和ServletResponse
对象. -
如果请求的是一个 servlet, 加载这个 servlet 类,调用它的
service
方法, 并传递ServletRequest
和ServletResponse
对象给它. 注意,在这个 servlet container 中, 请求的servlet 类会每次都加载.
在第一个程序里, servlet container 由6个类组成:
-
HttpServer1
-
Request
-
Response
-
StaticResourceProcessor
-
ServletProcessor1
-
Constants
就像上一篇文章中的程序那样, 这个程序的入口点 (the static main
method) 在 HttpServer
类中. 这个方法创建了 HttpServer
的一个实例并调用它的 await
方法. 就像名字所隐含的, 这个方法会等待 HTTP 请求, 创建一个 Request
对象和一个 Response
对象, 然后分发给一个 StaticResourceProcessor
实例或者一个 ServletProcessor
实例, 这依赖于请求的是静态资源还是一个servlet.
Constants
类包含 static final WEB_ROOT
这个被其它类引用的变量. WEB_ROOT
指示了 PrimitiveServlet
的位置和这个容器所能提供的静态资源.
HttpServer1
实例一直等待 HTTP 请求知道它接收到关闭命令. 发出关闭命令与上一篇文章里所做的一样.
这个程序里的每个类都会在下面的章节里讨论到.
The HttpServer1
Class
这个程序的 HttpServer1
类与上一篇文章中的那个简单的 web server 程序的 HttpServer
类很相似
. 不过, 在这个程序里, HttpServer1
能够同时支持静态资源和 servlet. 如果要请求静态资源, 使用下面形式的URL:
http://machineName:port/staticResource
这正是上一篇文章中,你如何请求web server程序的一个静态资源. 要想请求一个 servlet, 你需使用下面的 URL:
http://machineName:port/servlet/servletClass
因此, 如果你想使用浏览器请求一个叫PrimitiveServlet
的
servlet
, 在浏览器地址栏中输入如下 URL:
http://localhost:8080/servlet/PrimitiveServlet
Listing 2.2 中给出的类的 await
方法, 等待 HTTP 请求,直到发出关闭命令为止. 这跟上一篇文章讨论到的 await
方法很类似.
Listing 2.2. The HttpServer1
class' await
method
public void await() {
ServerSocket serverSocket = null;
int port = 8080;
try {
serverSocket = new ServerSocket(port, 1,
InetAddress.getByName("127.0.0.1"));
}
catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
// Loop waiting for a request
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
socket = serverSocket.accept();
input = socket.getInputStream();
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);
}
catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
Listing 2.2 中的await
方法与前一篇文章的
await
Listing 2.2中, request 可以分发至 方法的区别是,
StaticResourceProcessor
或者 ServletProcessor
. 如果URI 包含字符串"/servlet/
",request 会被 forward 到后者. 否则, request 会被传给 StaticResourceProcessor
实例.
The Request
Class
Servlet 的 service
方法从 servlet container 接收一个 javax.servlet.ServletRequest
实例和一个 javax.servlet.ServletResponse
实例. container 因而必须要构造一个 ServletRequest
对象和一个 ServletResponse
对象并传给这个servlet的 service
方法.
ex02.pyrmont.Request
类代表要传递给 service
方法的 request 对象. 同样的, 它必须实现 javax.servlet.ServletRequest
接口. 这个类必须提供接口中所声明的所有方法的实现. 但是, 我们想使它简单一些,所以只实现了部分方法. 要想成功编译这个 Request
类, 你需要给其它一些方法空的实现. 如果你看了 Request
类, 你会看到所有方法声明中返回对象实例的方法都返回的是一个 null
, 如下:
...
public Object getAttribute(String attribute) {
return null;
}
public Enumeration getAttributeNames() {
return null;
}
public String getRealPath(String path) {
return null;
}
...
另外, Request
类也有 parse
和 getUri
方法, 这些上篇文章讨论过.
The Response
Class
Response
类实现了 javax.servlet.ServletResponse
. 同样地, 该类必须提供接口中的所有方法的实现. 类似于 Request
类, 我把所有方法都实现为空除了 getWriter
方法.
public PrintWriter getWriter() {
// autoflush is true, println() will flush,
// but print() will not.
writer = new PrintWriter(output, true);
return writer;
}
传给 PrintWriter
类构造器的第二个参数是一个 Boolean,指示是否要启用 autoflush
. 传递 true 作为第二个参数将使每次对 println
方法的调用都flush输出. 但是, print
调用不会 flush output. 因此, 如果在servlet 的 service
方法中的最后一行代码调用 print
方法, 那么输出不会发送至浏览器. 这个瑕疵将在后面的程序中得到修复.
Response
类也有一个我们在上一篇文章里讨论到的 sendStaticResource
方法.
The StaticResourceProcessor
Class
StaticResourceProcessor
类用来处理请求静态资源的请求. 它唯一的方法是 process
, 如 Listing 2.3 所示.
Listing 2.3. The StaticResourceProcessor
class' process
method
public void process(Request request, Response response) {
try {
response.sendStaticResource();
}
catch (IOException e) {
e.printStackTrace();
}
}
process
方法接收2个参数: 一个 Request
实例和一个 Response
实例. 它只简单的调用了一下 Response
类的 sendStaticResource
方法
.
The ServletProcessor1
Class
ServletProcessor1
类处理 HTTP 请求. 它令人惊讶的简单, 只包含 process
方法. 这个方法接收2个参数: 一个 javax.servlet.ServletRequest
实例和一个 javax.servlet.ServletResponse
实例
. process
方法也构造了一个 java.net.URLClassLoader
对象并用它来加载 servlet 类文件. 从 class loader 得到了 Class
对象后, process
方法创建了这个 servlet 的一个实例并调用 service
方法.
Listing 2.4 给出了 process
方法.
Listing 2.4. The ServletProcessor1
class' process
method
public void process(Request request, Response response) {
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
URLClassLoader loader = null;
try {
// create a URLClassLoader
URLStreamHandler streamHandler = null;
URL[] urls = new URL[1];
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);
}
catch (IOException e) {
System.out.println(e.toString() );
}
Class myClass = null;
try {
myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
System.out.println(e.toString());
}
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
}
}
process
方法接收2个参数: 一个 ServletRequest
实例和一个 ServletResponse
实例
. 它从 ServletRequest
的
getRequestUri
方法获得
URI:
String uri = request.getUri();
记住,URI 是如下格式的:
/servlet/servletName
servletName 是 servlet 类的名字.
要想加载 servlet 类, 我们必须要从 URI 中知道 servlet 的名字, 我们是通过 process
方法中的下面这行代码获得的:
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
接着, process
方法加载这个 servlet. 为了加载这个 servlet, 你必须要创建一个 class loader 并告诉这个 class loader 这个类的位置. 这个 servlet container 指示 class loader 在 Constants.WEB_ROOT
指向的路径下查找
. Constants.WEB_ROOT
指向工作路径下的 webroot/ 目录.
要加载一个 servlet, 请使用 java.net.URLClassLoader
类, 这是java.lang.ClassLoader
的间接子类
. 一旦你有了 URLClassLoader
类的实例, 就可以使用它的 loadClass
方法加载一个 servlet 类. 初始化 URLClassLoader
类非常简单. 这个类有3种构造器, 最简单的如下:
public URLClassLoader(URL[] urls);
urls
是 java.net.URL
对象的数组, java.net.URL
对象指向当加载一个类时所要搜寻的位置. 任何以 /
结尾的 URL 都假定是一个目录. 否则, URL 假定应用的是一个 .jar 文件, 必要的时候, 会下载并打开这个 .jar 文件.
在 servlet container 里, 一个 class loader 能够查找到 servlet 类的地方叫做 repository.
在我们的程序里, 只有一处 class loader 必须查看 — 位于工作路径下的 webroot/ 目录. 因此, 我们先创建了一个包含单个 URL 的数组. URL
类提供了好几种构造器, 所以有很多方式去构造一个 URL 对象. 对于本程序, 我使用了与 Tomcat 中的另一个类所使用的相同的构造器. 该构造器有如下名称:
public URL(URL context, String spec, URLStreamHandler hander)
throws MalformedURLException
你可以这样使用这个构造器: 传递一个 specification 作为第二个参数, null
作为它的第一个和第三个参数
. 但是, 还有另一种构造器, 它也接收3个参数:
public URL(String protocol, String host, String file)
throws MalformedURLException
因此, 如果你像下面这样写, 编译器将不知道你要用的是哪个构造器:
new URL(null, aString, null);
你可以这样解决: 告诉编译器第三个参数的类型:
URLStreamHandler streamHandler = null;
new URL(null, aString, streamHandler);
对于第二个参数, 传递一个包含 repository 的 String
(the directory where servlet classes can be found). 使用下面的代码创建:
String repository = (new URL("file", null,
classPath.getCanonicalPath() + File.separator)).toString();
把所有部分综合起来, 下面就是 process
方法如何构造了正确的 URLClassLoader
实例:
// create a URLClassLoader
URLStreamHandler streamHandler = null;
URL[] urls = new URL[1];
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);
生成 repository 的代码来自 org.apache.catalina.startup.ClassLoaderFactory
类的
createClassLoader
方法, 生成 URL 的代码取自 org.apache.catalina.loader.StandardClassLoader
类的 addRepository
方法. 但, 在这里你不必担心这些类.
有了 class loader, 你就能使用 loadClass
方法装载 servlet 类:
Class myClass = null;
try {
myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
System.out.println(e.toString());
}
接着, process
方法为装载进来的 servlet 类创建一个实例, 向下造型为 javax.servlet.Servlet
, 并调用 servlet 的 service
方法:
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
}
Compiling and Running the Application
要编译程序, 在工作路径敲入如下命令:
javac -d . -classpath ./lib/servlet.jar src/ex02/pyrmont/*.java
如果在 Windows 上运行程序, 需在工作路径下敲入如下命令:
java -classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer1
在 Linux 上, 各包间使用冒号进行分隔.
java -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer1
要测试程序, 在浏览器中敲入如下URL:
http://localhost:8080/index.html
or
http://localhost:8080/servlet/PrimitiveServlet
在你的浏览器中会看到如下文本:
Hello. Roses are red.
注意你看不到第二个字符串 (Violets are blue
) 因为只有第一个字符串才 flush 到浏览器. How Tomcat Works 这本书的后面章节所附带的程序会给你演示如何解决这个问题.
Application 2
在第一个程序里有一个严重的问题. 在 ServletProcessor1
类的 process
方法, 我们向上类型转换了 ex02.pyrmont.Request
to javax.servlet.ServletRequest
的实例
, 把它作为第一个参数传递给了 servlet 的 service
方法. 我们也向上造型了 ex02.pyrmont.Response
to javax.servlet.ServletResponse
的实例并把它作为第二个参数传递给了 servlet 的 service
方法.
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
}
这危及 security. 知道我们的 servlet container 内部工作机制的 Servlet 程序员能够向下造型 ServletRequest
和 ServletResponse
实例到 Request
和 Response
并调用它们的 public 方法. 有了 Request
实例, 他们就可以调用它的 parse
方法. 有了 Response
实例, 他们就能够调用它的 sendStaticResource
方法.
你不能把 parse
和 sendStaticResource
方法设置为 private, 因为在 ex02.pyrmont
包里的其它类会调用. 但是, 我们不打算让这两个方法在一个 servlet 内部可以使用. 一种解决方案是给 Request
和 Response
类设置一个默认的访问修饰符, 这样它们就不能在 ex02.pyrmont
包的外部使用. 但是, 有一个更优雅的解决方案: 使用 facade 类.
在第二个程序里, 我们加了2个 facade 类: RequestFacade
和 ResponseFacade
. RequestFacade
类实现了 ServletRequest
接口, 传递一个 Request
实例进行初始化, 这个 Request 实例被指派给了它的构造器中的 ServletRequest 对象引用. ServletRequest
接口中的每个方法实现都调用 Request
对象的相应方法, 但 ServletRequest
对象自己则是 private 并且在类外不能被访问到. 与将 Request
对象向上造型为 ServletRequest
并传递给 service
方法相反, 我们构造了一个 RequestFacade
对象并传递给 service
方法. servlet programmer 将 ServletRequest
实例向下造型回 RequestFacade
; 可是, 他不能只访问 ServletRequest
接口中的可用方法. 现在, parseUri
方法已经安全了.
Listing 2.5 展示了不完整的 RequestFacade
类.
Listing 2.5. The RequestFacade
class
package ex02.pyrmont;
public class RequestFacade implements ServletRequest {
private ServletRequest request = null;
public RequestFacade(Request request) {
this.request = request;
}
/* implementation of the ServletRequest*/
public Object getAttribute(String attribute) {
return request.getAttribute(attribute);
}
public Enumeration getAttributeNames() {
return request.getAttributeNames();
}
...
}
注意 RequestFacade
的构造器
. 它接收一个 Request
对象但马上指派给了私有的 servletRequest
对象引用. 也要注意 RequestFacade
类里的每个方法都调用了 ServletRequest
对象中的相应方法.
在 ResponseFacade
类中也是如此.
这些是在 Application 2 中使用到的类:
-
HttpServer2
-
Request
-
Response
-
StaticResourceProcessor
-
ServletProcessor2
-
Constants
HttpServer2
类类似于 HttpServer1
, 除了在 await
方法中它使用的是 ServletProcessor2
, 而不是 ServletProcessor1
:
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor2 processor = new ServletProcessor2();
processor.process(request, response);
}
else {
...
}
ServletProcessor2
类类似于 ServletProcessor1
, 除了 process
方法中的如下这部分代码:
Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) requestFacade,
(ServletResponse) responseFacade);
}
Compiling and Running the Application
在工作目录敲入如下命令编译程序.
javac -d . -classpath ./lib/servlet.jar src/ex02/pyrmont/*.java
如果在 Windows 运行这个程序, type the following command from the working directory:
java -classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer2
在 Linux 上, 各包之间使用分号分隔.
java -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer2
你可以使用与 Application1 一样的 URLs 接收到同样的结果.
Summary
本文讨论了一个简单的 Servlet 容器, 可以用来提供静态资源服务, 也能处理像 PrimitiveServlet
这样简单的
servlet
. 它也提供了javax.servlet.Servlet
接口的背景信息.
Budi Kurniawan is a senior J2EE architect and author.