2.2 应用程序 1
下面从servlet容器的角度审视servlet程序的开发。简单来说,对一个Servlet的每个HTTP请求,一个功能齐全的servlet容器有以下几件事要做:
当第一次调用某个servlet时,要载入该servlet类,并调用其init()方法(仅此一次);
针对每个request请求,创建一个javax.servlet.ServletRequest实例和一个javax.servlet.ServletResponse实例;
调用该servlet的service()方法,将servletRequest对象和servletResponse对象作为参数传入;
当关闭该servlet类时,调用其destroy()方法,并卸载该servlet类。
本章所要建立的servlet容器是一个很小的容器,没有实现所有的功能。因此,它只能运行非常简单的servlet,而且也会不调用servlet的init()和destroy()方法。它会做以下几件事:
等待HTTP请求;
创建一个servletRequest对象和一个servletResponse对象;
若请求的是一个静态资源,则调用StaticResourceProcessor对象的process()方法,传入servletRequest对象和servletResponse对象;
若请求的是servlet,则载入相应的servlet类,调用其service()方法,传入servletRequest对象和servletResponse对象。
注意 在该servlet容器中,每次请求servlet都会载入相应的servlet类。
本节的应用程序包括6个类:
HttpServer1
Request
Response
StaticResourceProcessor
ServletProcessor1
Constants
图2-1展示了本节中的servlet容器的UML类图。
该应用程序的入口点(静态main()方法)在类HttpServer1中。main()方法创建HttpServer1的一个实例,然后调用其await()方法。await()方法会等待HTTP请求,为接收到的每个请求创建一个Request和一个Response对象,并根据该HTTP请求的是静态资源或是servlet,将该HTTP请求分发给一个StaticResourceProcessor实例或一个ServletProcessor实例。
Constants类中定义了静态final WEB_ROOT,供其他的类引用。WEB_ROOT指定了该servlet容器中使用的PrimitiveServlet类和静态资源的位置。
HttpServer1类的实例会一直等待HTTP请求,直到接收到一条关闭命令。可以使用第1章介绍的方法来发布关闭命令。
该应用程序中的各个类会在接下来的几节中逐个说明。
2.2.1 HttpServer1类
应用程序1中的HttpServer1类与第1章中简单Web服务器应用程序中的HttpServer类似。但是,该应用程序中的HttpServer1类既可以对静态资源请求,也可以对于servlet资源请求。若要请求一个静态资源,可以在浏览器的地址栏或URL框中输入如下格式的URL:
http://machineName:port/staticResource
这与第1章的Web服务器应用程序中对静态资源的请求相同。
若要请求servlet资源,可以使用如下格式的URL:
http://machineName:port/servlet/servletClass
因此,若要请求本地浏览器上的名为PrimitiveServlet的servlet,可以在浏览器的地址栏或URL框中输入如下的URL:
http://localhost:8080/servlet/Primitiveservlet
应用程序1中的servlet容器会处理对PrimitiveServlet的请求。但是,若要调用其他的servlet(如ModernServlet),则servlet容器抛出异常。在后面的章节中,你将学会如何构建可以兼具两种功能的servlet容器。
HttpServer1类的定义在代码清单2-2中。
该类的await()方法会一直等待HTTP请求,直到接收到一条关闭命令,这点与第1章中的await()方法类似。区别在于,本章中的await()方法可以将HTTP请求分发给StaticResourceProcessor对象或ServletProcessor对象来处理。当URI包含字符串“/servlet/”时,会把请求转发给servletProcessor对象处理。否则的话,把HTTP请求传递给StaticResourceProcessor对象处理。注意代码清单2-2中灰色的部分。
2.2.2 Request类
servlet的service方法会从servlet容器中接收一个javax.servlet.ServletRequest实例一个和javax.servlet.ServletResponse实例。即,对每个HTTP请求来说,servlet容器必须创建一个ServletRequest对象和一个ServletResponse对象,并将它们作为参数传给它服务的servlet的service()方法。
ex02.pyrmont.Request类表示被传递给servlet的service()方法的一个request对象。它必须实现javax.Servlet.servletRequest接口中声明的所有方法。但为了简单起见,这里只给出了部分方法的实现,其余方法的实现会在后面的章节给出。为了能够编译Request类,需要将未实现的方法留空。代码清单2-3中给出了Request类的定义,其签名返回obejct实例的所有方法都会返回null。
此外,Request类还包括了在第1章中介绍过的parse()和getUri()方法。
2.2.3 Response类
ex02.pyrmont.Response类实现javax.servlet.servletResponse接口,类定义参见代码清单2-4。该类提供了servletResponse接口中声明的所有方法的实现。与Request类类似,除了getWriter()方法以外,大部分方法的实现都留空。
在getWriter()方法中,PrintWriter类的构造函数的第2个参数是一个布尔值,表示是否启用autoFlush。对第2个参数传入true表示对println()方法的任何调用都会刷新输出。但是调用print()方法不会刷新输出。
因此,如果在servlet的service()方法的最后一行调用print()方法,则该输出内容不会被发送给浏览器。这个bug会在后续的版本中修改。
Response类中仍然保留了第1章中介绍过的sendStaticResource()方法。
2.2.4 StaticResourceProcessor类
ex02.pyrmont.StaticResourceProcessor类用于处理对静态资源的请求。该类只有一个方法,即process()方法。代码清单2-5给出了StaticResourceProcessor类的定义。
process()方法接收两个参数:一个ex02.pyrmont.Request实例和一个ex02.pyrmont.Response实例。该方法仅仅调用Response对象的sendStaticResource()方法。
2.2.5 servletProcessor1类
ex02.pyrmont.servletProcessor1类的定义参见代码清单2-6,该类用于处理对servlet资源的HTTP请求。
代码清单2-6 servletProcessor1类的定义
package ex02.pyrmont;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.io.File;
import java.io.IOException;
import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class ServletProcessor1 {
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
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.
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());
}
}
}
servletProcessor1类很简单,只有一个方法:process()方法。该方法接收两个参数,一个javax.servlet.ServletRequest实例和一个javax.servlet.ServletResponse实例。该方法通过调用getRequestUri()方法从ServletRequest对象中获取URI:
String uri = request.getUri();
记住,URI的格式如下所示:
/servlet/servletName
其中,servletName是请求的servlet资源的类名。
为了载入servlet类,需要从URI中获取servlet的类名。可以使用process()方法的下一行语句获取servlet的类名:
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
接下来,porcess()方法会载入该servlet类。为了载入类,需要创建一个类载入器,并且指明到哪里查找要载入的类。对于本节的servlet容器,类载入器会到Constant.WEB_ROOT指定的工作目录下的webroot目录中查找要载入的类。
注意 有关类载入器的详细内容将在第8章中介绍。
为了载入一个servlet类,可以使用java.net.URLClassLoader类来完成,该类是java.lang.ClassLoader类的一个直接子类。一旦创建了URLClassLoader类的实例后,就可以使用它的loadClass()方法来载入servlet类。实例化URLClassLoader类很简单。该类有三个构造函数,其中比较简单的一个构造函数的签名如下所示:
public URLClassLoader(URL[] urls);
其中,urls是一个java.net.URL对象数组,当载入一个类时每个URL对象都指明了类载入器要到哪里查找类。若一个URL以“/”结尾,则表明它指向的是一个目录。否则,URL默认指向一个JAR文件,根据需要载入器会下载并打开这个JAR文件。
注意 在servlet容器中,类载入器查找servlet类的目录称为仓库(repository)。
在应用程序中,类载入器只需要查找一个位置,即工作目录下的webroot目录。因此,需要先创建只有一个URL的一个数组。URL类提供了一系列构造函数,因此有很多种方法可以创建URL对象。对于本应用程序,使用与Tomcat中另一个类中使用的相同构造函数,该构造函数的签名如下所示:
public URL(URL context, java.lang.String spec, URLStreamHandler hander)
throws MalformedURLException
可以为第2个参数指定一个目录,指定第1个和第3个参数为null,这样就可以使用构造函数了。但是还有一个构造函数,它接受3个参数:
public URL(java.lang.String protocol, java.lang.String host, java.lang.String file) throws MalformedURLException
因此,若只使用如下语句,编译器就无法知道要调用哪个构造函数了,并且会报错:
new URL(null, aString, null);
因此,可以使用下面的代码,对于编译器指明第三个参数的类型:
URLStreamHandler streamHandler = null;
new URL(null, aString, streamHandler);
第2个参数中的字符串指明了仓库的路径,也就是查找servlet类的目录。可以使用下面的代码生成仓库:
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
将上述代码综合到一起,就得到了创建URLCLassLoader实例的process()方法的部分代码:
注意 生成仓库后会调用org.apache.catalina.startup.ClassLoaderFactory类的createClassLoader()方法,生成URL对象后会调用org.apache.catalina.loader.StandardClassLoader类的addRepository()方法。这些方法将在后续章节中介绍。
有了类载入器后,就可以通过调用loadClass()方法来载入servlet类:
接下来,process()方法会创建已载入的servlet类的一个实例,将其向下转型为javax.servlet.servlet,并调用其service()方法:
2.2.6 运行应用程序
要在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
当调用PrimitiveServlet类时,可以在浏览器中看到如下输出:
Hello. Roses are red.
注意,你是看不到第2个字符串“Violets are blue”的,因为只有第1个字符串会发送到浏览器。这个问题将在第3章解决。