How Servlet Containers Work

编辑注: 在这个系列中, 本文和上一篇, "How Web Servers Work," Tomcat 内部工作原理的指南书籍 How Tomcat Works 的节选. 如果你还没有读过上一篇, 那么赶紧先去读读吧; 那篇文章告诉了你一些有用的背景信息. 在本文里, 你会学习到怎样去创建2servlet 容器. 本书附带的程序可以下载. 如果你有兴趣的话, 在限定的时间内, 作者的网站上允许下载本书的其它部分.

本文讲解了一个简单的 servlet 容器是怎样工作的. 将会给您展示2servlet 容器应用程序; 第一个尽可能简单, 第二个则在第一个基础上做了美化. 我不想把第一个容器做的完美的唯一原因是让它尽可能保持简单. 更多复杂的 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 的时候. 这个方法只会在这个servletservice方法中的所有线程都已经退出来或者超时时间已过之后. servlet container 调用了 destroy之后, 它就不会再调用这个servlet上的 service 方法. destroy 方法给了servlet一个机会去清理该servlet正在把持的任何资源 (例如, 内存, 文件句柄, 和线程) 和确认任何持久性状态信息都已经与内存中这个servlet的当前状态进行了同步.

Listing 2.1 包含PrimitiveServlet的代码, 这是一个非常简单的 servlet,你可以用来测试本文这个 servlet container 程序. PrimitiveServlet 类实现了 javax.servlet.Servlet (所有servlet 都必须实现) 并且为所有5servlet 方法都提供了实现. 它做的事情非常简单: 每次 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 需为每个servletHTTP 请求完成如下工作:

  • 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

因此, 如果你想使用浏览器请求一个叫PrimitiveServletservlet, 在浏览器地址栏中输入如下 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 对象并传给这个servletservice 方法.

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 实例. 它从 ServletRequestgetRequestUri方法获得 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 .

在第二个程序里, 我们加了2facade : 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.

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值