Skip to content

11 多层容器:如何通过实现Context与Wrapper形成多层容器?

你好,我是郭屹。今天我们继续手写MiniTomcat。

上一节课结束后,我们引入了Container对Servlet进行管理,将原本的Connector功能职责进行拆分,让它专门负责通信的管理。并且在第二个部分中,把Container进一步封装成Wrapper,实现Servlet更加精确、完善的管理。

事实上,Tomcat把Wrapper也看作一种容器,也就是隶属于Context之下的子容器(Child Container),所以在原理上是存在多层容器的。一个Server对外提供HTTP服务,它的内部支持管理多个虚拟主机,而每个虚拟主机下又有多个应用,在每个应用内又包含多个Servlet。因此Container存在多个,属于层层嵌套的关系。

图片

按照Tomcat官方的定义,自外向内分别分为Engine层、Host层、Context层与Wrapper层。我们也参考这个思路,把ServletContainer改成Context,但是我们不打算实现Engine和Host,只用两层Container。

不考虑使用这么多层Container的主要原因在于,Engine与Host本身的结构复杂,而且其思想已经不再符合现在的主流,现在我们使用了容器技术之后,Engine和Host的概念已经弱化很多了。实际上,当我们部署的时候,一个Tomcat一般就只用一个Engine和一个Host,如果需要多个,就用多个容器。用Context和Wrapper两层容器也可以明白地说明Tomcat的多层容器的概念。

实现了这些功能之后,我们的MiniTomcat也变得有模有样了。但是如果所有的类全部都放在Server包下,显然是不合适的,所以我们还会参考实际的Tomcat项目结构,把各部分代码文件分门别类地整理好。

接下来我们一起来动手实现。

项目结构

这节课的项目结构中我们新增Container接口和ContainerBase两个文件,把原来的ServletContainer改名为ServletContext,其他的暂时没有什么变化。

MiniTomcat
├─ src
│  ├─ main
│  │  ├─ java
│  │  │  ├─ server
│  │  │  │  ├─ Container.java
│  │  │  │  ├─ ContainerBase.java
│  │  │  │  ├─ CookieTools.java
│  │  │  │  ├─ DefaultHeaders.java
│  │  │  │  ├─ HttpConnector.java
│  │  │  │  ├─ HttpHeader.java
│  │  │  │  ├─ HttpProcessor.java
│  │  │  │  ├─ HttpRequest.java
│  │  │  │  ├─ HttpRequestFacade.java
│  │  │  │  ├─ HttpRequestLine.java
│  │  │  │  ├─ HttpResponse.java
│  │  │  │  ├─ HttpResponseFacade.java
│  │  │  │  ├─ HttpServer.java
│  │  │  │  ├─ Request.java
│  │  │  │  ├─ Response.java
│  │  │  │  ├─ ServletContext.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

Context结构改造

基于之前的积累,我们先进行一层抽象,定义一个Container类。

