现在开始《深入剖析Tomcat》第二章的内容,第一章中,我们编码实现了一个能正常接收HTTP请求并返回静态资源的Web容器,这一章开始引入Servlet的概念,使我们的服务能根据请求动态返回内容。
Servlet是什么?
这是首先要弄清楚的一个东西,有篇文章对它的历史由来介绍的很好【Servlet的历史与规范】,推荐阅读它的第一节:Servlet历史。
如果你是一个Java开发者,那么你经常写的Controller就可以视为一个Servlet。Servlet是处理某个特定HTTP请求的组件程序,它不能独立工作,由Servlet容器统一管理众多Servlet,Servlet容器负责将HTTP请求分发给指定的Servlet。
如上图所示,Tomcat大概分为连接器和Servlet容器两大块,连接器负责接收HTTP请求,并封装成Request与Response对象,然后将处理过程交给Servlet容器,Servlet容器会判断这个请求该交给哪个Servlet来处理,Servlet处理完后给客户端反馈。
Servlet规范是Sun公司制定的一套技术标准,包含与Web应用相关的一系列接口,是Web应用实现方式的宏观解决方案。而具体的Servlet容器则根据规范提供标准的实现。Tomcat,Jetty 等都是Servlet容器,它们都遵循Servlet规范,也就是说它们都是基于Servlet的标准接口来实现的自定义逻辑。
Servlet规范内容很多,这里仅提一下本章涉及到的知识,其他知识待用到时再补充。下面介绍几个规范中的接口
javax.servlet.Servlet
实现了 javax.servlet.Servlet 接口的类,就是一个Servlet。
package javax.servlet;import java.io.IOException;
/*** 定义所有servlet必须实现的方法。* servlet是在Web服务器中运行的小Java程序。* servlet通常通过HTTP(超文本传输协议)接收和响应来自Web客户机的请求。* 要实现这个接口,可以编写一个通用的servlet,例如 继承javax.servlet.GenericServlet,也可以编写一个HTTP servlet通过继承javax.servlet.http.HttpServlet。* 这个接口定义了初始化servlet、处理请求和从容器中删除servlet的方法。这些方法被称为生命周期方法,按以下顺序调用:* 1.首先构造servlet,然后使用init方法进行初始化。* 2.service方法处理客户端的所有请求。* 3.使用destroy方法销毁servlet,然后进行垃圾收集并最终完成销毁过程。* 除了生命周期方法之外,这个接口还提供了getServletConfig方法(servlet可以使用该方法获取任何启动信息)* 和getServletInfo方法(允许servlet返回关于自身的基本信息,例如作者、版本和版权)。*/
public interface Servlet {/*** 由servlet容器调用,以指示servlet正在被放入服务中。* servlet容器在实例化servlet之后只调用init方法一次。* init方法必须成功完成,servlet才能接收请求。* 如果init方法发生下列情况,servlet容器就不能将servlet放入容器* 1.抛出异常 ServletException* 2.没有在Web服务器定义的时间段内返回*/void init(ServletConfig var1) throws ServletException;/*** 返回一个ServletConfig对象,其中包含此servlet的初始化和启动参数。返回的ServletConfig对象是传递给init方法的对象。 * 这个接口的实现负责存储ServletConfig对象,以便这个方法可以返回它。实现这个接口的GenericServlet类已经做到了这一点。*/ServletConfig getServletConfig();/*** 由servlet容器调用,以允许servlet响应请求。* 此方法仅在servlet的init()方法成功完成后调用。* 响应的状态码应该始终为抛出或发送错误的servlet设置。* servlet通常运行在多线程servlet容器中,可以并发处理多个请求。开发人员必须注意同步访问任何共享资源,如文件、网络连接* 以及servlet的类和实例变量。有关Java中多线程编程的更多信息,请参见Java多线程编程教程。*/void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;/*** 返回有关servlet的信息,如作者、版本和版权。 * 此方法返回的字符串应该是纯文本,而不是任何类型的标记(如HTML、XML等)。*/String getServletInfo();/*** 由servlet容器调用,以指示servlet正在退出服务。此方法仅在servlet服务方法中的所有线程都退出或超时时间过后才调用。* 在servlet容器调用此方法之后,它将不会在此servlet上再次调用该服务方法。 * 此方法为servlet提供了一个机会来清理任何被占用的资源(例如,内存、文件句柄、线程),并确保任何持久状态都与servlet在内存中的当前状态同步。*/void destroy();
}
javax.servlet.ServletConfig
package javax.servlet;import java.util.Enumeration;/*** 一个servlet配置对象,由servlet容器使用,在初始化期间向servlet传递信息*/
public interface ServletConfig {/*** 返回此servlet实例的名称。该名称可以通过服务器管理提供,在web应用程序部署描述符中分配,或者对于未注册(因此未命名)的servlet实例,它将是servlet的类名。*/public String getServletName();/*** 返回对调用者正在其中执行的ServletContext的引用。*/public ServletContext getServletContext();/*** 返回一个包含初始化参数值的字符串,如果参数不存在则返回null。*/public String getInitParameter(String name);/*** 返回servlet初始化参数的枚举,如果servlet没有初始化参数,则返回空枚举。*/public Enumeration<String> getInitParameterNames();
}
javax.servlet.ServletContext
ServletContext接口定义的方法太多,就不在这里放代码了。
看一下这个接口的注释吧:
定义servlet用于与其servlet容器通信的一组方法,例如,获取文件的MIME类型、调度请求或写入日志文件。
每个Java虚拟机的每个“web应用程序”都有一个上下文。(“web应用程序”是安装在服务器URL命名空间(如/catalog)的特定子集下的servlet和内容的集合,可能通过.war文件安装。)
在部署描述符中标记为“分布式”的web应用程序的情况下,每个虚拟机将有一个上下文实例。在这种情况下,上下文不能用作共享全局信息的位置(因为信息不是真正的全局信息)。使用外部资源,比如数据库。
ServletContext对象包含在ServletConfig对象中,Web服务器在初始化servlet时提供ServletConfig对象。
下面列举这个接口的一些常用方法
- String getContextPath():获取Web应用程序的上下文路径。
- String getRealPath(String path):将指定的虚拟路径映射到实际路径。
- RequestDispatcher getRequestDispatcher(String path):获取用于将请求转发到另一个资源的请求分派器。
- InputStream getResourceAsStream(String path):获取位于Web应用程序上下文的指定路径的输入流。
- URL getResource(String path):返回指定路径的URL,用于从ServletContext获取资源。
- ServletContext getContext(String uripath):获取指定URI路径的ServletContext。
- String getMimeType(String file):获取指定文件的MIME类型。
- Set<String> getResourcePaths(String path):获取指定路径下的所有资源路径的集合。
- String getInitParameter(String name):获取指定名称的初始化参数的值。
- Enumeration<String> getInitParameterNames():获取所有初始化参数的名称的枚举。
- void setAttribute(String name, Object object):在ServletContext中设置一个属性。
- Object getAttribute(String name):获取指定名称的ServletContext属性。
- void removeAttribute(String name):从ServletContext中移除指定名称的属性。
- String getServerInfo():获取Servlet容器的名称和版本信息。
- int getMajorVersion() 和 int getMinorVersion():获取Servlet API的主版本号和次版本号。
javax.servlet.ServletRequest 与 javax.servlet.ServletResponse
这两个类是javax.servlet.Servlet#service 方法需要的两个参数,也就是说 servlet 需要这两个参数来处理请求与返回结果,所有的HTTP请求打到Servlet容器时都需要封装成这两个对象。
ServletRequest 接口代表客户端请求,并提供了访问请求参数、请求头和其他请求信息的方法。以下是一些常用的方法:
- String getParameter(String name):根据参数名称获取请求参数的值。
- Enumeration<String> getParameterNames():获取所有请求参数名称的枚举。
- String[] getParameterValues(String name):根据参数名称获取请求参数的值数组。
- Map<String, String[]> getParameterMap():获取所有请求参数的映射。
- String getHeader(String name):根据头部名称获取请求头的值。
- Enumeration<String> getHeaderNames():获取所有请求头名称的枚举。
- String getMethod():获取请求的 HTTP 方法,例如 GET、POST 等。
- String getRemoteAddr():获取客户端的 IP 地址。
- String getRemoteHost():获取客户端的主机名。
- int getContentLength():获取请求正文的长度。
- String getContentType():获取请求正文的 MIME 类型。
- InputStream getInputStream():获取请求正文的输入流。
ServletResponse 接口代表 Servlet 对客户端的响应,并提供了设置响应内容和发送响应的方法。以下是一些常用的方法:
- void setContentType(String type):设置响应的内容类型。
- String getContentType():获取响应的内容类型。
- void setContentLength(int len):设置响应正文的长度。
- void setCharacterEncoding(String charset):设置响应字符编码。
- PrintWriter getWriter():获取一个可以向客户端发送字符数据的 PrintWriter 对象。
- ServletOutputStream getOutputStream():获取一个可以向客户端发送二进制数据的 ServletOutputStream 对象。
- void sendRedirect(String location):重定向响应到指定的 URL。
- void setStatus(int sc):设置响应的状态码。
- void setHeader(String name, String value):设置响应头的值。
- void addHeader(String name, String value):添加响应头的值。
本章用到的接口就是上面这些了,下面来看看本章代码的设计
Servlet容器代码设计
本章内容中加入servlet的设计,立个规定:servlet的请求均以“/servlet”打头,格式为“/servlet/servletName”,其他请求皆认为还是请求静态资源。
本章通过两个小应用程序说明如何开发自己的servlet容器,第一个应用程序的设计非常简单,仅仅用于说明servlet容器是如何运行的。它然后演变为第二个servlet容器,后者稍微复杂一点。
第一个应用程序
借用一下书中的UML图,来看下本次代码设计
区别于第一章的代码,本章代码做了如下改造
- Request类实现了javax.servlet.ServletRequest接口
- Response类实现了javax.servlet.ServletResponse接口
- HttpServer1中能够同时接收动态与静态资源的请求,动态资源交给ServletProcessor1处理,静态资源交给StaticResourceProcessor处理。
接下来看看具体的代码
启动类HttpServer1
package ex02.hml.one;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;public class HttpServer1 {// 声明一个结束标识,用来判断是否需要终止服务public static boolean shutDown = false;public static void main(String[] args) {try {ServerSocket serverSocket = new ServerSocket(8080, 10, InetAddress.getByName("127.0.0.1"));Socket socket;while (!shutDown) {socket = serverSocket.accept();InputStream inputStream = socket.getInputStream();Request request = new Request();request.parse(inputStream);OutputStream outputStream = socket.getOutputStream();Response response = new Response(request, outputStream);if (request.getUri().startsWith("/servlet/")) {ServletProcessor1 servletProcessor1 = new ServletProcessor1();servletProcessor1.process(request, response);} else {StaticResourceProcessor staticResourceProcessor = new StaticResourceProcessor();staticResourceProcessor.process(request, response);}inputStream.close();socket.close();// 判断是否是结束服务的请求shutDown = request.getUri().equals("/shutdown");}} catch (IOException e) {e.printStackTrace();}}}
Request类
因为Servlet接收的request相关参数为ServletRequest,所以第一章的Request类也要实现ServletRequest接口,但是接口的方法暂时都不做实现,都返回空。原第一章的代码仍然保留。
package ex02.hml.one;import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;public class Request implements ServletRequest {private String uri;public void parse(InputStream inputStream) {try {byte[] bytes = new byte[1024];int readLenth = inputStream.read(bytes);String content = "";while (readLenth != -1) {content += new String(bytes);if (readLenth < 1024) {break;}readLenth = inputStream.read(bytes);}System.out.println("request body -------------->");System.out.println(content);//获取请求内容中第一个空格和第二个空格之间的内容setUri(content);} catch (IOException e) {e.printStackTrace();}}private void setUri(String content) {int index1 = content.indexOf(" ");if (index1 == -1) {return;}int index2 = content.indexOf(" ", index1 + 1);String substring = content.substring(index1 + 1, index2);this.uri = substring;}public String getUri() {return uri;}/*下面是实现 ServletRequest 接口的方法 */@Overridepublic Object getAttribute(String s) {return null;}@Overridepublic Enumeration getAttributeNames() {return null;}@Overridepublic String getCharacterEncoding() {return null;}@Overridepublic void setCharacterEncoding(String s) throws UnsupportedEncodingException {}@Overridepublic int getContentLength() {return 0;}@Overridepublic String getContentType() {return null;}@Overridepublic ServletInputStream getInputStream() throws IOException {return null;}@Overridepublic String getParameter(String s) {return null;}@Overridepublic Enumeration getParameterNames() {return null;}@Overridepublic String[] getParameterValues(String s) {return new String[0];}@Overridepublic Map getParameterMap() {return null;}@Overridepublic String getProtocol() {return null;}@Overridepublic String getScheme() {return null;}@Overridepublic String getServerName() {return null;}@Overridepublic int getServerPort() {return 0;}@Overridepublic BufferedReader getReader() throws IOException {return null;}@Overridepublic String getRemoteAddr() {return null;}@Overridepublic String getRemoteHost() {return null;}@Overridepublic void setAttribute(String s, Object o) {}@Overridepublic void removeAttribute(String s) {}@Overridepublic Locale getLocale() {return null;}@Overridepublic Enumeration getLocales() {return null;}@Overridepublic boolean isSecure() {return false;}@Overridepublic RequestDispatcher getRequestDispatcher(String s) {return null;}@Overridepublic String getRealPath(String s) {return null;}
}
Response类
因为Servlet接收的response相关参数为ServletResponse,所以第一章的Response类也要实现ServletResponse接口,接口的实现方法中仅实现一个getWriter方法,其他方法留空。原第一章的代码仍然保留。
package ex02.hml.one;import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.*;
import java.util.Locale;public class Response implements ServletResponse {private Request request;private OutputStream outputStream;private PrintWriter writer;public static final String rootDir = System.getProperty("user.dir") + File.separatorChar + "webroot";public Response(Request request, OutputStream outputStream) {this.request = request;this.outputStream = outputStream;}public void sendStaticResource() {try {if (request.getUri().equals("/shutdown")) {String msg = "HTTP/1.1 200 OK\r\n" +"Content-Type: text/html\r\n" +"Content-Length: 32\r\n" +"\r\n" +"<h1>server already shutdown</h1>";outputStream.write(msg.getBytes());return;}File file = new File(rootDir + request.getUri());if (file.exists()) {FileInputStream fileInputStream = new FileInputStream(file);byte[] bytes = new byte[fileInputStream.available()];fileInputStream.read(bytes);String successMsg = "HTTP/1.1 200 OK\r\n" +"Content-Type: text/html\r\n" +"Content-Length: " + bytes.length + "\r\n" +"\r\n";outputStream.write(successMsg.getBytes());outputStream.write(bytes);fileInputStream.close();} else {String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +"Content-Type: text/html\r\n" +"Content-Length: 23\r\n" +"\r\n" +"<h1>File Not Found</h1>";outputStream.write(errorMessage.getBytes());}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (outputStream != null) {try {outputStream.flush();outputStream.close();} catch (IOException e) {e.printStackTrace();}}}}@Overridepublic String getCharacterEncoding() {return null;}@Overridepublic ServletOutputStream getOutputStream() throws IOException {return null;}@Overridepublic PrintWriter getWriter() throws IOException {writer = new PrintWriter(outputStream, false);return writer;}@Overridepublic void setContentLength(int i) {}@Overridepublic void setContentType(String s) {}@Overridepublic void setBufferSize(int i) {}@Overridepublic int getBufferSize() {return 0;}@Overridepublic void flushBuffer() throws IOException {}@Overridepublic void resetBuffer() {}@Overridepublic boolean isCommitted() {return false;}@Overridepublic void reset() {}@Overridepublic void setLocale(Locale locale) {}@Overridepublic Locale getLocale() {return null;}
}
getWriter() 方法是提供给 servlet 获取 PrintWriter 对象的,PrintWriter对象可以将各种内容写入到Socket 的 OutputStream 中。
代码中使用了这个构造函数:public PrintWriter(OutputStream out, boolean autoFlush) ,第二个参数传true时,调用 println, printf, format 三个方法会自动刷新(flush),我这里的代码不同与书中的,我将它设为了false, 需要刷新输出流时,手动刷新,防止遗漏刷新过程。
StaticResourceProcessor类
这是处理静态资源的类,此类很简单,静态资源的处理逻辑不变,仍然放在Response类中
public class StaticResourceProcessor {public void process(Request request,Response response) {response.sendStaticResource();}
}
ServletProcessor1类
Servlet处理器,或者也可以叫它Servlet分发类,它负责将HTTP请求分发到指定的Servlet中。
process方法中需要创建类加载器并加载需要用到的 servlet ,然后才能使用它 。
package ex02.hml.one;import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;/*** 处理servlet请求 /servlet/servletName**/
public class ServletProcessor1 {public static final String rootDir = System.getProperty("user.dir") + File.separatorChar + "webroot";public void process(Request request, Response response) {try {String uri = request.getUri();String servletName = uri.substring(uri.lastIndexOf("/") + 1);//首先获取类加载器,这里采用URLClassLoaderFile file = new File(rootDir);String repository = (new URL("file", null, file.getCanonicalPath() + File.separator)).toString();URL[] urls = new URL[1];urls[0] = new URL(null, repository);URLClassLoader urlClassLoader = new URLClassLoader(urls);//加载servlet对应的类Class<?> aClass = urlClassLoader.loadClass(servletName);Servlet servlet = (Servlet) aClass.newInstance();// 调用servlet的service方法,处理请求并返回结果servlet.service(request, response);} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException |ServletException e) {e.printStackTrace();}}
}
PrimitiveServlet类
这就是一个具体的 servlet 了,实现了 Servlet 接口,这里仅仅实现了 service 一个方法,其他方法留空。
这个servlet类没有放在项目的src目录下,而是放在了webroot目录下,也就是说它不属于Tomcat的源码范围。使用过Tomcat部署项目的同学应该了解,servlet应该是我们自己根据各个项目的业务写出来的,项目打包后放在Tomcat的webapps目录下,Tomcat启动后自动扫描webapps目录下的项目,所以PrimitiveServlet也暂时放到一个非源码目录下。
PrimitiveServlet_origin.java是原书中的PrimitiveServlet类的源码,我给原书中的类都加了一个“_origin”后缀,但是里面的类名没改,所以不能编译,仅做对比参考。
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;public class PrimitiveServlet implements Servlet {public void init(ServletConfig config) throws ServletException {System.out.println("init");}public void service(ServletRequest request, ServletResponse response)throws ServletException, IOException {System.out.println("from service");PrintWriter out = response.getWriter();String body = "This is the response data。";String header = "HTTP/1.1 200 OK\r\n" +"Content-Type: text/html\r\n" +"Content-Length: " + body.length() + "\r\n" +"\r\n";out.print(header);out.print(body);out.flush();out.close();}public void service_chunked(ServletRequest request, ServletResponse response)throws ServletException, IOException {System.out.println("from service");PrintWriter out = response.getWriter();String msg = "HTTP/1.1 200 OK\r\n" +"Content-Type: text/plain\r\n" +"Transfer-Encoding: chunked\r\n" +"\r\n";out.print(msg);String msg1 = "This is the data in the first chunk。\r\n";out.print(Integer.toHexString(msg1.length()) + "\r\n");out.print(msg1 + "\r\n");String msg2 = "This is the data in the second chunk。\r\n";out.print(Integer.toHexString(msg2.length()) + "\r\n");out.print(msg2 + "\r\n");out.print("0\r\n\r\n");out.flush();out.close();}public void destroy() {System.out.println("destroy");}public String getServletInfo() {return null;}public ServletConfig getServletConfig() {return null;}}
service方法我提供了两种实现,区别是在HTTP返回头中指定数据长度的方式不同,一种是使用 Content-Length 声明返回body体的总长度;另一种是使用 Transfer-Encoding: chunked 的方式分块声明每块数据的长度。两种方式皆可成功返回数据。
使用Content-Length方式的话,需要提前知道完整body体后才可拼写HTTP Header。
使用Transfer-Encoding: chunked 的话,不需要在Header中指定长度,只要在body体中声明每块数据的长度即可。
在HTTP返回格式的定义上也有些许不同
使用Content-Length时
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
\r\n
Hello World
使用 Transfer-Encoding: chunked 时
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
\r\n
内容1长度(16进制数)\r\n
内容1\r\n
内容2长度(16进制数)\r\n
内容2\r\n
0\r\n\r\n (结尾必须是0和空内容,组合起来就是 0\r\n\r\n)
编译PrimitiveServlet类
在项目的webroot目录下有个complie.sh文件,里面写有可以编译PrimitiveServlet类的脚本,脚本如下
javac -cp "../lib/*" PrimitiveServlet.java
最终效果展示
启动HttpServer1,浏览器访问效果展示
至此,一个非常简易的servlet容器就完成了。但是这个程序也有一点问题,那就是ServletProcessor1在调用具体servlet的service方法时,将ex02.hml.one.Request与ex02.hml.one.Response对象传了过去,虽然PrimitiveServlet的service方法的入参类型是ServletRequest与ServletResponse,但是了解程序设计的人仍然能将它们向下转型为Request与Response对象,并调用其public方法,如Requst的 parse 方法与Response的sendStaticResource方法,这是违反我们的设计初衷的。怎么解决呢?使用外观类。借用一下原书中的UML图
从这两个外观类开始,下面进行本章第二个应用程序的设计
第二个应用程序
RequestFacade与ResponseFacade
RequestFacade类实现了ServletRequest接口并持有一个ServletRequest对象的引用->request,这个request属性用来存放我们的ex02.hml.two.Request对象。RequestFacade类中实现的ServletRequest接口方法的具体实现都调用request属性的对应实现方法。这样下来,RequestFacade类中就没有暴漏多余的方法了。即使servlet程序员将ServletRequest对象向下转型为RequestFacade对象,也访问不了Request类中的parse、getUri方法了。
ResponseFacade同理,实现了ServletResponse接口并持有一个ServletResponse对象的引用->response,这个response属性用来存放我们的ex02.hml.two.Response对象。
package ex02.hml.two;import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;public class RequestFacade implements ServletRequest {private ServletRequest request;public RequestFacade(Request request) {this.request = request;}@Overridepublic Object getAttribute(String s) {return null;}@Overridepublic Enumeration getAttributeNames() {return null;}@Overridepublic String getCharacterEncoding() {return null;}@Overridepublic void setCharacterEncoding(String s) throws UnsupportedEncodingException {}@Overridepublic int getContentLength() {return 0;}@Overridepublic String getContentType() {return null;}@Overridepublic ServletInputStream getInputStream() throws IOException {return null;}@Overridepublic String getParameter(String s) {return null;}@Overridepublic Enumeration getParameterNames() {return null;}@Overridepublic String[] getParameterValues(String s) {return new String[0];}@Overridepublic Map getParameterMap() {return null;}@Overridepublic String getProtocol() {return null;}@Overridepublic String getScheme() {return null;}@Overridepublic String getServerName() {return null;}@Overridepublic int getServerPort() {return 0;}@Overridepublic BufferedReader getReader() throws IOException {return null;}@Overridepublic String getRemoteAddr() {return null;}@Overridepublic String getRemoteHost() {return null;}@Overridepublic void setAttribute(String s, Object o) {}@Overridepublic void removeAttribute(String s) {}@Overridepublic Locale getLocale() {return null;}@Overridepublic Enumeration getLocales() {return null;}@Overridepublic boolean isSecure() {return false;}@Overridepublic RequestDispatcher getRequestDispatcher(String s) {return null;}@Overridepublic String getRealPath(String s) {return null;}
}
package ex02.hml.two;import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;public class ResponseFacade implements ServletResponse {private ServletResponse response;public ResponseFacade(Response response) {this.response = response;}@Overridepublic String getCharacterEncoding() {return null;}@Overridepublic ServletOutputStream getOutputStream() throws IOException {return null;}@Overridepublic PrintWriter getWriter() throws IOException {return response.getWriter();}@Overridepublic void setContentLength(int i) {}@Overridepublic void setContentType(String s) {}@Overridepublic void setBufferSize(int i) {}@Overridepublic int getBufferSize() {return 0;}@Overridepublic void flushBuffer() throws IOException {}@Overridepublic void resetBuffer() {}@Overridepublic boolean isCommitted() {return false;}@Overridepublic void reset() {}@Overridepublic void setLocale(Locale locale) {}@Overridepublic Locale getLocale() {return null;}
}
HttpServer2类
HttpServer2类与HttpServer1类相似,只是在main方法中会使用ServletProcessor2类,而不是ServletProcessor1类
package ex02.hml.two;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;public class HttpServer2 {public static boolean shutDown = false;public static void main(String[] args) {try {ServerSocket serverSocket = new ServerSocket(8080, 10, InetAddress.getByName("127.0.0.1"));Socket socket;while (!shutDown) {socket = serverSocket.accept();InputStream inputStream = socket.getInputStream();Request request = new Request();request.parse(inputStream);OutputStream outputStream = socket.getOutputStream();Response response = new Response(request, outputStream);if (request.getUri().startsWith("/servlet/")) {ServletProcessor2 servletProcessor2 = new ServletProcessor2();servletProcessor2.processor(request, response);} else {StaticResourceProcessor staticResourceProcessor = new StaticResourceProcessor();staticResourceProcessor.process(request, response);}inputStream.close();socket.close();shutDown = request.getUri().equals("/shutdown");}} catch (IOException e) {e.printStackTrace();}}}
ServletProcessor2类
ServletProcessor2类与ServletProcessor1类相似,只是在process方法中有一点不同
package ex02.hml.two;import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;/*** 处理servlet请求 /servlet/servletName*/
public class ServletProcessor2 {public static final String rootDir = System.getProperty("user.dir") + File.separatorChar + "webroot";public void processor(Request request, Response response) {try {String uri = request.getUri();String servletName = uri.substring(uri.lastIndexOf("/") + 1);//首先获取类加载器File file = new File(rootDir);String repository = (new URL("file", null, file.getCanonicalPath() + File.separator)).toString();URL[] urls = new URL[1];urls[0] = new URL(null, repository);URLClassLoader urlClassLoader = new URLClassLoader(urls);//加载servlet对应的类Class<?> aClass = urlClassLoader.loadClass(servletName);Servlet servlet = (Servlet) aClass.newInstance();RequestFacade requestFacade = new RequestFacade(request);ResponseFacade responseFacade = new ResponseFacade(response);servlet.service(requestFacade, responseFacade);} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException |ServletException e) {e.printStackTrace();}}
}
使用HttpServer2启动服务后的浏览器访问结果与HttpServer1相同,就不再贴图了。
好了一个稍加改造后的servlet容器就完成了,本章内容就到这里,下一章我们来实现一个精简版的连接器,敬请期待!
源码分享
https://gitee.com/huo-ming-lu/HowTomcatWorks
本章代码我按照两个应用程序分别放在了 one、two 两个包下