除非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 章详细讨论它。