package server;
public interface Container {
    public static final String ADD_CHILD_EVENT = "addChild";
    public static final String REMOVE_CHILD_EVENT = "removeChild";
    public String getInfo();
    public ClassLoader getLoader();
    public void setLoader(ClassLoader loader);
    public String getName();
    public void setName(String name);
    public Container getParent();
    public void setParent(Container container);
    public void addChild(Container child);
    public Container findChild(String name);
    public Container[] findChildren();
    public void invoke(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException;
    public void removeChild(Container child);
}

可以看到有Classloader的操作方法、Child和Parent的操作方法,还有invoke等基础方法。

因为存在多层Container,很多特性是共有的,所以我们再定义ContainerBase作为基础类,你可以看一下ContainerBase的定义。

package server;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public abstract class ContainerBase implements Container {
    //子容器
    protected Map<String, Container> children = new ConcurrentHashMap<>();
    //类加载器
    protected ClassLoader loader = null;
    protected String name = null;
    //父容器
    protected Container parent = null;

    //下面是基本的get和set方法
    public abstract String getInfo();
    public ClassLoader getLoader() {
        if (loader != null)
            return (loader);
        if (parent != null)
            return (parent.getLoader());
        return (null);
    }
    public synchronized void setLoader(ClassLoader loader) {
        ClassLoader oldLoader = this.loader;
        if (oldLoader == loader) {
            return;
        }
        this.loader = loader;
    }
    public String getName() {
        return (name);
    }

    public void setName(String name) {
        this.name = name;
    }
    public Container getParent() {
        return (parent);
    }

    public void setParent(Container container) {
        Container oldParent = this.parent;
        this.parent = container;
    }

    //下面是对children map的增删改查操作
    public void addChild(Container child) {
        addChildInternal(child);
    }
    private void addChildInternal(Container child) {
        synchronized(children) {
            if (children.get(child.getName()) != null)
                throw new IllegalArgumentException("addChild:  Child name '" +
                        child.getName() +
                        "' is not unique");
            child.setParent((Container) this);
            children.put(child.getName(), child);
        }
    }
    public Container findChild(String name) {
        if (name == null)
            return (null);
        synchronized (children) {       // Required by post-start changes
            return ((Container) children.get(name));
        }
    }

    public Container[] findChildren() {
        synchronized (children) {
            Container results[] = new Container[children.size()];
            return ((Container[]) children.values().toArray(results));
        }
    }
    public void removeChild(Container child) {
        synchronized(children) {
            if (children.get(child.getName()) == null)
                return;
            children.remove(child.getName());
        }
        child.setParent(null);
    }
}

通过上面这段代码,我们实现了Container接口,提供了部分方法的通用实现。

接下来要做的,就是把ServletContainer更名为ServletContext,我们需要改动几处内容。

第一处:HttpServer.java

public class HttpServer {
    public static final String WEB_ROOT =
            System.getProperty("user.dir") + File.separator + "webroot";
    public static void main(String[] args) {
        HttpConnector connector = new HttpConnector();
        ServletContext container = new ServletContext();
        connector.setContainer(container);
        container.setConnector(connector);
        connector.start();
    }
}

这里的Container替换为ServletContext类了。

第二处:HttpConnector.java

public class HttpConnector implements Runnable {
    ServletContext container = null;
    public ServletContext getContainer() {
        return container;
    }
    public void setContainer(ServletContext container) {
        this.container = container;
    }
}

第三处:ServletWrapper.java

public class ServletWrapper extends ContainerBase{
    private Servlet instance = null;
    private String servletClass;
    public ServletWrapper(String servletClass,ServletContext parent) {
        this.parent = parent;
        this.servletClass = servletClass;
        try {
            loadServlet();
        } catch (ServletException e) {
            e.printStackTrace();
        }
    }
}

ServletContext是Wrapper的parent。

调整完类名之后,我们让ServletContext继承ContainerBase基类,ServletWrapper也可以算作Container,所以也继承ContainerBase基类。

首先是ServletContext.java,你可以看一下我们调整的部分。

package server;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ServletContext extends ContainerBase{
    //与本容器关联的connector
    HttpConnector connector = null;
    //内部管理的servlet类和实例
    Map<String,String> servletClsMap = new ConcurrentHashMap<>(); //servletName - ServletClassName
    Map<String,ServletWrapper> servletInstanceMap = new ConcurrentHashMap<>();//servletName - servletWrapper
    public ServletContext() {
        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 "Minit Servlet Context, vesion 0.1";
    }
    public HttpConnector getConnector() {
        return connector;
    }
    public void setConnector(HttpConnector connector) {
        this.connector = connector;
    }
    //调用servlet的方法
    public void invoke(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        ServletWrapper servletWrapper = null;
        String uri = ((HttpRequest)request).getUri();
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);
        String servletClassName = servletName;
        //从容器中获取servlet wrapper
        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);
        }
        //将调用传递到下层容器即wrapper中
        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());
        }
    }
}

上述代码中,HttpRequestFacade和HttpResponseFacade两个类的构造函数的入参和invoke方法保持一致,也需要对应地做一些调整。

package server;
public class HttpRequestFacade implements HttpServletRequest {
    public HttpRequestFacade(HttpServletRequest request) {
        this.request = request;
    }
}
package server;
public class HttpResponseFacade implements HttpServletResponse {
    public HttpResponseFacade(HttpServletResponse response) {
        this.response = response;
    }
}

