Tomcat剖析(三):连接器(2)
- 1. Tomcat剖析(一):一个简单的Web服务器
- 2. Tomcat剖析(二):一个简单的Servlet服务器
- 3. Tomcat剖析(三):连接器(1)
- 4. Tomcat剖析(三):连接器(2)
- 5. Tomcat剖析(四):Tomcat默认连接器(1)
- 6. Tomcat剖析(四):Tomcat默认连接器(2)
- 7. Tomcat剖析(五):容器
第一部分:概述
这一节基于《深度剖析Tomcat》第三章:连接器 总结而成,作为上一节的补充,补充上一节未涉及的当我们获取参数时才解析参数这个功能。同时插入Tomcat错误处理的方法
最好先到我的github上下载本书相关的代码,同时去上网下载这本书。
核心类:
- HttpRequest.java:主要是它里面的相关获取参数的方法
- ParameterMap.java:存放参数列表
- StringManager.java:获取错误信息时使用的类
第二部分:代码讲解
HttpRequest.java
功能:动态获取参数
因为只有我们发出请求且从未解析过时才会去读取参数,所以ex03.pyrmont.connector.http.HttpRequest中的getParameter,getParameterMap, getParameterNames 或者 getParameterValues 四个读取参数的方法开头都调用了 parseParameter 方法。如果已经解析过了(参数在请求内容里边被找到的话,),参数解析将会使得 SocketInputStream 到达字节流的尾部。类 HttpRequest 使用一个布尔变量 parsed 来指示是否已经解析过了。
下面先看看parseParameter方法及其里面的注释
protected void parseParameters() {
if (parsed) //如果已经解析过了,直接返回
return;
ParameterMap results = parameters;
if (results == null)//ParameterMap本文下面讲解
results = new ParameterMap();
results.setLocked(false); //打开 parameterMap的锁以便写值。
String encoding = getCharacterEncoding();
if (encoding == null)//检查字符编码,并在字符编码为 null的时候赋予默认字符编码。
encoding = "ISO-8859-1";
//getQueryString方法在上一节解析了请求头时,如果URL如果有查询参数有设置。
//尝试解析查询字符串。解析参数是使用org.apache.Catalina.util.RequestUtil的 parseParameters方法来处理的。
//如果queryString为空(URL中没有参数),下面parseParameters方法的的解析直接返回
String queryString = getQueryString();
try {
RequestUtil.parseParameters(results, queryString, encoding);
} catch (UnsupportedEncodingException e) {
;
}
//获取内容类型
String contentType = getContentType();
if (contentType == null)
contentType = "";
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
//请求方式是POST(内容长度大于零)且内容类型是 application/x-www-form-urlencoded
//同样用parseParameters解析POST中的内容
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");
}
}
//锁定 ParameterMap表示不可修改参数
//设置 parsed为 true表示已经解析过了,
results.setLocked(true);
parsed = true;
parameters = results;
}
获取的参数可以在查询字符串或者请求内容里边找到。假如用户使用 GET 方法来请求 servlet 的话,所有的参数将在查询字符串里边出现。假如使用 POST 方法的话,你可以在请求内容中找到一些。这就是为什么会处理两次解析的原因。
ParameterMap.java
功能:参数所有的名/值对存储在HashMap里边。
Servlet 程序员可以以 Map 的形式获得参数(通过调用 HttpServletRequest 的 getParameterMap 方法)和参数名/值,但不允许修改参数值。因此将使用一个特殊的HashMap---org.apache.catalina.util.ParameterMap。
ParameterMap 继承了 java.util.HashMap,所以许多方法都是用super关键字直接调用HashMap中的方法
那又是如何保证参数不被修改呢?
Tomcat在ParameterMap中加入布尔变量 locked 当 locked 是false 的时候,名/值对仅仅可以添加,更新或者移除。否则,lock为true时,异常 IllegalStateException 会抛出,结合parseParameters方法可以更加清晰了解。所以在put时做了些许的修改(对错误处理的类StringManager上一节说过了。)
public Object put(Object key, Object value) { if (locked)
throw new IllegalStateException
(sm.getString("parameterMap.locked"));
return (super.put(key, value));
}
因为Tomcat4时还没有泛型,所以没有使用泛型,同时也没有继承效率更高的LinkedHashMap
Tomcat7中是这样的(一小片段)
public final class ParameterMap<K,V> extends LinkedHashMap<K,V> { public V put(K key, V value) { if (locked)
throw new IllegalStateException
(sm.getString("parameterMap.locked"));
return (super.put(key, value)); }
}
StringManager.java
功能:获取错误信息
这个类在整个Catalina项目中有着比较重要的作用,对于本节的应用,可以在ex03.pyrmont.connector.http.HttpProcessor中找到这个类的实例。
一个像 Tomcat 这样的大型应用需要仔细的处理错误信息。在 Tomcat 中,错误信息对于系统管理员和 servlet 程序员都是有用的。例如,Tomcat 记录错误信息,让系统管理员可以定位发生的任何异常。对 servlet 程 序 员 来 说 , Tomcat 会在抛出的任何一个javax.servlet.ServletException 中发送一个错误信息,这样程序员可以知道他 servlet究竟发送什么错误了。Tomcat 所采用的方法是在一个属性文件里边存储错误信息,这样,可以容易的修改这些信息。
看看StringManager类,下面是核心代码。 代码中的注释有助于理解。
package org.apache.catalina.util;
import java.util.Hashtable;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
public class StringManager {
private ResourceBundle bundle;
//设为private,单例模式的特点,要获取对象,可以通过getManager(String)方法
//就是在这里获取了实例对应包下的bundle对象。
//格式如org.apache.catalina.util.LocalStrigns.properties
private StringManager(String packageName) {
String bundleName = packageName + ".LocalStrings";
bundle = ResourceBundle.getBundle(bundleName);
}
//StringManager类实例的包下properties文件中等号左边的值作为Key传递
//返回的是等号右面的信息
public String getString(String key) {
if (key == null) {
String msg = "key is null";
throw new NullPointerException(msg);
}
String str = null;
try {
str = bundle.getString(key);
} catch (MissingResourceException mre) {
str = "Cannot find message associated with key '" + key + "'";
}
return str;
}
//保存不同包下的StringManager对象
private static Hashtable managers = new Hashtable();
//单例模式用这个方法获取不同包的StringManager对象,
//因为可能同时被多个类使用产生错误,所以方法需要设置为同步
public synchronized static StringManager getManager(String packageName) {
StringManager mgr = (StringManager) managers.get(packageName);
if (mgr == null) {
mgr = new StringManager(packageName);
managers.put(packageName, mgr);
}
return mgr;
}
}
StringManager详细说明:
Tomcat有数以百计的类,如果把所有的类的错误信息都保存在某个大的属性文件中,将导致很严重的维护问题。为了避免这一情况,Tomcat 为每个包都分配一个属性文件。例如,在包org.apache.catalina.connector 里边的属性文件包含了该包所有的类抛出的所有错误信息。每个属性文件都会被一个 org.apache.catalina.util.StringManager 类的实例所处理。相同包里边的许多类可能也需要 StringManager,为每个对象创建一个 StringManager 实例是一种资源浪费。StringManager类采用了单例模式,同一包下使用同一StringManager对象即可,放入StringManager类的managers实例中,不同包才使用不同的对象。
当Tomcat 运行时,将会有许多 StringManager 实例,每个实例会读取这个实例对应包下的一个properties属性文件。此外,由于 Tomcat 的受欢迎程度,提供多种语言的错误信息也是有意义的。目前,有三种语言是被支持的。英语的错误信息属性文件名为 LocalStrings.properties。另外两个是西班牙语和日语,分别放在 LocalStringses.properties 和 LocalStringsja.properties里边。
没有展示的方法都是getString的重载方法,可以在org.apache.catalina.util.Manager中找到这个类查看具体实现。
在ex03.pyrmont.connector.http.HttpProcessor.java中有这样几段
protected StringManager sm = StringManager
.getManager("ex03.pyrmont.connector.http");
throw new ServletException(
sm.getString("httpProcessor.parseHeaders.colon"));
在ex03.pyrmont.connector.http.LocalStrings.properties中对应的有
httpProcessor.parseHeaders.colon=Invalid HTTP header format
说了那么多,大家应该可以理解了吧。
最后简单说说ResourceBundle国际化的使用:
比如有如下目录结构
package org.bundle;
import java.util.Locale;
import java.util.ResourceBundle;
public class TestResourceBundle {
public static void main(String[] args) {
Locale locale1 = new Locale("zh", "CN");
ResourceBundle resb1 = ResourceBundle.getBundle("org.bundle.myres", locale1);
System.out.println(resb1.getString("login"));
Locale locale3 = new Locale("en", "US");
ResourceBundle resb3 = ResourceBundle.getBundle("org.bundle.myres", locale3);
System.out.println(resb3.getString("login"));
ResourceBundle resb2 = ResourceBundle.getBundle("org.bundle.myres");//按
System.out.println(resb2.getString("login"));
}
}
myres_en_US.properties和myres.properties内容
login=login
myres_zh_CN.properties内容:后面表示“请登录"中文的UTF-8编码
login=\u8BF7\u767B\u5F55
读取的文件命名有规范: 自定义名_语言代码_国别代码.properties,
对于ResourceBundle而言,需要加上完整包名,getBundle第一个参数就是完整包名+自定义名 ,而语言代码和国别代码来自Locale中。
输出结果
请登录
login
请登录
可以看到,如果没有指定Locale,使用的是系统默认的区域和语言。
第三部分:小结
首先我们先讲解了Tomcat如何动态获取参数且只解析一次,即parsed变量和HashMap维护
然后了解了如何防止修改参数值,即扩展HashMap并通过locked变量
最后我们了解了Tomcat获取错误信息的方法
如果我们以后在一个很大型的项目中有许多的类需要处理和管理大量信息时(不仅仅国际化和错误处理) ,你能联想到Tomcat是如何管理错误信息的?我们可以通过包内单例模式,包之间实例保存在Map中。 这样既实现了有效的管理,又节省了内存消耗。所以说学习Tomcat中最重要的是学习思想。
关于Tomcat4中连接器就简单说到这,下一节中将讲解Tomcat4默认的连接器,是难点。
如果觉得写得不错的话就推荐一下。
附
相应代码可以在我的github上找到下载,拷贝到eclipse,然后打开对应包的代码即可。
如发现编译错误,可能是由于jdk不同版本对编译的要求不同导致的,可以不管,供学习研究使用。