Servlet容器工作原理

来源:互联网 发布:红草药月季淘宝网 编辑:程序博客网 时间:2024/05/01 16:58
本文介绍一个简单 servlet 容器的基本原理。现有两个servlet容器,第一个很简单,第二个则是根据第一个写出。为了使第一个容器尽量简单,所以没有做得很完整。复杂一些的servlet容器(包括TOMCAT4和5)在TOMCAT运行内幕的其他章节有介绍。


两个servlet容器都处理简单的servlet及staticResource。您可以使用 webroot/ 目录下的 PrimitiveServlet 来测试它。复杂一些的 servlet会超出这些容器的容量,您可以从 TOMCAT 运行内幕 一书学习创建复杂的 servlet 容器。


两个应用程序的类都封装在ex02.pyrmont 包下。在理解应用程序如何运作之前,您必须熟悉 javax.servlet.Servlet 接口。首先就来介绍这个接口。随后,就介绍servlet容器服务servlet的具体内容。


javax.servlet.Servlet 接口


servlet 编程,需要引用以下两个类和接口:javax.servlet 和 javax.servlet.http,在这些类和接口中,javax.servlet.Servlet接口尤为重要。所有的 servlet 必须实现这个接口或继承已实现这个接口的类。


Servlet 接口有五个方法,如下


<ccid_nobr>

<ccid_code>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()</ccid_code>


</ccid_nobr>


init、service和 destroy 方法是 Servlet 生命周期的方法。当 Servlet 类实例化后,容器加载 init,以通知 servlet 它已进入服务行列。init 方法必须被加载,Servelt 才能接收和请求。如果要载入数据库驱动程序、初始化一些值等等,程序员可以重写这个方法。在其他情况下,这个方法一般为空。


service 方法由 Servlet 容器调用,以允许 Servlet 响应一个请求。Servlet 容器传递 javax.servlet.ServletRequest 对象和 javax.servlet.ServletResponse 对象。ServletRequest 对象包含客户端 HTTP 请求信息,ServletResponse 则封装servlet 响应。这两个对象,您可以写一些需要 servlet 怎样服务和客户怎样请求的代码。


从service中删除Servlet实例之前,容器调用destroy方法。在servlet容器关闭或servlet容器需要更多的内存时,就调用它。这个方法只有在servlet的service方法内的所有线程都退出的时候,或在超时的时候才会被调用。在 servlet 容器调用 destroy方法之后,它将不再调用servlet的service方法。destroy 方法给了 servlet 机会,来清除所有候住的资源(比如:内存,文件处理和线程),以确保在内存中所有的持续状态和 servlet的当前状态是同步的。Listing 2.1 包含了PrimitiveServlet 的代码,此servlet非常简单,您 可以用它来测试本文中的servlet容器应用程序。


PrimitiveServlet 类实现了javax.servlet.Servlet 并提供了五个servlet方法的接口 。它做的事情也很简单:每次调用 init,service 或 destroy方法的时候,servlet就向控制口写入方法名。service 方法也从ServletResponsec对象中获得java.io.PrintWriter 对象,并发送字符串到浏览器。


<ccid_nobr>

<ccid_code>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(&quot;init&quot;);
}
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException
{
System.out.println(&quot;from service&quot;);
PrintWriter out = response.getWriter();
out.println(&quot;Hello.Roses are red.&quot;);
out.print(&quot;Violets are blue.&quot;);
}
public void destroy()
{
System.out.println(&quot;destroy&quot;);
}
public String getServletInfo()
{
return null;
}

public ServletConfig getServletConfig()
{
return null;
}
}</ccid_code>


</ccid_nobr>


Application 1


现在,我们从 servlet容器的角度来看看 servlet 编程。一个功能健全的 servlet容器对于每个 servlet 的HTTP请求会完成以下事情:


当servlet 第一次被调用的时候,加载了 servlet类并调用它的init方法(仅调用一次)


响应每次请求的时候 ,构建一个javax.servlet.ServletRequest 和 javax.servlet.ServletResponse实例。


激活servlet的service方法,传递 ServletRequest 和 ServletResponse 对象。


当servlet类关闭的时候,调用servlet的destroy方法,并卸载servlet类。


发生在 servlet 容器内部的事就复杂多了。只是这个简单的servlet容器的功能不很健全,所以,这它只能运行非常简单的servelt ,并不能调用servlet的init和destroy方法。然而,它也执行了以下动作:


等待HTTP请求。


构建ServletRequest和ServletResponse对象


如果请求的是一个staticResource,就会激活StaticResourceProcessor实例的 process方法,传递ServletRequest 和 ServletResponse 对象。


如果请求的是一个servlet ,载入该类,并激活它的service方法,传递ServletRequest和ServletResponse 对象。注意:在这个servlet 容器,每当 servlet被请求的时候该类就被载入。


