手寫迷你版 Tomcat - Minicat

大家也可以關(guān)注我的公眾號(hào): 漿果捕鼠草,文章也會(huì)同步更新歹茶,當(dāng)然盟广,公眾號(hào)還會(huì)有一些資源可以分享給大家~

手寫迷你版 Tomcat - Minicat

Minicat 的目標(biāo)

我們可以通過瀏覽器客戶端發(fā)送 http 請(qǐng)求廷粒, Minicat 可以接收到請(qǐng)求進(jìn)?處理鸳址,處理之后的結(jié)果可以返回瀏覽器客戶端。

基本方向

  • 提供服務(wù)矮固,接收請(qǐng)求(Socket 通信)

  • 請(qǐng)求信息封裝成 Request 對(duì)象失息,同樣響應(yīng)信息封裝成 Response 對(duì)象

  • 客戶端請(qǐng)求資源,資源分為靜態(tài)資源(HTML)和動(dòng)態(tài)資源(Servlet)

  • 資源返回給客戶端瀏覽器

迭代實(shí)現(xiàn)

我們實(shí)現(xiàn)時(shí)候呢档址,一步一步來盹兢,可以制定的小版本計(jì)劃

  • V1.0 需求:瀏覽器請(qǐng)求 http://localhost:8080, 返回?個(gè)固定的字符串到??"Hello Minicat."
  • V2.0 需求:封裝 Request 和 Response 對(duì)象,返回 HTML 靜態(tài)資源?件
  • V3.0 需求:可以請(qǐng)求動(dòng)態(tài)資源(Servlet)
  • V4.0 需求:可以多線程訪問
  • V5.0 需求:在已有 Minicat 基礎(chǔ)上進(jìn)?步擴(kuò)展守伸,模擬出 webapps 部署效果,磁盤上放置?個(gè) webapps ?錄绎秒,webapps 中可以有多個(gè)項(xiàng)?,?如 demo1,demo2,demo3… 具體的項(xiàng)??如 demo1 中有 serlvet(也即為:servlet 是屬于具體某?個(gè)項(xiàng)?的 servlet)尼摹,這樣的話在 Minicat 初始化配置加載见芹,以及根據(jù)請(qǐng)求 url 查找對(duì)應(yīng) serlvet 時(shí)都需要進(jìn)?步處理。

V1.0 版本

環(huán)境搭建

確定好方向蠢涝,就進(jìn)行項(xiàng)目搭建開發(fā)

新建一個(gè) Maven 項(xiàng)目玄呛,并調(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>

編寫啟動(dòng)類 Bootstrap

public class Bootstrap {

  /**
   * 設(shè)定啟動(dòng)和監(jiān)聽端口
   */
  private int port = 8080;

  /**
   * 啟動(dòng)函數(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();
      }
  }

  /**
   * 啟動(dòng)入口
   *
   * @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 響應(yīng)信息長(zhǎng)度
     * @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";
    }

    /**
     * 為響應(yīng)碼 404 提供請(qǐng)求頭信息(此處也包含了數(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;
    }

}

然后我們?cè)L問瀏覽器:http://localhost:8080和二,頁(yè)面顯示 Hello Minicat.,就說明成功啦徘铝。

這就完成 V1.0 版本了 ??


V2.0 版本

緊接著我們實(shí)現(xiàn)請(qǐng)求信息、響應(yīng)信息的封裝,即:Request惕它、Response 的實(shí)現(xiàn)

public class Request {

  /**
   * 請(qǐng)求方式, eg: GET怕午、POST
   */
  private String method;

  /**
   * 請(qǐng)求路徑,eg: /index.html
   */
  private String url;

  /**
   * 請(qǐng)求信息輸入流 <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 {

    /**
     * 響應(yīng)信息
     */
    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 拼接絕對(duì)路徑,根據(jù)絕對(duì)路徑再讀取文件資源淹魄,再響應(yīng)
     *
     * @param url 請(qǐng)求 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ù)請(qǐng)求 url 獲取完整絕對(duì)路徑
     *
     * @param url 請(qǐng)求 url
     * @return 完整絕對(duì)路徑
     */
    public static String getAbsolutePath(String url) {
        String path = StaticResourceUtil.class.getResource("/").getPath();
        return path.replaceAll("\\\\", "/") + url;
    }

    /**
     * 輸出靜態(tài)資源信息
     *
     * @param inputStream  靜態(tài)資源文件的讀取流
     * @param outputStream 響應(yīng)的輸出流
     * @throws IOException
     */
    public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
        int count = 0;
        while (count == 0) {
            count = inputStream.available();
        }

        // 輸出 http 請(qǐng)求頭,然后再輸出具體內(nèi)容
        int resourceSize = count;
        // 讀取內(nèi)容輸出
        outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
        // 已經(jīng)讀取的內(nèi)容?度
        long written = 0;
        // 計(jì)劃每次緩沖的?度
        int byteSize = 1024;
        byte[] bytes = new byte[byteSize];
        while (written < resourceSize) {
            // 說明剩余未讀取??不??個(gè) 1024 ?度,那就按真實(shí)?度處理
            if (written + byteSize > resourceSize) {
                // 計(jì)算實(shí)際剩余內(nèi)容
                byteSize = (int) (resourceSize - written);
                bytes = new byte[byteSize];
            }
            inputStream.read(bytes);
            outputStream.write(bytes);
            outputStream.flush();
            // 計(jì)算已經(jīng)讀取的長(zhǎ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();
    }
}

