现在开始《深入剖析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 接口的方法 */
@Override
public Object getAttribute(String s) {
return null;
}
@Override
public Enumeration getAttributeNames() {
return null;
}
@Override
public String getCharacterEncoding() {
return null;
}
@Override
public void setCharacterEncoding(String s) throws UnsupportedEncodingException {
}
@Override
public int getContentLength() {
return 0;
}
@Override
public String getContentType() {
return null;
}
@Override
public ServletInputStream getInputStream() throws IOException {
return null;
}
@Override
public String getParameter(String s) {
return null;
}
@Override
public Enumeration getParameterNames() {
return null;
}
@Override
public String[] getParameterValues(String s) {
return new String[0];
}
@Override
public Map getParameterMap() {
return null;
}
@Override
public String getProtocol() {
return null;
}
@Override
public String getScheme() {
return null;
}
@Override
public String getServerName() {
return null;
}
@Override
public int getServerPort() {
return 0;
}
@Override
public BufferedReader getReader() throws IOException {
return null;
}
@Override
public String getRemoteAddr() {
return null;
}
@Override
public String getRemoteHost() {
return null;
}
@Override
public void setAttribute(String s, Object o) {
}
@Override
public void removeAttribute(String s) {
}
@Override
public Locale getLocale() {
return null;
}
@Override
public Enumeration getLocales() {
return null;
}
@Override
public boolean isSecure() {
return false;
}
@Override
public RequestDispatcher getRequestDispatcher(String s) {
return null;
}
@Override
public 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();
}
}
}
}
@Override
public String getCharacterEncoding() {
return null;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return null;
}
@Override
public PrintWriter getWriter() throws IOException {
writer = new PrintWriter(outputStream, false);
return writer;
}
@Override
public void setContentLength(int i) {
}
@Override
public void setContentType(String s) {
}
@Override
public void setBufferSize(int i) {
}
@Override
public int getBufferSize() {
return 0;
}
@Override
public void flushBuffer() throws IOException {
}
@Override
public void resetBuffer() {
}
@Override
public boolean isCommitted() {
return false;
}
@Override
public void reset() {
}
@Override
public void setLocale(Locale locale) {
}
@Override
public 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);
//首先获取类加载器,这里采用URLClassLoader
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();
// 调用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;
}
@Override
public Object getAttribute(String s) {
return null;
}
@Override
public Enumeration getAttributeNames() {
return null;
}
@Override
public String getCharacterEncoding() {
return null;
}
@Override
public void setCharacterEncoding(String s) throws UnsupportedEncodingException {
}
@Override
public int getContentLength() {
return 0;
}
@Override
public String getContentType() {
return null;
}
@Override
public ServletInputStream getInputStream() throws IOException {
return null;
}
@Override
public String getParameter(String s) {
return null;
}
@Override
public Enumeration getParameterNames() {
return null;
}
@Override
public String[] getParameterValues(String s) {
return new String[0];
}
@Override
public Map getParameterMap() {
return null;
}
@Override
public String getProtocol() {
return null;
}
@Override
public String getScheme() {
return null;
}
@Override
public String getServerName() {
return null;
}
@Override
public int getServerPort() {
return 0;
}
@Override
public BufferedReader getReader() throws IOException {
return null;
}
@Override
public String getRemoteAddr() {
return null;
}
@Override
public String getRemoteHost() {
return null;
}
@Override
public void setAttribute(String s, Object o) {
}
@Override
public void removeAttribute(String s) {
}
@Override
public Locale getLocale() {
return null;
}
@Override
public Enumeration getLocales() {
return null;
}
@Override
public boolean isSecure() {
return false;
}
@Override
public RequestDispatcher getRequestDispatcher(String s) {
return null;
}
@Override
public 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;
}
@Override
public String getCharacterEncoding() {
return null;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return null;
}
@Override
public PrintWriter getWriter() throws IOException {
return response.getWriter();
}
@Override
public void setContentLength(int i) {
}
@Override
public void setContentType(String s) {
}
@Override
public void setBufferSize(int i) {
}
@Override
public int getBufferSize() {
return 0;
}
@Override
public void flushBuffer() throws IOException {
}
@Override
public void resetBuffer() {
}
@Override
public boolean isCommitted() {
return false;
}
@Override
public void reset() {
}
@Override
public void setLocale(Locale locale) {
}
@Override
public 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 两个包下