手寫迷你版 Tomcat - Minicat
原文連接:https://notes./archives/handwriting-minicat
手寫簡單版本的 tomcat 的意義是,有助于大家對 HTTP 協(xié)議的理解,增加對 tomcat 開源服務器的理解。
本文中對 servlet 的支持比較復雜,大家可以依賴 javax.servlet 來對servlet 程序進行支持,此外可以考慮增加對于長連接的支持和解析。
Minicat 的目標 
我們可以通過瀏覽器客戶端發(fā)送 http 請求, Minicat 可以接收到請求進?處理,處理之后的結(jié)果可以返回瀏覽器客戶端。
基本方向
- 請求信息封裝成 Request 對象,同樣響應信息封裝成 Response 對象
- 客戶端請求資源,資源分為靜態(tài)資源(HTML)和動態(tài)資源(Servlet)
迭代實現(xiàn)
我們實現(xiàn)時候呢,一步一步來,可以制定的小版本計劃
- V1.0 需求:瀏覽器請求 http://localhost:8080, 返回?個固定的字符串到??'Hello Minicat.'
- V2.0 需求:封裝 Request 和 Response 對象,返回 HTML 靜態(tài)資源?件
- V3.0 需求:可以請求動態(tài)資源(Servlet)
- V5.0 需求:在已有 Minicat 基礎上進?步擴展,模擬出 webapps 部署效果,磁盤上放置?個 webapps ?錄,webapps 中可以有多個項?,?如 demo1,demo2,demo3… 具體的項??如 demo1 中有 serlvet(也即為:servlet 是屬于具體某?個項?的 servlet),這樣的話在 Minicat 初始化配置加載,以及根據(jù)請求 url 查找對應 serlvet 時都需要進?步處理。
V1.0 版本 
環(huán)境搭建
確定好方向,就進行項目搭建開發(fā)
新建一個 Maven 項目,并調(diào)整在 pom.xml
  <groupId>site.suremotoo</groupId>
  <artifactId>Minicat</artifactId>
  <version>1.0-SNAPSHOT</version>
  <build>
      <plugins>
          <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>3.1</version>
              <configuration>
                  <source>11</source>
                  <target>11</target>
                  <encoding>utf-8</encoding>
              </configuration>
          </plugin>
      </plugins>
  </build>