在第一个应用程序中,servlet容器由六个类组成 。


HttpServer1


Request


Response


StaticResourceProcessor


ServletProcessor1


Constants





证如前文中的应用程序一样,这个程序的进入口(静态 main 方法)是HttpServer 类。这个方法创建了HttpServer实例,并调用它的await方法。这个方法等待 HTTP 请示,然后创建一个 request 对象和 response对象,根据请求是否是staticResource还是 servlet 来分派它们到 StaticResourceProcessor实例或ServletProcessor实例。


Constants 类包含 static find WEB_ROOT,它是从其他类引用的。 WEB_ROOT 指明 PrimitiveServlet 位置 和容器服务的staticResource。


HttpServer1 实例等待 HTTP 请求,直到它收到一个 shutdown 命令。发布 shutdown命令和前文是一样的。 

 

此应用程序内的 HttpServer1类 与前文简单的 WEB 服务器应用程序中的HttpServer 十分相似。但是,此应用程序内的 HttpServer1 能服务静态资源和 servlet。如果要请求一个静态资源,请输入以下 URL:


http://machineName:port/staticResource


它就是前文中提到的怎样在 WEB 服务器应用程序里请求静态资源。如果要请求一个 servlet,请输入以下 URL:


http://machineName:port/servlet/servletClass


如果您想在本地浏览器请求一个 PrimitiveServle servlet ,请输入以下 URL:


http://localhost:8080/servlet/PrimitiveServlet


下面 Listing 2.2 类的 await 方法,是等待一个 HTTP 请求,直到一个发布 shutdown 命令。与前文的 await 方法相似。


<ccid_nobr>


<ccid_code>Listing 2.2. HttpServer1 类的 await 方法
public void await() {
ServerSocket serverSocket = null;
int port = 8080;

try {
serverSocket = new ServerSocket(port, 1,
InetAddress.getByName(&quot;127.0.0.1&quot;));
}
catch (IOException e) {
e.printStackTrace();
System.exit(1);
}

// 循环,等待一个请求
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;

try {
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();

// 创建请求对象并解析
Request request = new Request(input);
request.parse();

// 创建回应对象
Response response = new Response(output);
response.setRequest(request);

//检测是否是 servlet 或静态资源的请求
//servlet 请求以 &quot;/servlet/&quot; 开始
if (request.getUri().startsWith(&quot;/servlet/&quot;)) {
ServletProcessor1 processor = new ServletProcessor1();
processor.process(request, response);
}
else {
StaticResourceProcessor processor =
new StaticResourceProcessor();
processor.process(request, response);
}

// 关闭socket
socket.close();

//检测是否前面的 URI 是一个 shutdown 命令
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}
catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}</ccid_code>


</ccid_nobr>


此文 await 方法和前文的不同点就是,此文的 await 方法中的请求调度到StaticResourceProcessor 或 ervletProcessor 。


如果 URI中包含 "/servlet/.",请求推进到后面,否则,请求传递到 StaticResourceProcessor 实例


Request 类


Servlet service 方法接受 servlet 容器的 javax.servlet.ServletRequest 和javax.servlet.ServletResponse 实例。因此,容器必须构建 ServletRequest和ServletResponse对象,然后将其传递到正在被服务的service 方法。


ex02.pyrmont.Request 类代表一个请求对象传递到 service 方法。同样地,它必须实现 javax.servlet.ServletRequest 接口。这个类必须提供接口内所有方法的实现。这里尽量简化它并只实现几个方法。要编译 Request 类的话,必须提供这些方法的空实现。再来看看 request 类,内部所有需要返回一个对象实例都返回null,如下:


<ccid_nobr>


<ccid_code>public Object getAttribute(String attribute) {
return null;
}

public Enumeration getAttributeNames() {
return null;
}

public String getRealPath(String path) {
return null;
}</ccid_code>


</ccid_nobr>


另外,request 类仍需有前文有介绍的 parse 和getUri 方法。


Response 类


response 类实现 javax.servlet.ServletResponse,同样,该类也必须提供接口内所有方法的实现。类似于 Request 类,除 getWriter 方法外,其他方法的实现都为空。


<ccid_nobr>


<ccid_code>public PrintWriter getWriter() {
// autoflush is true, println() will flush,
// but print() will not.
writer = new PrintWriter(output, true);
return writer;

}</ccid_code>


</ccid_nobr>


PrintWriter 类构建器的第二个参数是一个代表是否启用 autoflush 布尔值 ,如果为真,所有调用println 方法都 flush 输出。而 print 调用则不 flush 输出。因此,如果在servelt 的service 方法的最后一行调用 print方法,则从浏览器上看不到此输出 。这个不完整性在后面的应用程序内会有调整。


response 类也包含有前文中介绍的 sendStaticResource方法。


StaticResourceProcessor 类


StaticResourceProcessor 类用于服务静态资源的请求。它唯一的方法是 process。