接下来我们关注一下ServletWrapper类的调整。

package server;
public class ServletWrapper extends ContainerBase{
    //wrapper内含了一个servlet实例和类
    private Servlet instance = null;
    private String servletClass;

    public ServletWrapper(String servletClass,ServletContext parent) {
        //以ServletContext为parent
        this.parent = parent;
        this.servletClass = servletClass;
        try {
            loadServlet();
        } catch (ServletException e) {
            e.printStackTrace();
        }
    }
    public String getServletClass() {
        return servletClass;
    }
    public void setServletClass(String servletClass) {
        this.servletClass = servletClass;
    }
    public Servlet getServlet(){
        return this.instance;
    }
    //load servlet类,创建新实例,并调用init()方法
    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;
    }
    //wrapper是最底层容器,调用将转化为service()方法
    public void invoke(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        if (instance != null) {
            instance.service(request, response);
        }
    }
    @Override
    public String getInfo() {
        return "Minit Servlet Wrapper, version 0.1";
    }
    public void addChild(Container child) {}
    public Container findChild(String name) {return null;}
    public Container[] findChildren() {return null;}
    public void removeChild(Container child) {}
}

ServletWrapper继承了ContainerBase抽象类,主要有两个变化。

  1. 原本定义的loader、name、parent域直接使用ContainerBase里的定义。
  2. 实现getInfo、addChild、findChild、findChildren、removeChild方法。

到这里我们就改造完了。

向Tomcat目录对齐

在这一部分我们开始参考Tomcat的目录结构,来梳理MiniTomcat的程序结构。在Tomcat的项目结构中,主要的类都放在org.apache.catalina包里,基本的子包有startup、core、connector、loader、logger、session和util等等。

我们也参考这个结构,把大的包命名为com.minit,在这个包下构建startup、core、connector、loader、logger、session、util多个子包。

为了更加规范,我们在com.minit包下新增几个接口:Connector、Context、Wrapper、Request、Response、Session、Container。其中Container直接复用之前定义的同名接口,原本定义的Request与Response两个类不再需要使用,可以直接删除。

同时,修改下面这些类的名字并实现上述接口,尽可能和Tomcat保持一致。

ServletContext改为StandardContext
ServletWrapper改为StandardWrapper
Session改为StandardSession
SessionFacade改为StandardSessionFacade
HttpRequest改为HttpRequestImpl
HttpResponse改为HttpResponseImpl
HttpServer改为Bootstrap

改造后的项目结构如下:

MiniTomcat
├─ src
│  ├─ main
│  │  ├─ java
│  │  │  ├─ com
│  │  │  │  ├─ minit
│  │  │  │  │  ├─ connector
│  │  │  │  │  │  ├─ http
│  │  │  │  │  │  │  ├─ DefaultHeaders.java
│  │  │  │  │  │  │  ├─ HttpConnector.java
│  │  │  │  │  │  │  ├─ HttpHeader.java
│  │  │  │  │  │  │  ├─ HttpProcessor.java
│  │  │  │  │  │  │  ├─ HttpRequestImpl.java
│  │  │  │  │  │  │  ├─ HttpRequestLine.java
│  │  │  │  │  │  │  ├─ HttpResponseImpl.java
│  │  │  │  │  │  │  ├─ ServletProcessor.java
│  │  │  │  │  │  │  ├─ SocketInputStream.java
│  │  │  │  │  │  │  ├─ StatisResourceProcessor.java
│  │  │  │  │  │  ├─ HttpRequestFacade.java
│  │  │  │  │  │  ├─ HttpResponseFacade.java
│  │  │  │  │  ├─ core
│  │  │  │  │  │  ├─ ContainerBase.java
│  │  │  │  │  │  ├─ StandardContext.java
│  │  │  │  │  │  ├─ StandardWrapper.java
│  │  │  │  │  ├─ logger
│  │  │  │  │  ├─ session
│  │  │  │  │  │  ├─ StandardSession.java
│  │  │  │  │  │  ├─ StandardSessionFacade.java
│  │  │  │  │  ├─ startup
│  │  │  │  │  │  ├─ Bootstrap.java
│  │  │  │  │  ├─ util
│  │  │  │  │  │  ├─ CookieTools.java
│  │  │  │  ├─ Connector.java
│  │  │  │  ├─ Container.java
│  │  │  │  ├─ Contexts.java
│  │  │  │  ├─ Request.java
│  │  │  │  ├─ Responses.java
│  │  │  │  ├─ Session.java
│  │  │  │  ├─ Wrapper.java
│  │  ├─ resources
│  ├─ test
│  │  ├─ java
│  │  │  ├─ test
│  │  │  │  ├─ HelloServlet.java
│  │  │  │  ├─ TestServlet.java
│  │  ├─ resources
├─ webroot
│  ├─ test
│  │  ├─ HelloServlet.class
│  │  ├─ TestServlet.class
│  ├─ hello.txt
├─ pom.xml

