10 Servlet Wrapper:如何维护Servlet生命周期及实现容器管理?
你好,我是郭屹。今天我们继续手写MiniTomcat。
上节课我们把Request和Response从无状态变成了有状态,实现了Session和Cookie的管理,还实现了同一页面的资源请求复用Socket,减少了性能消耗。
到目前为止,我们已经基本将浏览器与服务器之间的通信处理完毕。接下来我们再看后端服务器,现在我们还是使用ServletProcessor简单地调用Servlet的service方法,接下来我们考虑将其扩展,对Servlet进行管理,这就引入了Container容器的概念。 我们计划让Container和Connector配合在一起工作,前者负责后端Servlet管理,而后者则负责通信管理。
初步构建容器后,我们还会考虑使用Wrapper进行包装,用于维护Servlet的生命周期:初始化、提供服务、销毁这个全过程,把Servlet完全纳入程序自动管理之中,让应用程序员更少地感知到底层的配置,更专注于业务逻辑本身。
接下来我们一起来动手实现。
项目结构
这节课我们新增ServletContainer与ServletWrapper两个类,分别定义Container与Wrapper,你可以看一下现在的程序结构。
MiniTomcat
├─ src
│ ├─ main
│ │ ├─ java
│ │ │ ├─ server
│ │ │ │ ├─ CookieTools.java
│ │ │ │ ├─ DefaultHeaders.java
│ │ │ │ ├─ HttpConnector.java
│ │ │ │ ├─ HttpHeader.java
│ │ │ │ ├─ HttpProcessor.java
│ │ │ │ ├─ HttpRequest.java
│ │ │ │ ├─ HttpRequestFacade.java
│ │ │ │ ├─ HttpRequestLine.java
│ │ │ │ ├─ HttpResponse.java
│ │ │ │ ├─ HttpResponseFacade.java
│ │ │ │ ├─ HttpServer.java
│ │ │ │ ├─ Request.java
│ │ │ │ ├─ ServletContainer.java
│ │ │ │ ├─ Response.java
│ │ │ │ ├─ ServletProcessor.java
│ │ │ │ ├─ ServletWrapper.java
│ │ │ │ ├─ Session.java
│ │ │ │ ├─ SessionFacade.java
│ │ │ │ ├─ SocketInputStream.java
│ │ │ │ ├─ StatisResourceProcessor.java
│ │ ├─ resources
│ ├─ test
│ │ ├─ java
│ │ │ ├─ test
│ │ │ │ ├─ HelloServlet.java
│ │ │ │ ├─ TestServlet.java
│ │ ├─ resources
├─ webroot
│ ├─ test
│ │ ├─ HelloServlet.class
│ │ ├─ TestServlet.class
│ ├─ hello.txt
├─ pom.xml
Container——Servlet管理容器
在改造之前,我们先关注一下整个Server的启动类——HttpServer。目前,我们的启动类是比较简单的,main函数内只有两行。
package server;
public class HttpServer {
public static void main(String[] args) {
HttpConnector connector = new HttpConnector();
connector.start();
}
}
通过代码可以知道,我们Server的起点就是HttpConnector,所以之前对Servlet的管理也全是交由Connector进行处理,不过这并不好,角色混合了。所以接下来我们要做的,就是引入Container容器这个概念,将Servlet管理和网络通信功能一分为二。
首先是定义ServletContainer类。
package server;
//Servlet容器
public class ServletContainer {
HttpConnector connector = null;
ClassLoader loader = null;
//包含servlet类和实例的map
Map<String,String> servletClsMap = new ConcurrentHashMap<>(); //servletName - ServletClassName
Map<String,Servlet> servletInstanceMap = new ConcurrentHashMap<>();//servletName - servlet
public ServletContainer() {
try {
// create a URLClassLoader
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(HttpServer.WEB_ROOT);
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
} catch (IOException e) {
System.out.println(e.toString() );
}
}
public String getInfo() {
return null;
}
public ClassLoader getLoader(){
return this.loader;
}
public void setLoader(ClassLoader loader) {
this.loader = loader;
}
public HttpConnector getConnector() {
return connector;
}
public void setConnector(HttpConnector connector) {
this.connector = connector;
}
public String getName() {
return null;
}
public void setName(String name) {
}
//invoke方法用于从map中找到相关的servlet,然后调用
public void invoke(HttpRequest request, HttpResponse response)
throws IOException, ServletException {
Servlet servlet = null;
ClassLoader loader = getLoader();
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
String servletClassName = servletName;
servlet = servletInstanceMap.get(servletName);
//如果容器内没有这个servlet,先要load类,创建新实例
if (servlet == null) {
Class<?> servletClass = null;
try {
servletClass = loader.loadClass(servletClassName);
} catch (ClassNotFoundException e) {
System.out.println(e.toString());
}
try {
servlet = (Servlet) servletClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
servletClsMap.put(servletName, servletClassName);
servletInstanceMap.put(servletName, servlet);
//按照规范,创建新实例的时候需要调用init()
servlet.init(null);
}
//然后调用service()
try {
HttpRequestFacade requestFacade = new HttpRequestFacade(request);
HttpResponseFacade responseFacade = new HttpResponseFacade(response);
System.out.println("Call service()");
servlet.service(requestFacade, responseFacade);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
}
}
}
从ServletContainer的代码里,我们又看到了熟悉的面孔——ClassLoader,此前将ClassLoader直接交由HttpConnector管理,定义了域。
现在进行改造,用新创建的ServletContainer类管理ClassLoader,并提供对应的 getLoader()
和 setLoader()
方法,同时也将原来在ServletProcessor内调用Servlet的代码挪到ServletContainer的 invoke()
方法中。
之前在调用 invoke()
方法时,每次都是加载Servlet的类进行实例化,并调用service方法,在这里我们进一步把Servlet放到Map中存起来,包含多Servlet实例,其中servletClsMap用于存储Servlet名称与Servlet类名的映射关系,而servletInstanceMap用于存储Servlet名称与具体Servlet对象的映射关系。
这样改造后,当 invoke()
方法被调用时,如果有Servlet实例,就直接调用 service()
,如果没有实例,就加载并创建实例,并调用 init()
进行初始化工作。
现在ServletProcessor可以尽可能地简化了,你可以看一下简化后的代码。
package server;
public class ServletProcessor {
private HttpConnector connector;
public ServletProcessor(HttpConnector connector) {
this.connector = connector;
}
public void process(HttpRequest request, HttpResponse response) throws IOException, ServletException {
this.connector.getContainer().invoke(request, response);
}
}
在ServletProcessor中我们定义了传入Connector的构造函数,所以在Processor代码中,需要调整初始化Processor的代码。
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor processor = new ServletProcessor(this.connector);
processor.process(request, response);
}
接下来,我们再转向HttpConnector。在定义了Container之后,自然地,要把Container和Connector结合起来,我们在HttpConnector中改一下代码。
package server;
public class HttpConnector implements Runnable {
int minProcessors = 3;
int maxProcessors = 10;
int curProcessors = 0;
Deque<HttpProcessor> processors = new ArrayDeque<>();
public static Map<String, HttpSession> sessions = new ConcurrentHashMap<>();
//这是与connector相关联的container
ServletContainer container = null;
public void run() {
ServerSocket serverSocket = null;
int port = 8080;
try {
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
// initialize processors pool
for (int i = 0; i < minProcessors; i++) {
HttpProcessor initprocessor = new HttpProcessor(this);
initprocessor.start();
processors.push(initprocessor);
}
curProcessors = minProcessors;
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();
HttpProcessor processor = createProcessor();
if (processor == null) {
socket.close();
continue;
}
processor.assign(socket);
// Close the socket
// socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void start() {
Thread thread = new Thread(this);
thread.start();
}
public ServletContainer getContainer() {
return container;
}
public void setContainer(ServletContainer container) {
this.container = container;
}
}
上述代码中,新增了ServletContainer类型的container属性,添加了对应 getContainer()
与 setContainer()
方法,移除了原本处理Classloader的相关代码。
这个时候,我们就可以调整HttpServer里的代码,拆分功能了。
package server;
public class HttpServer {
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
public static void main(String[] args) {
//创建connector和container
HttpConnector connector = new HttpConnector();
ServletContainer container = new ServletContainer();
//connector和container互相指引
connector.setContainer(container);
container.setConnector(connector);
connector.start();
}
}
这里我们进一步拆分了HttpConnector,做到了ServletContainer管理Servlet,HttpConnector负责通信管理,各司其职。
到这里,我想多说几句。软件结构应该怎么进行拆分?我们可以直观地将一个软件当成一个公司或者一个团体,里面有很多岗位和人,如果在公司里需要将某一个工作专门交由专人负责,就可以设置一个岗位,类比软件结构,就是在软件中新添加一个类,一个类就是一个岗位。这种拟人化的思考方式对我们分析软件结构很有帮助。
Wrapper——增强Servlet管理
刚刚我们已经使用Container实现了Servlet的管理,我们继续关注这一部分, 采用Wrapper包装,用来维护Servlet的生命周期。
为什么需要这么一个Wrapper呢?从功能角度,不引入它也是没有问题的。但是如果没有Wrapper,我们就得在Container这个容器里直接管理Servlet,这相当于在一个大的纸盒子中直接放上很多小玩具,比较繁琐。
所以超市给了我们一个方案:每个小玩具外面套一个包装,比如小盒子或者是塑料袋子,再将这些小盒子或者袋子放在大纸盒中,方便人们拿取。这个Wrapper也是同样的思路。
首先我们来定义ServletWrapper类。
package server;
public class ServletWrapper {
private Servlet instance = null;
private String servletClass;
private ClassLoader loader;
private String name;
protected ServletContainer parent = null;
public ServletWrapper(String servletClass, ServletContainer parent) {
this.parent = parent;
this.servletClass = servletClass;
try {
loadServlet();
} catch (ServletException e) {
e.printStackTrace();
}
}
public ClassLoader getLoader() {
if (loader != null)
return loader;
return parent.getLoader();
}
public String getServletClass() {
return servletClass;
}
public void setServletClass(String servletClass) {
this.servletClass = servletClass;
}
public ServletContainer getParent() {
return parent;
}
public void setParent(ServletContainer container) {
parent = container;
}
public Servlet getServlet(){
return this.instance;
}
public Servlet loadServlet() throws ServletException {
if (instance!=null)
return instance;
Servlet servlet = null;
String actualClass = servletClass;
if (actualClass == null) {
throw new ServletException("servlet class has not been specified");
}
ClassLoader classLoader = getLoader();
Class classClass = null;
try {
if (classLoader!=null) {
classClass = classLoader.loadClass(actualClass);
}
}
catch (ClassNotFoundException e) {
throw new ServletException("Servlet class not found");
}
try {
servlet = (Servlet) classClass.newInstance();
}
catch (Throwable e) {
throw new ServletException("Failed to instantiate servlet");
}
try {
servlet.init(null);
}
catch (Throwable f) {
throw new ServletException("Failed initialize servlet.");
}
instance =servlet;
return servlet;
}
public void invoke(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
if (instance != null) {
instance.service(request, response);
}
}
}
在ServletWrapper类中,核心在于 loadServlet()
方法,主要是通过一个Classloader加载并实例化Servlet,然后调用 init()
方法进行初始化工作,其实也是刚刚我们在ServletContainer中的处理。所以在ServletContainer类里,我们可以进一步改造,将一些对Servlet的处理交给ServletWrapper进行。
首先是servletInstanceMap,Value类型可设置成更高层次的ServletWrapper,你可以看一下修改后的样子。
其次是调整invoke方法,你可以看一下调整后的invoke方法。
public void invoke(HttpRequest request, HttpResponse response)
throws IOException, ServletException {
ServletWrapper servletWrapper = null;
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
String servletClassName = servletName;
servletWrapper = servletInstanceMap.get(servletName);
if ( servletWrapper == null) {
servletWrapper = new ServletWrapper(servletClassName,this);
//servletWrapper.setParent(this);
this.servletClsMap.put(servletName, servletClassName);
this.servletInstanceMap.put(servletName, servletWrapper);
}
try {
HttpServletRequest requestFacade = new HttpRequestFacade(request);
HttpServletResponse responseFacade = new HttpResponseFacade(response);
System.out.println("Call service()");
servletWrapper.invoke(requestFacade, responseFacade);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
}
}
这样在ServletContainer中,只是获取到ServletWrapper的实例,调用ServletWrapper内的 invoke()
方法,进一步进行了解耦。
测试
这节课并没有功能性的变化,所以没有新增测试类,还是和之前的测试方式保持一致。这里就不重复说了。
小结
这节课我们实现了ServletContainer管理全部的Servlet,将原本的管理功能从Connector内抽离,让Connector专注服务器通信管理。这个Container就是我们MiniTomcat初始容器,它里面有一个map,包含了管理的Servlet实例。有了这个Container,Processor就变得简单了,它里面的新方法 process()
只需要调用Container的 invoke()
就可以了。
同时,我们引入ServletWrapper,对ServletContainer做了更进一步的拆分,更加方便对Servlet进行管理。今后,我们会把Container进一步拆成多层。
本节课代码参见: https://gitee.com/yaleguo1/minit-learning-demo/tree/geek_chapter10
思考题
学完了这节课的内容,我们来思考一个问题:现在这个HttpServer仅仅只是创建Connector和Container,只是一个壳子了,按照拟人化的思路,HttpServer应该是一个公司,那么这个类从道理上还应该分工负责做些什么?
欢迎你把你思考后的结果分享到评论区,也欢迎你把这节课的内容分享给其他朋友,我们下节课再见!