編寫啟動類 Bootstrap
public class Bootstrap {
  /**
   * 設定啟動和監(jiān)聽端口
   */
  private int port = 8080;
  /**
   * 啟動函數(shù)
   *
   * @throws IOException
   */
  public void start() throws IOException {
      System.out.println('Minicat starting...');
      String responseData = 'Hello Minicat.';
      ServerSocket socket = new ServerSocket(port);
      while (true) {
          Socket accept = socket.accept();
          OutputStream outputStream = accept.getOutputStream();
          String responseText = HttpProtocolUtil.getHttpHeader200(responseData.length()) + responseData;
          outputStream.write(responseText.getBytes());
          accept.close();
      }
  }
  /**
   * 啟動入口
   *
   * @param args
   */
  public static void main(String[] args) throws IOException {
      Bootstrap bootstrap = new Bootstrap();
      bootstrap.start();
  }
}
HTTP 協(xié)議輔助類 HttpProtocolUtil
public class HttpProtocolUtil {
    /**
     * 200 狀態(tài)碼,頭信息
     *
     * @param contentLength 響應信息長度
     * @return 200 header info
     */
    public static String getHttpHeader200(long contentLength) {
        return 'HTTP/1.1 200 OK \n' + 'Content-Type: text/html \n'
                + 'Content-Length: ' + contentLength + ' \n' + '\r\n';
    }
    /**
     * 為響應碼 404 提供請求頭信息(此處也包含了數(shù)據(jù)內(nèi)容)
     *
     * @return 404 header info
     */
    public static String getHttpHeader404() {
        String str404 = '<h1>404 not found</h1>';
        return 'HTTP/1.1 404 NOT Found \n' + 'Content-Type: text/html \n'
                + 'Content-Length: ' + str404.getBytes().length + ' \n' + '\r\n' + str404;
    }
}
然后我們訪問瀏覽器:http://localhost:8080,頁面顯示 Hello Minicat.,就說明成功啦。
這就完成 V1.0 版本了 ??
V2.0 版本 
緊接著我們實現(xiàn)請求信息、響應信息的封裝,即:Request、Response 的實現(xiàn)
public class Request {
  /**
   * 請求方式, eg: GET、POST
   */
  private String method;
  /**
   * 請求路徑,eg: /index.html
   */
  private String url;
  /**
   * 請求信息輸入流 <br>
   * 示例
   * <pre>
   *  GET / HTTP/1.1
   *  Host: www.baidu.com
   *  Connection: keep-alive
   *  Pragma: no-cache
   *  Cache-Control: no-cache
   *  Upgrade-Insecure-Requests: 1
   *  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
   * </pre>
   */
  private InputStream inputStream;
  public Request() {
  }
  public Request(InputStream inputStream) throws IOException {
      this.inputStream = inputStream;
      int count = 0;
      while (count == 0) {
          count = inputStream.available();
      }
      byte[] bytes = new byte[count];
      inputStream.read(bytes);
      // requestString 參考:this.inputStream 示例
      String requestString = new String(bytes);
      // 按換行分隔
      String[] requestStringArray = requestString.split('\\n');
      // 讀取第一行數(shù)據(jù),即:GET / HTTP/1.1
      String firstLine = requestStringArray[0];
      // 遍歷第一行數(shù)據(jù)按空格分隔
      String[] firstLineArray = firstLine.split(' ');
      this.method = firstLineArray[0];
      this.url = firstLineArray[1];
  }
}
Response
public class Response {
    /**
     * 響應信息
     */
    private OutputStream outputStream;
    public Response() {
    }
    public Response(OutputStream outputStream) {
        this.outputStream = outputStream;
    }
    public void output(String content) throws IOException {
        outputStream.write(content.getBytes());
    }
    /**
     * 根據(jù) url 拼接絕對路徑,根據(jù)絕對路徑再讀取文件資源,再響應
     *
     * @param url 請求 url
     */
    public void outputHtml(String url) throws IOException {
        // 獲取 url 資源的全路徑
        String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(url);
        File file = new File(absoluteResourcePath);
        if (file.exists() && file.isFile()) {
            // 輸出靜態(tài)資源
            StaticResourceUtil.outputStaticResource(new FileInputStream(file), outputStream);
        } else {
            output(HttpProtocolUtil.getHttpHeader404());
        }
    }
}
我們也提取了一些共用類和函數(shù),StaticResourceUtil
public class StaticResourceUtil {
    /**
     * 根據(jù)請求 url 獲取完整絕對路徑
     *
     * @param url 請求 url
     * @return 完整絕對路徑
     */
    public static String getAbsolutePath(String url) {
        String path = StaticResourceUtil.class.getResource('/').getPath();
        return path.replaceAll('\\\\', '/') + url;
    }
    /**
     * 輸出靜態(tài)資源信息
     *
     * @param inputStream  靜態(tài)資源文件的讀取流
     * @param outputStream 響應的輸出流
     * @throws IOException
     */
    public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
        int count = 0;
        while (count == 0) {
            count = inputStream.available();
        }
        // 輸出 http 請求頭,然后再輸出具體內(nèi)容
        int resourceSize = count;
        // 讀取內(nèi)容輸出
        outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
        // 已經(jīng)讀取的內(nèi)容?度
        long written = 0;
        // 計劃每次緩沖的?度
        int byteSize = 1024;
        byte[] bytes = new byte[byteSize];
        while (written < resourceSize) {
            // 說明剩余未讀取??不??個 1024 ?度,那就按真實?度處理
            if (written + byteSize > resourceSize) {
                // 計算實際剩余內(nèi)容
                byteSize = (int) (resourceSize - written);
                bytes = new byte[byteSize];
            }
            inputStream.read(bytes);
            outputStream.write(bytes);
            outputStream.flush();
            // 計算已經(jīng)讀取的長度
            written += byteSize;
        }
    }
}
最后調(diào)整一下Bootstrap中的start()函數(shù)
public void start() throws IOException {
    System.out.println('Minicat starting...');
    ServerSocket socket = new ServerSocket(port);
    while (true) {
        Socket accept = socket.accept();
        OutputStream outputStream = accept.getOutputStream();
        // 分別封裝 Request 和 Response
        Request request = new Request(accept.getInputStream());
        Response response = new Response(outputStream);
        // 根據(jù) request 中的 url,輸出
        response.outputHtml(request.getUrl());
        accept.close();
    }
}
哦對了,還缺少 1 個具體的 HTML 靜態(tài)資源?件,我們創(chuàng)建一個名為 index.html 的吧
<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <title>Hello Minicat</title>
</head>
<body>
    <h1>Hello Minicat,  index.html</h1>
