除非servlet 调用javax.servlet.http.HttpServletRequest 的getParameter 、getParameterMap 、getParameterNames 和getParameterValues 方 法来读取请求参数,否则,我们都不需要解析query string 或HTTP 请求体(request body )。 因此,HttpRequest 类实现的这四个方法,都 是先调用parseParameter 方法。
请求参数只需要被解析一次,而且也许只能被解析一次,因为如果请求参数是在请求体中,参数解析会导致SocketInputStream 到 达字节流的末尾。HttpRequest 类使用boolean 成员parsed 来 表示请求参数是否已经被解析。
如果是GET 请求,那 么所有的请求参数都在query sting 中。如果是POST 请求,在请求体中也可以找到一些参数。所有参数以名/值对的形式存储在一个HashMap 中。Servlet 程 序员能够以两种形式获取参数:Map (调用HttpServletRequest 的getParameterMap 方 法)和名/值对。但是,这里有个“圈套”。Servlet 程序员不允许修改参 数的值。因此,使用了一个特殊的HashMap :org.apache.catalina.util.ParameterMap 。
ParameterMap 类继承了java.util.HashMap ,并使用了一个boolean 成 员locked 。只有locked 的 值为false 时,名/值对才能被添加、修改和删除。否则,将会抛出一个IllegalStateException 异常。ParameterMap 的代码见Listing 3.6 。 它重载了添加、修改和删除元素的方法。这些方法只能在locked 值为false 才能被调用。
Listing 3.6: The org.apache.Catalina.util.ParameterMap class.
package org.apache.catalina.util; import java.util.HashMap; import java.util.Map; public final class ParameterMap extends HashMap { public ParameterMap() { super (); } public ParameterMap(int initialCapacity) { super(initialCapacity); } public ParameterMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); } public ParameterMap(Map map) { super(map); } private boolean locked = false; public boolean isLocked() { return (this.locked); } public void setLocked(boolean locked) { this.locked = locked; } private static final StringManager sm = StringManager.getManager("org.apache.catalina.util"); public void clear() { if (locked) throw new IllegalStateException(sm.getString("parameterMap.locked")); super.clear(); } public Object put(Object key, Object value) { if (locked) throw new IllegalStateException (sm.getString("parameterMap.locked")); return (super.put(key, value)); } public void putAll(Map map) { if (locked) throw new IllegalStateException (sm.getString("parameterMap.locked")); super.putAll(map); } public Object remove(Object key) { if (locked) throw new IllegalStateException (sm.getString("parameterMap.locked")); return (super.remove(key)); } }
现在,让我们看看parseParameters 方法是如何工作的。
由于请求参数既可以存在于query string 中,也可以存在于 HTTP请求体中,parseParameters 方法必须都检查query string 和请求体。一旦解析完成,就可以在parameters 成员变量中找到这些参数。因此该方法首先检查成员变量parsed 的值,如果之前已经解析过,parsed 的 值就是true 。
if (parsed) return;
然后,parseParameters 方法声明创建一个名为results 的ParameterMap 变量,将它指向parameters 。如果parameters 为null ,就创建一个新的ParameterMap 对 象。
ParameterMap results = parameters; if (results == null) results = new ParameterMap();
接着,parseParameters 方法打开ParameterMap 的 锁,以便修改它。
results.setLocked(false);
下一步,parseParameters 方法检查编码,如果编码为空则赋予默认编码。
String encoding = getCharacterEncoding(); if (encoding == null) encoding = "ISO-8859-1";
接着,parseParameters 方 法尝试解析query string 。参数解析是由org.apache.Catalina.util.RequestUtil 类的parseParameter 方法完成的。
// Parse any parameters specified in the query string String queryString = getQueryString(); try { RequestUtil.parseParameters(results, queryString, encoding); } catch (UnsupportedEncodingException e) { ; }
下一步,parseParameters 方法尝试看看HTTP 请求体中是否包含参数。如果用户使用POST 方法发送请求,content length 大 于0 ,并且content type 是application/x-www-form-urlencoded , 那么请求体中就会包含参数。解析请求体的代码如下。
// Parse any parameters specified in the input stream String contentType = getContentType(); if (contentType == null) contentType = ""; int semicolon = contentType.indexOf(';'); if (semicolon >= 0) { contentType = contentType.substring (0, semicolon).trim(); } else { contentType = contentType.trim(); } if ("POST".equals(getMethod()) && (getContentLength() > 0) && "application/x-www-form-urlencoded".equals(contentType)) { try { int max = getContentLength(); int len = 0; byte buf[] = new byte[getContentLength()]; ServletInputStream is = getInputStream(); while (len < max) { int next = is.read(buf, len, max - len); if (next < 0 ) { break; } len += next; } is.close(); if (len < max) { throw new RuntimeException("Content length mismatch"); } RequestUtil.parseParameters(results, buf, encoding); } catch (UnsupportedEncodingException ue) { ; } catch (IOException e) { throw new RuntimeException("Content read fail"); } }
最后,parseParameters 方 法锁住ParameterMap ,设置parsed 成员的值为true ,并将results 赋值给parameters 。
// Store the final results results.setLocked(true); parsed = true; parameters = results;
HttpResponse 类实现了javax.servlet.http.HttpServletResponse 接口,跟随该类的 还有一个门面类HttpResponseFacade 。Figure 3.3 展现了HttpResponse 和 相关类的类图。
在第2 章,我们使用的HttpResponse 类 仅包括部分功能。例如,它的getWriter 方法返回了一个java.io.PrintWriter 对象,该对象的print 方法并不会自动flush 。本章的应用将修复这个 问题。为了理解如何修复的,你需要知道Writer 是什么。
在servlet 内部,你使用PrintWriter 来 写入字符。你可以使用任何你希望的编码,但是字符总是作为字节流发送给浏览器。因此,第2 章 中ex02.pyrmont.HttpResponse 类getWriter 方法的实现也就不奇怪了:
public PrintWriter getWriter() { // if autoflush is true, println() will flush, // but print() will not. // the output argument is an OutputStream writer = new PrintWriter(output, true); return writer; }
看,我们是如何创建PrintWriter 对象的?传递一个java.io.OutputStream 实 例作为参数。任何内容传递给PrintWriter 的print 或println 方 法,都会被转成字节流,通过底层OutputStream 对象发送出去。
本章我们使用ex03.pyrmont.connector.ResponseStream 类 的实例作为PrintWriter 的OutputStream 。注意,ResponseStream 类 直接继承了java.io.OutputStream 类。
本章我们还使用继承了PrintWriter 的ex03.pyrmont.connector.ResponseWriter 类。ResponseWriter 类重写了所有的print 和println 方法,对这些方法的调用都会自动将输出flush 到底层的OutputStream 。因此,我们使 用以ResponseStream 对象作为底层的ResponseWriter 实例。
我们本可以将一个ResponseStream 对象作为参数,实例化ResponseWriter 类。然而,我们使用java.io.OutputStreamWriter 对 象桥接了ResponseWriter 对象和ResponseStream 对象。
有了OutputStreamWriter , 被写入的字符自动按照指定的字符集被编码成字节。这里使用的字符集,或者通过名称来指定,或者显式指定Charset 对 象,或者使用平台默认的字符集。对write 方法的每次调用,都会对给定的字符进行编 码转换。在写入底层输入流之前,转换后的字节先被累积在缓冲区中。缓冲区的大小可以指定,但对于大多数场景来说,默认大小已经足够了。
因此,getWriter 方法就按下面的代码来实现:
public PrintWriter getWriter() throws IOException { ResponseStream newStream = new ResponseStream(this); newStream.setCommit(false); OutputStreamWriter osr = new OutputStreamWriter(newStream, getCharacterEncoding()); writer = new ResponseWriter(osr); return writer; }
ServletProcessor 类和第2 章的ex02.pyrmont.ServletProcessor 类 是类似的。它们都只有一个方法:process 。然而,ex03.pyrmont.connector.ServletProcessor 的process 方法接受一个HttpRequest 对 象和一个HttpResponse 对象,而不是Request 对象和Response 对 象。下面是本章应用的process 方法原型:
public void process(HttpRequest request, HttpResponse response) {
另外,process 方法使用HttpRequestFacade 和HttpResponseFacade ,作为请求和响应的门面类。而且在调用了servlet 的service 方 法后,还调用了HttpResponse 类的finishResponse 方法。
servlet = (Servlet) myClass.newInstance(); HttpRequestFacade requestPacade = new HttpRequestFacade(request); HttpResponseFacade responseFacade = new HttpResponseFacade(response); servlet.service(requestFacade, responseFacade); ((HttpResponse) response).finishResponse();
StaticResourceProcessor 类和ex02.pyrmont.StaticResourceProcessor 基本相同。
要在Windows 运行该应用,在工作目录运行以下命令:
java -classpath ./lib/servlet.jar;./ ex03.pyrmont.startup.Bootstrap
在Linux 上,需要使用冒号来分 隔两个库。
java -classpath ./lib/servlet.jar:./ ex03.pyrmont.startup.Bootstrap
要显示index.html ,使用下面的URL :
http://localhost:808O/index.html
要调用PrimitiveServlet,直接在浏览器中访问下面的URL:
http://localhost:8080/servlet/PrimitiveServlet
你会在浏览器中看到下面的内容:
Hello. Roses are red. Violets are blue.
提示:第2章中运行 PrimitiveServlet 时,并没有输出第二行。
你也可以调用第2 章中没有的ModernServlet 。URL 是:
http://localhost:8080/servlet/ModernServlet
提 示: ModernServlet 的代码在工作目录的 webroot 目录下。
你可以在URL后面拼上一个query stirng ,来测试servlet 。Figure 3.4 显示了, 以下面URL 运行ModernServlet 的 输出结果:
http://localhost:8080/servlet/ModernServlet?userName=tarzan&password=pwd
Figure 3.4 : 运行ModernServlet
本章你已经学习到了连接器 是如何工作的。本章构建的连接器 ,是Tomcat 4 默 认连接器 的一个简化版本。正如你知道的,这个默认连接器 因为低效而被不推荐使用(deprecated )。 例如,所有的HTTP 请求headers都被解析,即使servlet 从来没有使用它们。结果,默认连接器 运行速度缓慢,并被更快的Coyote 代替。Coyote 的代码可以从Apache基金会网站下载到。无论如何,默认连接器都是一个很好的学 习工具,我们将在第4 章详细讨论它。