Tomcat剖析(三)
目录:
大家都知道Catalina 中有两个主要的模块:连接器和容器。本节将HttpServer2完善为HttpConnect,创建一个更好的请求和响应对象的连接器,不仅仅是简简单单的调用自己的await方法,而是用线程启动。
上一节是使用Reques对象,因为我们不知道请求的类型,而这一节中,如果是HTTP请求,那么Request对象中需要更多的信息,所以本节的重点是连接器(中的处理器)解析 HTTP 请求头部和cookie并让 servlet 可以获得头部, cookies, 参数名/值等等,不再只是解析URI。同时,将会完善第 2 节中 Response 类的 getWriter 方法,让它能够正确运行。由于这些改进,你将会从上一节的PrimitiveServlet 中获取一个完整的响应,并能够运行更加复杂的ModernServlet。
推荐先到我的github(地址在本文最后)上下载本书相关的代码,同时去上网下载这本书。
上一节的HttpServer 类被分离为两个类:HttpConnector和 HttpProcessor,Request 被 HttpRequest 所取代,而 Response 被 HttpResponse 所取代。HttpServer 类的职责是等待 HTTP 请求并创建请求和响应对象。在本节的应用中,等待 HTTP 请求的工作交给 HttpConnector 实例,而创建请求和响应对象的工作交给了HttpProcessor 实例。
一个 HttpRequest 对象将会给转换为一个 HttpServletRequest 实例并传递给被调用的 servlet 的 service 方法。因此,每个 HttpRequest 实例必须适当增加字段,以便 servlet可以使用它们。值需要赋给 HttpRequest 对象,包括 URI,查询字符串,参数,cookies 和其他的头部等等(可以在代码中看到)。因为连接器并不知道被调用的 servlet(就是我们自己定义的servlet可能是很复杂的,需要请求中所有的参数) 需要哪个值,所以连接器必须从 HTTP 请求中解析所有可获得的值。
这一节的核心类有4个
(上一节讲过的类这一节有的重复出现的,不列出来了):
HttpConnector.java: 等待 HTTP 请求并创建请求和响应对象
HttpProcessor.java:创建请求和响应对象
SocketInputStream.java:负责直接从输入流中获取头部和请求行信息
RequestUtil.java:这节的功能是对请求头中cookie的解析
HttpHeader.java、HttpRequest.java、HttpRequestLine.java、HttpResponse.java分别封装了请求头,请求,请求行,响应需要的内容。HttpRequestFacade.java和HttpResponseFacade.java同样是为了安全性而写的,实现方式和上一节类似。ServletProcessor.java也只是将相应的类(如RequestFacade改为HttpRequestFacade)进行修改。
为了避免篇幅太长,解析头部和解析请求行以及如何获取请求将在下一次补充上。
HttpConnector.java:
1、等待 HTTP 请求
2、为每个请求创建个 HttpProcessor 实例
3、调用 HttpProcessor 的 process 方法
这个类是这一节中最简单的,不用详细说明了,不同的可以参考上一节
代码注释解释如下:
1 package ex03.pyrmont.connector.http; 2 3 import java.io.IOException; 4 import java.net.InetAddress; 5 import java.net.ServerSocket; 6 import java.net.Socket; 7 8 public class HttpConnector implements Runnable { 9 10 boolean stopped; 11 private String scheme = "http"; 12 13 // 返回一个 scheme,这里是HttpConnector,当然是http 14 public String getScheme() { 15 return scheme; 16 } 17 18 public void run() { 19 ServerSocket serverSocket = null; 20 int port = 8080; 21 try { 22 serverSocket = new ServerSocket(port, 1, 23 InetAddress.getByName("127.0.0.1")); 24 } catch (IOException e) { 25 e.printStackTrace(); 26 System.exit(1); 27 } 28 while (!stopped) { 29 // Accept the next incoming connection from the server socket 30 Socket socket = null; 31 try { 32 socket = serverSocket.accept(); 33 } catch (Exception e) { 34 continue; 35 } 36 // 除了用线程启动外,这里也是与上节的不同之处,不从连接器中直接判断请求类型,而是交给HttpProcessor处理 37 HttpProcessor processor = new HttpProcessor(this); 38 processor.process(socket); 39 } 40 } 41 42 public void start() { 43 Thread thread = new Thread(this); 44 thread.start(); 45 } 46 }
HttpProcessor.java
HttpProcessor 类的 process 方法接受前来的 HTTP 请求的套接字,会做下面的事情:
1. 创建一个 HttpRequest 对象。
2. 创建一个 HttpResponse 对象。
3. 解析 HTTP 请求的第一行和头部,并放到 HttpRequest 对象。
4. 解析 HttpRequest 和 HttpResponse 对 象 到 一 个 ServletProcessor 或者StaticResourceProcessor
所以这个类需要有HttpRequest、HttpRequestLine和HttpResponse实例
1 package ex03.pyrmont.connector.http; 2 3 import ex03.pyrmont.ServletProcessor; 4 import ex03.pyrmont.StaticResourceProcessor; 5 6 import java.net.Socket; 7 import java.io.OutputStream; 8 import java.io.IOException; 9 10 import javax.servlet.ServletException; 11 import javax.servlet.http.Cookie; 12 13 import org.apache.catalina.util.RequestUtil; 14 import org.apache.catalina.util.StringManager; 15 16 public class HttpProcessor { 17 18 public HttpProcessor(HttpConnector connector) { 19 this.connector = connector; 20 } 21 //HttpProcessor对应的HttpConnector,由构造器传入 22 private HttpConnector connector = null; 23 private HttpRequest request; 24 private HttpRequestLine requestLine = new HttpRequestLine(); 25 private HttpResponse response; 26 27 protected String method = null; 28 protected String queryString = null; 29 30 //Tomcat的错误管理方式,将在本节最后讲解 31 protected StringManager sm = StringManager 32 .getManager("ex03.pyrmont.connector.http"); 33 34 //由HttpConnector类调用,socket为当前发出请求的用户 35 public void process(Socket socket) { 36 37 //SocketInputStream将在本文后面讲解 38 SocketInputStream input = null; 39 OutputStream output = null; 40 try { 41 //对获取输入流进行封装。 42 //里面的readHeader(HttpHeader)和readRequestLine(HttpRequestLine) 43 //用于直接从得到的流中获取Heander和RequestLine对象 44 input = new SocketInputStream(socket.getInputStream(), 2048); 45 output = socket.getOutputStream(); 46 47 //创建HttpRequest对象 48 request = new HttpRequest(input); 49 50 //创建HttpRequest对象 51 response = new HttpResponse(output); 52 response.setRequest(request); 53 //这里可以设置响应给浏览器的响应头 54 response.setHeader("Server", "Pyrmont Servlet Container"); 55 56 //本节的重点,解析请求行和请求头 57 parseRequest(input, output); 58 parseHeaders(input); 59 60 //if else块和上一节一样,通过判断URI的不同调用不同的Processor处理器 61 if (request.getRequestURI().startsWith("/servlet/")) { 62 ServletProcessor processor = new ServletProcessor(); 63 processor.process(request, response); 64 } else { 65 StaticResourceProcessor processor = new StaticResourceProcessor(); 66 processor.process(request, response); 67 } 68 69 //关闭socket 70 socket.close(); 71 //注意:这个应用还没有关闭程序功能,需要自己强制关闭 72 } catch (Exception e) { 73 e.printStackTrace(); 74 } 75 } 76 77 //功能:解析请求头 78 //这个方法是org.apache.catalina.connector.http.HttpProcessor的简化版本 79 //这个方法解析了一些“简单”的头部,像"cookie", "content-length","content-type",忽略了其他头部 80 private void parseHeaders(SocketInputStream input) throws IOException, 81 ServletException { 82 //进入死循环,直到解析请求头结束跳出 83 while (true) { 84 HttpHeader header = new HttpHeader(); 85 //SocketInputStream流中获取Header对象,本文后面具体讲解 86 //从SocketInputStream代码的readHeader中可以看到通过这里的while方法,一次提取出一个键值对的请求头的值 87 //所以下面的name和value就获得到了相应的值 88 //parseRequest也是使用这样的方法,下面就忽略了 89 input.readHeader(header); 90 91 //检测 HttpHeader 实例的 nameEnd 和 valueEnd 字段来测试是否可以从输入流中读取下一个头部信息 92 if (header.nameEnd == 0) { 93 if (header.valueEnd == 0) { 94 return; 95 } else { 96 throw new ServletException( 97 sm.getString("httpProcessor.parseHeaders.colon")); 98 } 99 } 100 101 String name = new String(header.name, 0, header.nameEnd); 102 String value = new String(header.value, 0, header.valueEnd); 103 104 //将获取到的请求头的名称和值放入HttpRquest对象中 105 //如名称可以为content-length,值可以为10164(某个数字) 106 request.addHeader(name, value); 107 108 //判断是否是cookie(cookie包含在请求头中),格式如 109 //Cookie: BD_UPN=1d314753; ispeed_lsm=4; sugstore=1; BAIDUID=3E664426E867095427DD59:FG=1; BIDUPSID=3E664426E827DD59; PSTM=1440774226; BDUSS=Ex4NkJ0bEF0WTgwMwAAAA; ATS_PSID=1 110 if (name.equals("cookie")) { 111 //如果是cookie,还要对cookie做特殊处理,本文后面讲解 112 Cookie cookies[] = RequestUtil.parseCookieHeader(value); 113 for (int i = 0; i < cookies.length; i++) { 114 if (cookies[i].getName().equals("jsessionid")) { 115 // Override anything requested in the URL 116 if (!request.isRequestedSessionIdFromCookie()) { 117 // Accept only the first session id cookie 118 request.setRequestedSessionId(cookies[i].getValue()); 119 request.setRequestedSessionCookie(true); 120 request.setRequestedSessionURL(false); 121 } 122 } 123 request.addCookie(cookies[i]); 124 } 125 //判断请求中是否有content-length 126 } else if (name.equals("content-length")) { 127 int n = -1; 128 try { 129 //有的话直接转为int类型保存到HttpRequest对象中 130 n = Integer.parseInt(value); 131 } catch (Exception e) { 132 throw new ServletException( 133 sm.getString("httpProcessor.parseHeaders.contentLength")); 134 } 135 request.setContentLength(n); 136 } else if (name.equals("content-type")) { 137 //如果是content-type直接保存 138 request.setContentType(value); 139 } 140 } // end while 141 } 142 143 //解析请求行 144 //请求行如:GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1 145 private void parseRequest(SocketInputStream input, OutputStream output) 146 throws IOException, ServletException { 147 148 //从SocketInputStream对象中直接获取RequestLine对象 149 input.readRequestLine(requestLine); 150 151 //获取请求的方式:如GET 152 String method = new String(requestLine.method, 0, requestLine.methodEnd); 153 //这里没有直接获取请求的URI 154 //因为如/myApp/ModernServlet?userName=tarzan&password=pwd后面有查询的字符串,需要先分割 155 String uri = null; 156 //获取请求的协议版本:如HTTP/1.1 157 String protocol = new String(requestLine.protocol, 0, 158 requestLine.protocolEnd); 159 160 //请求行无效的情况:没有请求的方式或没有请求的URI 161 if (method.length() < 1) { 162 throw new ServletException("Missing HTTP request method"); 163 } else if (requestLine.uriEnd < 1) { 164 throw new ServletException("Missing HTTP request URI"); 165 } 166 //判断和获取请求行中第二项中的请求参数,并获取到URI 167 int question = requestLine.indexOf("?"); 168 if (question >= 0) {//有参数的 169 //得到"?"后面的查询字符串:如userName=tarzan&password=pwd,并保存到HttpRequest对象中 170 request.setQueryString(new String(requestLine.uri, question + 1, 171 requestLine.uriEnd - question - 1)); 172 //得到URI 173 uri = new String(requestLine.uri, 0, question); 174 } else { 175 //没参数的 176 request.setQueryString(null); 177 uri = new String(requestLine.uri, 0, requestLine.uriEnd); 178 } 179 180 //这里的if语句用于请求的不是以/开头的相对资源, 181 //即获取以绝对地址的请求方式的URI 182 if (!uri.startsWith("/")) { 183 int pos = uri.indexOf("://"); 184 // Parsing out protocol and host name 185 if (pos != -1) { 186 pos = uri.indexOf(‘/‘, pos + 3); 187 if (pos == -1) { 188 uri = ""; 189 } else { 190 uri = uri.substring(pos); 191 } 192 } 193 } 194 195 //检查并解析第二项中的可能存在的 jsessionid 196 String match = ";jsessionid="; 197 int semicolon = uri.indexOf(match); 198 if (semicolon >= 0) { 199 String rest = uri.substring(semicolon + match.length()); 200 int semicolon2 = rest.indexOf(‘;‘); 201 if (semicolon2 >= 0) { 202 //将获取到的值放到HttpRequest对象中 203 request.setRequestedSessionId(rest.substring(0, semicolon2)); 204 rest = rest.substring(semicolon2); 205 } else { 206 request.setRequestedSessionId(rest); 207 rest = ""; 208 } 209 //当 jsessionid 被找到,也意味着会话标识符是携带在查询字符串里边,而不是在 cookie里边。需要传递true 210 request.setRequestedSessionURL(true); 211 uri = uri.substring(0, semicolon) + rest; 212 } else { 213 request.setRequestedSessionId(null); 214 request.setRequestedSessionURL(false); 215 } 216 217 //用于纠正“异常”的 URI。 218 String normalizedUri = normalize(uri); 219 220 // Set the corresponding request properties 221 ((HttpRequest) request).setMethod(method); 222 request.setProtocol(protocol); 223 if (normalizedUri != null) { 224 ((HttpRequest) request).setRequestURI(normalizedUri); 225 } else { 226 ((HttpRequest) request).setRequestURI(uri); 227 } 228 229 if (normalizedUri == null) { 230 throw new ServletException("Invalid URI: " + uri + "‘"); 231 } 232 } 233 234 //纠正“异常”的 URI。例如,任何\的出现都会给/替代。 235 //这里涉及到URL的编码解码:编码的格式为:%加字符的ASCII码的十六进制表示 236 //如果URL不能纠正返回null,否则返回相同的或者被纠正后的 URI 237 protected String normalize(String path) { 238 if (path == null) 239 return null; 240 // Create a place for the normalized path 241 String normalized = path; 242 243 //如果URI是/~开头的,除去URI前面前四个字符并加上/~ 244 //%7E->~ 245 if (normalized.startsWith("/%7E") || normalized.startsWith("/%7e")) 246 normalized = "/~" + normalized.substring(4); 247 248 //下面是解码后对应的结果,这些字符不能在URI中出现 249 //%25->% %2F->/ %2E->. %5c->250 //如果找到如下字符的其中一个,说明URI错误 251 if ((normalized.indexOf("%25") >= 0) 252 || (normalized.indexOf("%2F") >= 0) 253 || (normalized.indexOf("%2E") >= 0) 254 || (normalized.indexOf("%5C") >= 0) 255 || (normalized.indexOf("%2f") >= 0) 256 || (normalized.indexOf("%2e") >= 0) 257 || (normalized.indexOf("%5c") >= 0)) { 258 return null; 259 } 260 //如果URI仅仅只是/.则返回/ 261 //如www.cnblogs.com/.是可以纠正的 262 if (normalized.equals("/.")) 263 return "/"; 264 265 //将\转为/,这里的\\是指\,第一个\是转义字符 266 if (normalized.indexOf(‘\\‘) >= 0) 267 normalized = normalized.replace(‘\\‘, ‘/‘); 268 //URI字符串如果没有以/开头就加给它 269 if (!normalized.startsWith("/")) 270 normalized = "/" + normalized; 271 //如果存在//,将剩下/ 272 //如http://www.cnblogs.com/lzb1096101803/p//4797948.html变为 273 //http://www.cnblogs.com/lzb1096101803/p/4797948.html 274 while (true) { 275 int index = normalized.indexOf("//"); 276 if (index < 0) 277 break; 278 normalized = normalized.substring(0, index) 279 + normalized.substring(index + 1); 280 } 281 //如果存在/./,变成/ 282 while (true) { 283 int index = normalized.indexOf("/./"); 284 if (index < 0) 285 break; 286 normalized = normalized.substring(0, index) 287 + normalized.substring(index + 2); 288 } 289 //如果存在/../ 290 while (true) { 291 int index = normalized.indexOf("/../"); 292 if (index < 0) 293 break; 294 if (index == 0) 295 return (null); // Trying to go outside our context 296 int index2 = normalized.lastIndexOf(‘/‘, index - 1); 297 normalized = normalized.substring(0, index2) 298 + normalized.substring(index + 3); 299 } 300 //URI中存在/...或者3个点以上,认为不能纠正 301 if (normalized.indexOf("/...") >= 0) 302 return (null); 303 304 //返回修改后的URI 305 return (normalized); 306 307 } 308 309 }
HttpProcessor.java详细说明:
(其实我更推荐看我注释的代码结合github上的源码进行分析,下面是书上的文字解释,可以帮助大家了解整个流程)
1.解析请求行
HttpProcessor 的 process 方法调用私有方法 parseRequest 用来解析请求行例如一个 HTTP请求的第一行。这里是一个请求行的例子:GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1请求行的第二部分是 URI 加上一个查询字符串。在上面的例子中,URI 是这样的:/myApp/ModernServlet
另外,在问好后面的任何东西都是查询字符串。因此,查询字符串是这样的:userName=tarzan&password=pwd查询字符串可以包括零个或多个参数。在上面的例子中,有两个参数名 / 值对,userName/tarzan 和 password/pwd。在 servlet/JSP 编程中,参数名 jsessionid 是用来携带一个会话标识符。会话标识符经常被作为 cookie 来嵌入,但是程序员可以选择把它嵌入到查询字符串去,例如,当浏览器的 cookie 被禁用的时候。当 parseRequest 方法被 HttpProcessor 类的 process 方法调用的时候,request 变量指向一个 HttpRequest 实例。parseRequest 方法解析请求行用来获得几个值并把这些值赋给HttpRequest 对象。
parseRequest 方法首先调用 SocketInputStream 类的 readRequestLine 方法,在这里 requestLine 是 HttpProcessor 里边的 HttpRequestLine 的一个实例.调用它的 readRequestLine 方法来告诉 SocketInputStream 去填入 HttpRequestLine 实例。接下去,parseRequest 方法获得请求行的方法,URI 和协议.不过,在 URI 后面可以有查询字符串,假如存在的话,查询字符串会被一个问好分隔开来。因此,parseRequest 方法试图首先获取查询字符串。并调用 setQueryString 方法来填充HttpRequest 对象.
大多数情况下,URI 指向一个相对资源,URI 还可以是一个绝对值
然后,查询字符串也可以包含一个会话标识符,用 jsessionid 参数名来指代。因此,parseRequest 方法也检查一个会话标识符。假如在查询字符串里边找到 jessionid,方法就取得会话标识符,并通过调用 setRequestedSessionId 方法把值交给 HttpRequest 实例当 jsessionid 被找到,也意味着会话标识符是携带在查询字符串里边,而不是在 cookie里边。因此,传递 true 给 request 的 setRequestSessionURL 方法。否则,传递 false 给setRequestSessionURL 方法并传递 null 给 setRequestedSessionURL 方法。
到这个时候,uri 的值已经被去掉了 jsessionid。接下去,parseRequest 方法传递 uri 给 normalize 方法,用于纠正“异常”的 URI。例如,任何\的出现都会给/替代。假如 uri 是正确的格式或者异常可以给纠正的话, normalize 将会返回相同的或者被纠正后的 URI。假如 URI 不能纠正的话,它将会给认为是非法的并且通常会返回null。在这种情况下(通常返回 null),parseRequest 将会在方法的最后抛出一个异常。
2.解析头部
parseHeaders 方法包括一个 while 循环用于持续的从 SocketInputStream 中读取头部,直到再也没有头部出现为止。循环从构建一个 HttpHeader 对象开始,并把它传递给类SocketInputStream 的 readHeader 方法.然后,你可以通过检测 HttpHeader 实例的 nameEnd 和 valueEnd 字段来测试是否可以从输入流中读取下一个头部信息
一旦你获取到头部的名称和值,你通过调用 HttpRequest 对象的 addHeader 方法来把它加入
headers 这个 HashMap 中。
些头部也需要某些属性的设置。例如,当 servlet 调用 javax.servlet.ServletRequest的 getContentLength 方法的时候, content-length 头部的值将被返回。而包含 cookies 的 cookie头部将会给添加到 cookie 集合中。
RequestUtil.java解析cookie
这里只贴上解析cookie的相关代码
cookie格式如下:Cookie: userName=budi; password=pwd
只是简单的切割字符串然后将key value放入一个cookie对象中,因为不是很关键的代码,所以不展开代码了,否则篇幅太多
至于如何切割的,可以自己将这句“userName=budi;password=pwd”进行调试
1 public static Cookie[] parseCookieHeader(String header) { 2 3 if ((header == null) || (header.length() < 1)) 4 return (new Cookie[0]); 5 6 ArrayList cookies = new ArrayList(); 7 while (header.length() > 0) { 8 int semicolon = header.indexOf(‘;‘); 9 if (semicolon < 0) 10 semicolon = header.length(); 11 if (semicolon == 0) 12 break; 13 String token = header.substring(0, semicolon); 14 if (semicolon < header.length()) 15 header = header.substring(semicolon + 1); 16 else 17 header = ""; 18 try { 19 int equals = token.indexOf(‘=‘); 20 if (equals > 0) { 21 String name = token.substring(0, equals).trim(); 22 String value = token.substring(equals+1).trim(); 23 cookies.add(new Cookie(name, value)); 24 } 25 } catch (Throwable e) { 26 ; 27 } 28 } 29 30 return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()])); 31 32 }
SocketInputStream.java
上一节,你没有试图为那两个应用程序去进一步解析请求。org.apache.catalina.connector.http.SocketInputStream 提供的方法不
仅用来获取请求行,还有请求头部。通过传递一个 InputStream 和一个指代实例使用的缓冲区大小的整数,来构建一个
SocketInputStream 实例。
将在下一节具体说明。
最后我们了解一下StringManager类,这个类在整个Catalina项目中有着重要的作用(用来处理错误信息),对于本节的应用,可以在ex03.pyrmont.connector.http.HttpProcessor中找到这个类的实例。
一个像 Tomcat 这样的大型应用需要仔细的处理错误信息。在 Tomcat 中,错误信息对于系统管理员和 servlet 程序员都是有用的。例如,Tomcat 记录错误信息,让系统管理员可以定位发生的任何异常。对 servlet 程 序 员 来 说 , Tomcat 会在抛出的任何一个javax.servlet.ServletException 中发送一个错误信息,这样程序员可以知道他 servlet究竟发送什么错误了。Tomcat 所采用的方法是在一个属性文件里边存储错误信息,这样,可以容易的修改这些信息。
看看StringManager类,因为篇幅有限,下面是部分核心代码。
代码中的注释有助于理解。
1 package org.apache.catalina.util; 2 3 import java.util.Hashtable; 4 import java.util.MissingResourceException; 5 import java.util.ResourceBundle; 6 7 public class StringManager { 8 9 private ResourceBundle bundle; 10 11 //设为private,单例模式的特点,要获取对象,可以通过getManager(String)方法 12 //就是在这里获取了实例对应包下的bundle对象。 13 //格式如org.apache.catalina.util.packageName.LocalStrigns 14 private StringManagerMy(String packageName) { 15 String bundleName = packageName + ".LocalStrings"; 16 bundle = ResourceBundle.getBundle(bundleName); 17 } 18 19 //StringManager类实例的包下properties文件中等号左边的值作为Key传递 20 //返回的是等号右面的信息 21 public String getString(String key) { 22 if (key == null) { 23 String msg = "key is null"; 24 25 throw new NullPointerException(msg); 26 } 27 String str = null; 28 try { 29 str = bundle.getString(key); 30 } catch (MissingResourceException mre) { 31 str = "Cannot find message associated with key ‘" + key + "‘"; 32 } 33 return str; 34 } 35 36 //保存不同包下的StringManager对象 37 private static Hashtable managers = new Hashtable(); 38 39 //单例模式用这个方法获取不同包的StringManager对象, 40 //因为可能同时被多个类使用产生错误,所以方法需要设置为同步 41 public synchronized static StringManager getManager(String packageName) { 42 StringManager mgr = (StringManager) managers.get(packageName); 43 if (mgr == null) { 44 mgr = new StringManager(packageName); 45 managers.put(packageName, mgr); 46 } 47 return mgr; 48 } 49 }
StringManager详细说明:
Tomcat有数以百计的类,如果把所有的类的错误信息都保存在某个大的属性文件中,将导致很严重的维护问题。为了避免这一情况,Tomcat 为每个包都分配一个属性文件。例如,在包 org.apache.catalina.connector 里边的属性文件包含了该包所有的类抛出的所有错误信息。每个属性文件都会被一个 org.apache.catalina.util.StringManager 类的实例所处理。相同包里边的许多类可能也需要 StringManager,为每个对象创建一个 StringManager 实例是一种资源浪费。StringManager类采用了单例模式,同一包下使用同一StringManager对象即可,放入StringManager类的managers实例中,不同包才使用不同的对象。
当Tomcat 运行时,将会有许多 StringManager 实例,每个实例会读取这个实例对应包下的一个properties属性文件。此外,由于 Tomcat 的受欢迎程度,提供多种语言的错误信息也是有意义的。目前,有三种语言是被支持的。英语的错误信息属性文件名为 LocalStrings.properties。另外两个是西班牙语和日语,分别放在 LocalStrings_es.properties 和 LocalStrings_ja.properties 里边。如果对Bundle实现国际化使用不熟悉的,可以自行查阅相关资料。
没有展示的方法都是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
说了那么多,大家应该可以理解了吧。
附:
相应代码可以在我的github上找到下载,拷贝到eclipse,然后打开对应包的代码即可。
https://github.com/zebinlin/Tomcat4-src
如发现编译错误,可能是由于jdk不同版本对编译的要求不同导致的,可以不管,供学习研究使用。