</body>
</html>
然后我們訪問瀏覽器:http://localhost:8080/index.html,頁面顯示 Hello Minicat, index.html,就說明成功啦。
這就完成 V2.0 版本了 ????
V3.0 版本 
接下來就來實現(xiàn)請求動態(tài)資源
我們先定義 Servlet 的接口,指定規(guī)范
public interface Servlet {
    void init() throws Exception;
    void destroy() throws Exception;
    void service(Request request, Response response) throws Exception;
}
緊接著再完成 1 個公共的抽象父類 HttpServlet
public abstract class HttpServlet implements Servlet {
    public abstract void doGet(Request request, Response response) throws Exception;
    public abstract void doPost(Request request, Response response) throws Exception;
    @Override
    public void service(Request request, Response response) throws Exception {
        String method = request.getMethod();
        if ('GET'.equalsIgnoreCase(method)) {
            doGet(request, response);
        } else {
            doPost(request, response);
        }
    }
}
實際的 doGet、doPost 由實際配置的業(yè)務 Servlet 來實現(xiàn),那么我們就創(chuàng)建 1 個實際業(yè)務:ShowServlet
public class ShowServlet extends HttpServlet {
    @Override
    public void doGet(Request request, Response response) throws Exception {
        String repText = '<h1>ShowServlet by GET</h1>';
        response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);
    }
    @Override
    public void doPost(Request request, Response response) throws Exception {
        String repText = '<h1>ShowServlet by POST</h1>';
        response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);
    }
    @Override
    public void init() throws Exception {}
    @Override
    public void destroy() throws Exception {}
}
我們再在項目 resources 文件夾中創(chuàng)建 web.xml,并配置我們的業(yè)務 Servlet: ShowServlet 的名稱和訪問路徑
<?xml version='1.0' encoding='utf-8'?>
<web-app>
    <servlet>
        <servlet-name>show</servlet-name>
        <servlet-class>server.ShowServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>show</servlet-name>
        <url-pattern>/show</url-pattern>
    </servlet-mapping>
</web-app>
既然配置了 web.xml,那我們還需要再對其進行解析和處理
解析 XML,我們在 pom.xml 里引入相關坐標依賴
<dependencies>
    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.1.6</version>
    </dependency>
</dependencies>
我們把處理邏輯寫在 Bootstrap 啟動類中
/**
 * 存放 Servlet信息,url: Servlet 實例
 */
private Map<String, HttpServlet> servletMap = new HashMap<>();
/**
 * 加載配置的 Servlet
 *
 * @throws Exception
 */