哦對(duì)了兆蕉,還缺少 1 個(gè)具體的 HTML 靜態(tài)資源?件,我們創(chuàng)建一個(gè)名為 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>

然后我們?cè)L問瀏覽器:http://localhost:8080/index.html缤沦,頁(yè)面顯示 Hello Minicat, index.html,就說明成功啦恨樟。

這就完成 V2.0 版本了 ????


V3.0 版本

接下來就來實(shí)現(xiàn)請(qǐng)求動(dòng)態(tài)資源

我們先定義 Servlet 的接口,指定規(guī)范

public interface Servlet {

    void init() throws Exception;

    void destroy() throws Exception;

    void service(Request request, Response response) throws Exception;
}

緊接著再完成 1 個(gè)公共的抽象父類 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);
        }
    }

}

實(shí)際的 doGet疚俱、doPost 由實(shí)際配置的業(yè)務(wù) Servlet 來實(shí)現(xiàn),那么我們就創(chuàng)建 1 個(gè)實(shí)際業(yè)務(wù): 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 {}

}

我們?cè)僭陧?xiàng)目 resources 文件夾中創(chuàng)建 web.xml缩多,并配置我們的業(yè)務(wù) 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呆奕,那我們還需要再對(duì)其進(jìn)行解析和處理

解析 XML,我們?cè)?pom.xml 里引入相關(guān)坐標(biāo)依賴

<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 啟動(dòng)類中

/**
 * 存放 Servlet信息衬吆,url: Servlet 實(shí)例
 */
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("http://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)資源逊抡,不為空即為動(dòng)態(tài)資源姆泻,需要執(zhí)行 Servlet 里的方法
          if (httpServlet == null) {
              response.outputHtml(request.getUrl());
          } else {
              httpServlet.service(request, response);
          }
          accept.close();
      }
  }

然后我們?cè)L問瀏覽器: http://localhost:8080/index.html,頁(yè)面顯示 Hello Minicat, index.html,就說明靜態(tài)資源訪問成功啦冒嫡。

我們?cè)僭L問: http://localhost:8080/show, 頁(yè)面顯示 ShowServlet by GET,就說明動(dòng)態(tài)資源也訪問成功啦拇勃。

這就完成 V3.0 版本了 ??????


V4.0 版本

完成 V3.0 之后,3.0 還是有點(diǎn)問題的孝凌,比如在多線程訪問情況下

我們假設(shè)業(yè)務(wù) ShowServlet 里方咆,動(dòng)態(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);
}

然后再重新啟動(dòng)蟀架,先訪問:http://localhost:8080/show瓣赂,會(huì)發(fā)現(xiàn)一直在加載處理,此時(shí)再去訪問 http://localhost:8080/index.html片拍,也會(huì)一直在加載處理煌集。

意思就是我們?cè)L問動(dòng)態(tài)資源時(shí)候的一直等待,此時(shí)捌省,再去訪問靜態(tài)資源的時(shí)候苫纤,也會(huì)一直等待,這樣就不行了。

這里就可以使用線程來解決方面,每個(gè)請(qǐng)求過來的 Socket 都分配 1 個(gè)線程放钦,各自處理各自的請(qǐng)求

所以再在 Bootstrap 中的 start 方法做個(gè)改造

public void start() throws Exception {
    System.out.println("Minicat starting...");
    loadServlet();
    ServerSocket socket = new ServerSocket(port);
    while (true) {
        Socket accept = socket.accept();
        // 多線程改造,每個(gè) socket 是個(gè)單獨(dú)的線程
        RequestProcessor requestProcessor = new RequestProcessor(accept, servletMap);
        requestProcessor.start();
    }
}