接下来我们分别定义Connector、Context、Wrapper、Request、Response、Session这几个接口。

Connector.java:

package com.minit;
public interface Connector {
    public Container getContainer();
    public void setContainer(Container container);
    public String getInfo();
    public String getScheme();
    public void setScheme(String scheme);
    public Request createRequest();
    public Response createResponse();
    public void initialize();
}

Context.java:

package com.minit;
public interface Context extends Container {
    public static final String RELOAD_EVENT = "reload";
    public String getDisplayName();
    public void setDisplayName(String displayName);
    public String getDocBase();
    public void setDocBase(String docBase);
    public String getPath();
    public void setPath(String path);
    public ServletContext getServletContext();
    public int getSessionTimeout();
    public void setSessionTimeout(int timeout);
    public String getWrapperClass();
    public void setWrapperClass(String wrapperClass);
    public Wrapper createWrapper();
    public String findServletMapping(String pattern);
    public String[] findServletMappings();
    public void reload();
}

Wrapper.java:

package com.minit;
public interface Wrapper {
    public int getLoadOnStartup();
    public void setLoadOnStartup(int value);
    public String getServletClass();
    public void setServletClass(String servletClass);
    public void addInitParameter(String name, String value);
    public Servlet allocate() throws ServletException;
    public String findInitParameter(String name);
    public String[] findInitParameters();
    public void load() throws ServletException;
    public void removeInitParameter(String name);
}

Request.java:

package com.minit;
public interface Request {
    public Connector getConnector();
    public void setConnector(Connector connector);
    public Context getContext();
    public void setContext(Context context);
    public String getInfo();
    public ServletRequest getRequest();
    public Response getResponse();
    public void setResponse(Response response);
    public Socket getSocket();
    public void setSocket(Socket socket);
    public InputStream getStream();
    public void setStream(InputStream stream);
    public Wrapper getWrapper();
    public void setWrapper(Wrapper wrapper);
    public ServletInputStream createInputStream() throws IOException;
    public void finishRequest() throws IOException;
    public void recycle();
    public void setContentLength(int length);
    public void setContentType(String type);
    public void setProtocol(String protocol);
    public void setRemoteAddr(String remote);
    public void setScheme(String scheme);
    public void setServerPort(int port);
}

Response.java:

package com.minit;
public interface Response {
    public Connector getConnector();
    public void setConnector(Connector connector);
    public int getContentCount();
    public Context getContext();
    public void setContext(Context context);
    public String getInfo();
    public Request getRequest();
    public void setRequest(Request request);
    public ServletResponse getResponse();
    public OutputStream getStream();
    public void setStream(OutputStream stream);
    public void setError();
    public boolean isError();
    public ServletOutputStream createOutputStream() throws IOException;
    public void finishResponse() throws IOException;
    public int getContentLength();
    public String getContentType();
    public PrintWriter getReporter();
    public void recycle();
    public void resetBuffer();
    public void sendAcknowledgement() throws IOException;
}

Session.java:

package com.minit;
public interface Session {
    public static final String SESSION_CREATED_EVENT = "createSession";
    public static final String SESSION_DESTROYED_EVENT = "destroySession";
    public long getCreationTime();
    public void setCreationTime(long time);
    public String getId();
    public void setId(String id);
    public String getInfo();
    public long getLastAccessedTime();
    public int getMaxInactiveInterval();
    public void setMaxInactiveInterval(int interval);
    public void setNew(boolean isNew);
    public HttpSession getSession();
    public void setValid(boolean isValid);
    public boolean isValid();
    public void access();
    public void expire();
    public void recycle();
}

最后再给StandardContext、StandardWrapper和StandardSession分别实现Context、Wrapper与Session接口,这节课的改造就实现完了。最后我们再来看一下调整后需要新增的实现方法。

StandardContext.java:

package com.minit.core;
public class StandardContext extends ContainerBase implements Context {
    @Override
    public String getDisplayName() {
        return null;
    }
    @Override
    public void setDisplayName(String displayName) {
    }
    @Override
    public String getDocBase() {
        return null;
    }
    @Override
    public void setDocBase(String docBase) {
    }
    @Override
    public String getPath() {
        return null;
    }
    @Override
    public void setPath(String path) {
    }
    @Override
    public ServletContext getServletContext() {
        return null;
    }
    @Override
    public int getSessionTimeout() {
        return 0;
    }
    @Override
    public void setSessionTimeout(int timeout) {
    }
    @Override
    public String getWrapperClass() {
        return null;
    }
    @Override
    public void setWrapperClass(String wrapperClass) {
    }
    @Override
    public Wrapper createWrapper() {
        return null;
    }
    @Override
    public String findServletMapping(String pattern) {
        return null;
    }
    @Override
    public String[] findServletMappings() {
        return null;
    }
    @Override
    public void reload() {
    }
}

StandardWrapper.java:

package com.minit.core;
public class StandardWrapper extends ContainerBase implements Wrapper {
   @Override
    public int getLoadOnStartup() {
        return 0;
    }
    @Override
    public void setLoadOnStartup(int value) {
    }
    @Override
    public void addInitParameter(String name, String value) {
    }
    @Override
    public Servlet allocate() throws ServletException {
        return null;
    }
    @Override
    public String findInitParameter(String name) {
        return null;
    }
    @Override
    public String[] findInitParameters() {
        return null;
    }
    @Override
    public void load() throws ServletException {
    }
    @Override
    public void removeInitParameter(String name) {
    }
}

StandardSession.java:

package com.minit.session;
public class StandardSession implements HttpSession, Session {
   @Override
    public String getInfo() {
        return null;
    }
    @Override
    public void setNew(boolean isNew) {
    }
    @Override
    public HttpSession getSession() {
        return null;
    }
    @Override
    public boolean isValid() {
        return false;
    }
    @Override
    public void access() {
    }
    @Override
    public void expire() {
    }
    @Override
    public void recycle() {
    }
}

到这里我们就完成了项目结构的改造,可以看出,MiniTomcat和Tomcat已经长得比较像了。

测试

这节课没有新增什么对外的功能,所以测试还是和之前的测试方式一样。

小结

这节课我们把项目结构进一步抽象成了两层Container,分别是Context和Wrapper,Context对应于我们平常所说的一个应用,Wrapper是对应的一个Servlet的包装。在Context这个容器中有一个map包含了多个Wrapper,这样构成了父子容器的两层结构。

然后我们进一步通用化,提出ContainerBase,只要一个类基于base,就可以当成一个新的容器。通过这些手段实现了一个服务器管理多个容器,而容器又可以管理多个Servlet,层层嵌套,实现系统结构的扩展和管理清晰化。然后在此基础上,参考Tomcat的项目结构,进行对应调整,让它更贴近Tomcat源码本身。这样一来,你去阅读Tomcat源码,难度就会大大降低。

这节课代码参见: https://gitee.com/yaleguo1/minit-learning-demo/tree/geek_chapter11

思考题

学完了这节课的内容,我们来思考一个问题:我们现在的代码中,servletContext的invoke()方法仅仅只是简单地调用了子容器Wrapper的invoke(),但是原则上每一层的容器的invoke()可以另外加入本层容器特殊的逻辑,有没有合适的设计方案?

欢迎你把你想到的方案分享到评论区,也欢迎你把这节课的内容分享给其他朋友,我们下节课再见!