public void loadServlet() throws Exception {
    InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream('web.xml');
    SAXReader saxReader = new SAXReader();
    Document document = saxReader.read(resourceAsStream);
    Element rootElement = document.getRootElement();
    List<Element> list = rootElement.selectNodes('//servlet');
    for (Element element : list) {
        // <servlet-name>show</servlet-name>
        Element servletnameElement = (Element) element.selectSingleNode('servlet-name');
        String servletName = servletnameElement.getStringValue();
        // <servlet-class>server.ShowServlet</servlet-class>
        Element servletclassElement = (Element) element.selectSingleNode('servlet-class');
        String servletClass = servletclassElement.getStringValue();
        // 根據(jù) servlet-name 的值找到 url-pattern
        Element servletMapping = (Element) rootElement.selectSingleNode('/web-app/servlet-mapping[servlet-name='' + servletName + '']');
        // /show
        String urlPattern = servletMapping.selectSingleNode('url-pattern').getStringValue();
        servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).getDeclaredConstructor().newInstance());
    }
}
再調(diào)整 start 方法,在方法里執(zhí)行 loadServlet 函數(shù)來初始化 Servlet
public void start() throws Exception {
      System.out.println('Minicat starting...');
   // 此處加載并初始化 Servlet
    loadServlet();
      ServerSocket socket = new ServerSocket(port);
      while (true) {
          Socket accept = socket.accept();
          OutputStream outputStream = accept.getOutputStream();
          // 分別封裝 Request 和 Response
          Request request = new Request(accept.getInputStream());
          Response response = new Response(outputStream);
         // 根據(jù) url 來獲取 Servlet
          HttpServlet httpServlet = servletMap.get(request.getUrl());
         // 如果 Servlet 為空,說明是靜態(tài)資源,不為空即為動態(tài)資源,需要執(zhí)行 Servlet 里的方法
          if (httpServlet == null) {
              response.outputHtml(request.getUrl());
          } else {
              httpServlet.service(request, response);
          }
          accept.close();
      }
  }
然后我們訪問瀏覽器: http://localhost:8080/index.html,頁面顯示 Hello Minicat, index.html,就說明靜態(tài)資源訪問成功啦。
我們再訪問: http://localhost:8080/show, 頁面顯示 ShowServlet by GET,就說明動態(tài)資源也訪問成功啦。
這就完成 V3.0 版本了 ??????
V4.0 版本 
完成 V3.0 之后,3.0 還是有點問題的,比如在多線程訪問情況下
我們假設業(yè)務 ShowServlet 里,動態(tài)資源處理延遲,這里使用 Thread.sleep 模擬延遲的情況
@Override
public void doGet(Request request, Response response) throws Exception {
    // 模擬延遲
    Thread.sleep(100000);
    String repText = '<h1>ShowServlet by GET</h1>';
    response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);
}
然后再重新啟動,先訪問:http://localhost:8080/show,會發(fā)現(xiàn)一直在加載處理,此時再去訪問 http://localhost:8080/index.html,也會一直在加載處理。
意思就是我們訪問動態(tài)資源時候的一直等待,此時,再去訪問靜態(tài)資源的時候,也會一直等待,這樣就不行了。
這里就可以使用線程來解決,每個請求過來的 Socket 都分配 1 個線程,各自處理各自的請求
所以再在 Bootstrap 中的 start 方法做個改造
public void start() throws Exception {
    System.out.println('Minicat starting...');
    loadServlet();
    ServerSocket socket = new ServerSocket(port);
    while (true) {
        Socket accept = socket.accept();
        // 多線程改造,每個 socket 是個單獨的線程
        RequestProcessor requestProcessor = new RequestProcessor(accept, servletMap);
        requestProcessor.start();
    }
}
RequestProcessor 就集成 Thread ,重寫 run 函數(shù),實現(xiàn)具體的輸入輸出的處理
public class RequestProcessor extends Thread {
    private Socket socket;
    private Map<String, HttpServlet> servletMap;
    public RequestProcessor() {
    }
    public RequestProcessor(Socket socket, Map<String, HttpServlet> servletMap) {
        this.socket = socket;
        this.servletMap = servletMap;
    }
    @Override
    public void run() {
        try {
            OutputStream outputStream = socket.getOutputStream();
            // 分別封裝 Request 和 Response
            Request request = new Request(socket.getInputStream());
            Response response = new Response(outputStream);
            HttpServlet httpServlet = servletMap.get(request.getUrl());
            if (httpServlet == null) {
                response.outputHtml(request.getUrl());
            } else {
                httpServlet.service(request, response);
            }
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
每個都分配個線程,有點消耗資源,所以我們再升級下,使用線程池,所以再對 start 函數(shù)進行一次簡單的調(diào)整
public void start() throws Exception {
    System.out.println('Minicat starting...');
    loadServlet();
    // 定義一個線程池
    int corePoolSize = 10;
    int maximumPoolSize =50;
    long keepAliveTime = 100L;
    TimeUnit unit = TimeUnit.SECONDS;
    BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
    ThreadFactory threadFactory = Executors.defaultThreadFactory();
    RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            unit,
            workQueue,
            threadFactory,
            handler
    );
    ServerSocket socket = new ServerSocket(port);
    while (true) {
        Socket accept = socket.accept();
        // 多線程改造,每個 socket 是個單獨的線程
        RequestProcessor requestProcessor = new RequestProcessor(accept, servletMap);
         // 線程池執(zhí)行
        threadPoolExecutor.execute(requestProcessor);
    }
}
這就完成 V4.0 版本了 ????????
V5.0 版本 
開始之前,我們先捋一下之前實現(xiàn) Minicat 的步驟,來看個圖
 handwriting-minicat-process
handwriting-minicat-process5.0 版本來了,我們要模擬出 webapps 部署效果,這個功能說簡單點了,就是把一個指定的文件夾 webapps 下面的所有的 Servlet 給讀取出來并加載,然后在請求的時候去解析對應的 servlet 就行了。
那么我們就 resources 里寫個配置文件:server.xml 來配置指定的文件夾路徑信息,為了模仿實際的 Tomcat,我們再加幾個標簽
<Server>
    <Service name='Mojave'>
        <Connector port='8080'/>
        <Engine defaultHost='localhost'>
          <!-- appBase 就是指定的文件夾 -->
            <Host name='localhost' appBase='/Users/suremotoo/Documents/webapps'/>
        </Engine>
    </Service>
</Server>
而業(yè)務 Servlet 呢,肯定要在單獨的項目中寫了,Minicat 是要部署加載它們的,后面再說。
根據(jù)面向?qū)ο蟮乃枷?,我們在編寫一?POJO 類
Context 就是在指定的 webapps 目錄中的,每個項目,每個項目里有自己的 Servlet 集合
public class Context {
    public Context() {
    }
    public Context(Map<String, HttpServlet> servletMap) {
        this.servletMap = servletMap;
    }
  