RequestProcessor 就集成 Thread 恭金,重寫 run 函數(shù)操禀,實(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();
        }
    }

}

每個(gè)都分配個(gè)線程,有點(diǎn)消耗資源横腿,所以我們?cè)偕?jí)下颓屑,使用線程池,所以再對(duì) start 函數(shù)進(jìn)行一次簡(jiǎn)單的調(diào)整

public void start() throws Exception {
    System.out.println("Minicat starting...");
    loadServlet();
    // 定義一個(gè)線程池
    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();
        // 多線程改造耿焊,每個(gè) socket 是個(gè)單獨(dú)的線程
        RequestProcessor requestProcessor = new RequestProcessor(accept, servletMap);
         // 線程池執(zhí)行
        threadPoolExecutor.execute(requestProcessor);
    }
}

這就完成 V4.0 版本了 ????????


V5.0 版本

開始之前揪惦,我們先捋一下之前實(shí)現(xiàn) Minicat 的步驟,來看個(gè)圖

在這里插入圖片描述

5.0 版本來了罗侯,我們要模擬出 webapps 部署效果器腋,這個(gè)功能說簡(jiǎn)單點(diǎn)了,就是把一個(gè)指定的文件夾 webapps 下面的所有的 Servlet 給讀取出來并加載钩杰,然后在請(qǐng)求的時(shí)候去解析對(duì)應(yīng)的 servlet 就行了纫塌。

那么我們就 resources 里寫個(gè)配置文件:server.xml 來配置指定的文件夾路徑信息,為了模仿實(shí)際的 Tomcat讲弄,我們?cè)偌訋讉€(gè)標(biāo)簽

<Server>
    <Service name="Mojave">
        <Connector port="8080"/>
        <Engine defaultHost="localhost">
          <!-- appBase 就是指定的文件夾 -->
            <Host name="localhost" appBase="/Users/suremotoo/Documents/webapps"/>
        </Engine>
    </Service>
</Server>

而業(yè)務(wù) Servlet 呢措左,肯定要在單獨(dú)的項(xiàng)目中寫了,Minicat 是要部署加載它們的避除,后面再說怎披。

根據(jù)面向?qū)ο蟮乃枷耄覀冊(cè)诰帉懸恍?POJO 類

Context 就是在指定的 webapps 目錄中的瓶摆,每個(gè)項(xiàng)目凉逛,每個(gè)項(xiàng)目里有自己的 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 作為配置中的主機(jī)信息,每個(gè)主機(jī)下面有自己的 Context 項(xiàng)目集合

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 其實(shí)指的是 Service,每個(gè) Service 下面有自己的 Host 主機(jī)集合

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 服務(wù)實(shí)例, 1 個(gè) Minicat 就 1 個(gè) Server,每個(gè) 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