<ccid_nobr>


<ccid_code>Listing 2.3.StaticResourceProcessor 类的 process方法。
public void process(Request request, Response response) {
try {
response.sendStaticResource();
}
catch (IOException e) {
e.printStackTrace();
}
}</ccid_code>


</ccid_nobr>


process 方法接受两个参数:Request 和 Response 实例。它仅仅是调用 response 类的 sendStaticResource 方法。 

ServletProcessor1 类


ServletProcessor1 类用来处理对 servlet 的 HTTP 请求。 它非常简单,只包含了一个 process 方法。 而这个方法接受两个参数: 一个javax.servlet.ServletRequest 实例和一个 avax.servlet.ServletResponse实例。 process 方法也构建了一个 java.net.URLClassLoader 对象并使用它装载 servlet 类文件。 在从类装载器获得的 Class 对象上,process 方法创建一个 servlet 实例并调用它的 service 方法。


process 方法


Listing 2.4. ServletProcessor1 类中 process 方法


<ccid_nobr>

<ccid_code>public void process(Request request, Response response) {
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf(&quot;/&quot;) + 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(&quot;file&quot;, 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 (Exception 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());
}
}</ccid_code>


</ccid_nobr>


process方法接受两个参数:一个 ServletRequest实例和一个 ServletResponse 实例。process方法通过调用 getRequestUri 方法从 ServletRequest获取 URI。


String uri = request.getUri();切记 URI 的格式:


/servlet/servletName


servletName是servlet类的名称。


如果要装载 servlet 类,则需要使用以下代码从 URI 获知 servlet 名称:String servletName = uri.substring(uri.lastIndexOf("/") + 1);然后 process 方法装载 servlet。 要做到这些,需要创建一个类装载器,并告诉装载器该类的位置, 该 servlet 容器可以指引类装载器在 Constants.WEB_ROOT 指向的目录中查找。 在工作目录下,WEB_ROOT 指向 webroot/ 目录。


如果要装载一个 servlet,则要使用 java.net.URLClassLoader 类,它是java.lang.ClassLoader 的间接子类。 一旦有了 URLClassLoader 类的实例,就可以使用 loadClass 方法来装载一个 servlet 类。 实例化 URLClassLoader 是很简单的。 该类有三个构建器,最简单的是:


public URLClassLoader(URL[] urls);


urls 是一组指向其位置 java.net.URL 对象, 当装载一个类时它会自动搜索其位置。任一以 / 结尾的 URL 都被假定为一目录, 否则,就假定其为 .jar 文件,在需要时可以下载并打开。


在一个 servlet 容器内,类装载器查找 servlet 类的位置称为储存库 (repository)。在所举的应用程序中,类装载器只可在当前工作目录下的 webroot/ 目录查找,所以,首先得创建一组简单的 URL。 URL 类提供了多个构建器,因此有许多的方法来构建一个URL 对象。 在这个应用程序内,使用了和 TOMCAT 内另外一个类所使用的相同的构建器。 该构建器头部 (signature) 如下:


public URL(URL context, String spec, URLStreamHandler hander)


throws MalformedURLException


可以通过传递给第二个参数一个规范,传递给第一个和第三个参数 null 值来使用这个构建器, 但在些有另外一种可接受三个参数的构建器:


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 。 以下代码可创建:


String repository = (new URL("file", null,


classPath.getCanonicalPath() + File.separator)).toString();


结合起来,以下是构建正确 URLClassLoader 实例的 process 方法的部分代码


<ccid_nobr>

<ccid_code>// create a URLClassLoader
URLStreamHandler streamHandler = null;
URL[] urls = new URL[1];
File classPath = new File(Constants.WEB_ROOT);
String repository = (new URL(&quot;file&quot;, null,
classPath.getCanonicalPath() + File.separator)).toString()
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);</ccid_code>


</ccid_nobr>


创建储存库 (repository)的代码摘自org.apache.catalina.startup.ClassLoaderFactory内的createClassLoader 方法,而创建 URL 的代码摘自org.apache.catalina.loader.StandardClassLoader 类内的 addRepository 方法。 但在此阶段您还没有必要去关心这些类。


有了类装载器,您可以使用loadClass方法装载servlet类:


<ccid_nobr>

<ccid_code>Class myClass = null;
try {
myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
System.out.println(e.toString());
}</ccid_code>


</ccid_nobr>


然后,process方法创建已装载的 servlet类的实例,传递给 javax.servlet.Servlet ,并激活 servlet 的 service 方法:


<ccid_nobr>

<ccid_code>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());
}</ccid_code>


</ccid_nobr>


编译并运行该应用程序


如果要编译该应用程序,在工作目录下键入以下命令:


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


或者是:


http://localhost:8080/servlet/PrimitiveServlet


您将会在浏览器中看到以下文本:


Hello. Roses are red.