   // Context 中的 Servlet
    private Map<String, HttpServlet> servletMap;
    public Map<String, HttpServlet> getServletMap() {
        return servletMap;
    }
    public void setServletMap(Map<String, HttpServlet> servletMap) {
        this.servletMap = servletMap;
    }
}
Host 作為配置中的主機信息,每個主機下面有自己的 Context 項目集合
public class Host {
    public Host() {
    }
    public Host(Map<String, Context> contextMap) {
        this.contextMap = contextMap;
    }
  
   // hHst 中的 Context
    private Map<String, Context> contextMap;
    public Map<String, Context> getContextMap() {
        return contextMap;
    }
    public void setContextMap(Map<String, Context> contextMap) {
        this.contextMap = contextMap;
    }
}
Mapper 其實指的是 Service,每個 Service 下面有自己的 Host 主機集合
public class Mapper {
   // Service 中的 Host
    private Map<String, Host> hostMap;
    public Mapper(Map<String, Host> hostMap) {
        this.hostMap = hostMap;
    }
    public Map<String, Host> getHostMap() {
        return hostMap;
    }
    public void setHostMap(Map<String, Host> hostMap) {
        this.hostMap = hostMap;
    }
}
Server 指 Minicat 服務實例, 1 個 Minicat 就 1 個 Server,每個 Server 下面有自己的 Mapper 集合
public class Server {
   // Server 里的 Service
    private Map<String, Mapper> serviceMap;
    public Server() {
    }
    public Server(Map<String, Mapper> serviceMap) {
        this.serviceMap = serviceMap;
    }
    public Map<String, Mapper> getServiceMap() {
        return serviceMap;
    }
    public void setServiceMap(Map<String, Mapper> serviceMap) {
        this.serviceMap = serviceMap;
    }
}
POJO 類完成后,就開始改造程序入口 Bootstrap 了,要加載指定目錄下的文件,所以我們要改造 loadServlet
/**
 * 加載實際項目里配置的 Servlet
 *
 * @throws Exception
 */
@SuppressWarnings({'unchecked'})
public Context loadContextServlet(String path) throws Exception {
    String webPath = path + '/web.xml';
    if (!(new File(webPath).exists())) {
        System.out.println('not found ' + webPath);
        return null;
    }
    InputStream resourceAsStream = new FileInputStream(webPath);
    SAXReader saxReader = new SAXReader();
    Document document = saxReader.read(resourceAsStream);
    Element rootElement = document.getRootElement();
    List<Element> list = rootElement.selectNodes('//servlet');
    Map<String, HttpServlet> servletMap = new HashMap<>(16);
    for (Element element : list) {
        // <servlet-name>show</servlet-name>
        Element servletnameElement = (Element) element.selectSingleNode('servlet-name');
        String servletName = servletnameElement.getStringValue();
        // <servlet-class>server.ShowServlet</servlet-class>
        Element servletclassElement = (Element) element.selectSingleNode('servlet-class');
        String servletClass = servletclassElement.getStringValue();
        // 根據(jù) servlet-name 的值找到 url-pattern
        Element servletMapping = (Element) rootElement.selectSingleNode('/web-app/servlet-mapping[servlet-name='' + servletName + '']');
        // /show
        String urlPattern = servletMapping.selectSingleNode('url-pattern').getStringValue();
        // 自定義類加載器,來加載 webapps 目錄下的 class
        WebClassLoader webClassLoader = new WebClassLoader();
        Class<?> aClass = webClassLoader.findClass(path, servletClass);
        servletMap.put(urlPattern, (HttpServlet) aClass.getDeclaredConstructor().newInstance());
    }
    return new Context(servletMap);
}
/**
 * 加載 server.xml,解析并初始化 webapps 下面的各個項目的 servlet
 */
@SuppressWarnings({'unchecked'})
public void loadServlet() {
    InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream('server.xml');
    SAXReader saxReader = new SAXReader();
    try {
        Document document = saxReader.read(resourceAsStream);
        Element rootElement = document.getRootElement();
        // 解析 server 標簽
        Element serverElement = (Element) rootElement.selectSingleNode('//Server');
        // 解析 server 下的 Service 標簽
        List<Element> serviceNodes = serverElement.selectNodes('//Service');
        // 存儲各個 Host
        Map<String, Host> hostMap = new HashMap<>(8);
        //遍歷 service
        for (Element service : serviceNodes) {
            String serviceName = service.attributeValue('name');
            Element engineNode = (Element) service.selectSingleNode('//Engine');
            List<Element> hostNodes = engineNode.selectNodes('//Host');
            // 存儲有多少個項目
            Map<String, Context> contextMap = new HashMap<>(8);
            for (Element hostNo : hostNodes) {
                String hostName = hostNo.attributeValue('name');
                String appBase = hostNo.attributeValue('appBase');
                File file = new File(appBase);
                if (!file.exists() || file.list() == null) {
                    break;
                }
                String[] list = file.list();
                //遍歷子文件夾,即:實際的項目列表
                for (String path : list) {
                    //將項目封裝成 context,并保存入map
                    contextMap.put(path, loadContextServlet(appBase + '/' + path));
                }
                // hsot:port
                // eg: localhost:8080
                hostMap.put(hostName + ':' + port, new Host(contextMap));
            }
            serviceMap.put(serviceName, new Mapper(hostMap));
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
代碼有點長,稍微說明下理解起來還是比較簡單的:
先解析 server.xml ,然后根據(jù)配置的指定文件夾目錄去解析,目錄下面的每個文件夾就是一個 Context;
而 Context 里的 servlet 信息,都會配置在 Context 自己的 web.xml 中;
所以再解析每個 Context 里自己的 web.xml,封裝所有的 Servlet
當然了,Context、Host、Mapper、Server 也都會一并封裝
封裝所有的 Servlet 的時候,需要自己去寫個類加載器去實例化,所以加了個自定義的類加載器
public class WebClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String basePath, String className) {
        byte[] classBytes = getClassBytes(basePath, className);
        return defineClass(className, classBytes, 0, classBytes.length);
    }
    /**
     * 讀取類的字節(jié)碼
     *
     * @param basePath  根路徑
     * @param className 類的全限定名
     * @return servlet 的字節(jié)碼信息
     * @throws IOException
     */
    private byte[] getClassBytes(String basePath, String className) {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path = basePath + File.separatorChar +
                className.replace('.', File.separatorChar) + '.class';
        try {
            in = new FileInputStream(path);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int len = 0;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}
基礎的工作都做好了,就差解析了,我們要改造 RequestProcessor
public class RequestProcessor extends Thread {
    private Socket socket;
    private Server server;
    public RequestProcessor() {
    }
    public RequestProcessor(Socket socket, Server server) {
        this.socket = socket;
        this.server = server;
    }
    @Override
    public void run() {
        try {
            OutputStream outputStream = socket.getOutputStream();
            // 分別封裝 Request 和 Response
            Request request = new Request(socket.getInputStream());
            Response response = new Response(outputStream);
            HttpServlet httpServlet = findHttpServlet(request);
            if (httpServlet == null) {
                response.outputHtml(request.getUrl());
            } else {
                httpServlet.service(request, response);
            }
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 根據(jù)請求信息找到對應業(yè)務 Servlet
     * <pre>
     *  GET web-greet/greet HTTP/1.1
     *  Host: suremotoo.com
     * </pre>
     *
     * @param request
     * @return 具體要執(zhí)行的 servlet
     */
    private HttpServlet findHttpServlet(Request request) {
        HttpServlet businessServlet = null;
        Map<String, Mapper> serviceMap = server.getServiceMap();
        for (String key : serviceMap.keySet()) {
            String hostName = request.getHost();
            Map<String, Host> hostMap = serviceMap.get(key).getHostMap();
            Host host = hostMap.get(hostName);
            if (host != null) {
                Map<String, Context> contextMap = host.getContextMap();
                // 處理 url
                // eg: web-greet/greet
                String url = request.getUrl();
                String[] urlPattern = url.split('/');
                String contextName = urlPattern[1];
                String servletStr = '/';
                if (urlPattern.length > 2) {
                    servletStr += urlPattern[2];
                }
                // 獲取上下文
                Context context = contextMap.get(contextName);
                if (context != null) {
                    Map<String, HttpServlet> servletMap = context.getServletMap();
                    businessServlet = servletMap.get(servletStr);
                }
            }
        }
        return businessServlet;
    }
}
核心就是 findHttpServlet 方法,從 Server、Mapper、Host、Context 依次取出,直到最后的 Servlet 配置集合,
根據(jù) url 找到對應的 Servlet 執(zhí)行
至于我們的測試項目,用于部署到 webapps 目錄嘛,我這里就簡單說一下就行
先把 Minicat 打個 jar 包,給測試項目用!
參考 Minicat 的目錄,可以建立 maven 項目,編寫自定義的業(yè)務 Servlet ,然后在 resources 文件夾中,建立 web.xml 文件,用于配置自定義的 Servlet
???? 注意啦:而自定義的 Servlet 一定要 extends Minicat 中的 HttpServet?。?/strong>
至此就 V5.0 就大功告成 ??????????
各版本實現(xiàn)回顧 
 handwriting-minicat-timelines感謝 suremotoo 的這篇文章,本人收獲匪淺,也希望對你們有幫助。
handwriting-minicat-timelines感謝 suremotoo 的這篇文章,本人收獲匪淺,也希望對你們有幫助。