/**
 * 加載實(shí)際項(xiàng)目里配置的 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("http://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 下面的各個(gè)項(xiàng)目的 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 標(biāo)簽
        Element serverElement = (Element) rootElement.selectSingleNode("http://Server");
        // 解析 server 下的 Service 標(biāo)簽
        List<Element> serviceNodes = serverElement.selectNodes("http://Service");
        // 存儲(chǔ)各個(gè) Host
        Map<String, Host> hostMap = new HashMap<>(8);
        //遍歷 service
        for (Element service : serviceNodes) {
            String serviceName = service.attributeValue("name");
            Element engineNode = (Element) service.selectSingleNode("http://Engine");
            List<Element> hostNodes = engineNode.selectNodes("http://Host");
            // 存儲(chǔ)有多少個(gè)項(xiàng)目
            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();
                //遍歷子文件夾,即:實(shí)際的項(xiàng)目列表
                for (String path : list) {
                    //將項(xiàng)目封裝成 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();
    }
}

代碼有點(diǎn)長(zhǎng)自晰,稍微說明下理解起來還是比較簡(jiǎn)單的:

先解析 server.xml ,然后根據(jù)配置的指定文件夾目錄去解析稍坯,目錄下面的每個(gè)文件夾就是一個(gè) Context酬荞;

而 Context 里的 servlet 信息搓劫,都會(huì)配置在 Context 自己的 web.xml 中;

所以再解析每個(gè) Context 里自己的 web.xml混巧,封裝所有的 Servlet

當(dāng)然了枪向,ContextHost咧党、Mapper秘蛔、Server 也都會(huì)一并封裝

封裝所有的 Servlet 的時(shí)候,需要自己去寫個(gè)類加載器去實(shí)例化傍衡,所以加了個(gè)自定義的類加載器


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;
    }
}

基礎(chǔ)的工作都做好了深员,就差解析了,我們要改造 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ù)請(qǐng)求信息找到對(duì)應(yīng)業(yè)務(wù) 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 找到對(duì)應(yīng)的 Servlet 執(zhí)行

至于我們的測(cè)試項(xiàng)目屡江,用于部署到 webapps 目錄嘛燎悍,我這里就簡(jiǎn)單說一下就行

先把 Minicat 打個(gè) jar 包,給測(cè)試項(xiàng)目用盼理!

參考 Minicat 的目錄,可以建立 maven 項(xiàng)目俄删,編寫自定義的業(yè)務(wù) Servlet 宏怔,然后在 resources 文件夾中,建立 web.xml 文件畴椰,用于配置自定義的 Servlet

???? 注意啦:而自定義的 Servlet 一定要 extends Minicat 中的 HttpServetk铩!

至此就 V5.0 就大功告成 ??????????

各版本實(shí)現(xiàn)回顧

]

附-精美 PDF 版本

????????另外特殊福利 ?????? 關(guān)注我的公眾號(hào): 漿果捕鼠草斜脂,發(fā)送關(guān)鍵字: Minicat 精美
即可獲得本文的精美 PDF 版本哦抓艳!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帚戳,隨后出現(xiàn)的幾起案子玷或,更是在濱河造成了極大的恐慌,老刑警劉巖片任,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偏友,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡对供,警方通過查閱死者的電腦和手機(jī)位他,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鹅髓,你說我怎么就攤上這事舞竿。” “怎么了窿冯?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵骗奖,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我靡菇,道長(zhǎng)重归,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任厦凤,我火速辦了婚禮鼻吮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘较鼓。我一直安慰自己椎木,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布博烂。 她就那樣靜靜地躺著香椎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪禽篱。 梳的紋絲不亂的頭發(fā)上畜伐,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音躺率,去河邊找鬼玛界。 笑死,一個(gè)胖子當(dāng)著我的面吹牛悼吱,可吹牛的內(nèi)容都是我干的慎框。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼后添,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼笨枯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起遇西,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤馅精,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后粱檀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體硫嘶,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年梧税,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了沦疾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片称近。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖哮塞,靈堂內(nèi)的尸體忽然破棺而出刨秆,到底是詐尸還是另有隱情,我是刑警寧澤忆畅,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布衡未,位于F島的核電站,受9級(jí)特大地震影響家凯,放射性物質(zhì)發(fā)生泄漏缓醋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一绊诲、第九天 我趴在偏房一處隱蔽的房頂上張望送粱。 院中可真熱鬧,春花似錦掂之、人聲如沸抗俄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽动雹。三九已至,卻和暖如春跟压,著一層夾襖步出監(jiān)牢的瞬間胰蝠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工震蒋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留姊氓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓喷好,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親读跷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子梗搅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • java動(dòng)態(tài)網(wǎng)頁(yè)技術(shù) servlet 本質(zhì)就是一段Java程序 在Servlet中最大的問題是,HTML輸出和Ja...
    Liang_JC閱讀 498評(píng)論 0 0
  • Tomcat 系統(tǒng)架構(gòu)與原理剖析 注意:瀏覽器訪問服務(wù)器使?的是Http協(xié)議效览,Http是應(yīng)?層協(xié)議无切,?于定義數(shù)據(jù)通...
    acc8226閱讀 1,721評(píng)論 0 6
  • 1、JavaWeb概念Java web丐枉,是用java技術(shù)來解決相關(guān)web互聯(lián)網(wǎng)領(lǐng)域的技術(shù)的總稱哆键。web包括:web...
    寒劍飄零閱讀 2,575評(píng)論 0 12
  • 久違的晴天,家長(zhǎng)會(huì)瘦锹。 家長(zhǎng)大會(huì)開好到教室時(shí)籍嘹,離放學(xué)已經(jīng)沒多少時(shí)間了闪盔。班主任說已經(jīng)安排了三個(gè)家長(zhǎng)分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,528評(píng)論 16 22
  • 創(chuàng)業(yè)是很多人的夢(mèng)想辱士,多少人為了理想和不甘選擇了創(chuàng)業(yè)來實(shí)現(xiàn)自我價(jià)值泪掀,我就是其中一個(gè)。 創(chuàng)業(yè)后颂碘,我由女人變成了超人异赫,什...
    亦寶寶閱讀 1,818評(píng)論 4 1