大佬說:怎麼手寫一個Tomcat然後、給這個Tomcat 部署項目並且成功處理請求 ? 我TM當時就嚇尿了!

@TOC

前言

提示:上篇我們自定義了簡單的Tomcat,但是不能去部署web項目,當然也不能從根據url進行訪問:

閱讀本文前請先閱讀: 自定義一個簡單的Tomcat 即:自定義一個簡單的Tomcat 可以訪問靜態頁面,返回字符串等;

提示:如何在自定義Tomcat中部署外部的web項目呢?

一、怎麼部署項目?

示例:通常我們部署項目是在Tomcat的webapps下面將打好的1個或多個war包進去,也可以配置響應的上下文以及具體的項目路徑,然後tomcat會根據指定的路徑去訪問,這期間Tomcat是怎麼來根據這個路徑去解析這些項目?怎麼去根據不同的url去找到不同的項目以及處理不同的請求?

二、分析以及思路

1.Tomcat的配置文件

精簡後的server.xml

<?xml version="1.0" encoding="UTF-8"?>

<Server port="8005" shutdown="SHUTDOWN">
 
  <Service name="Catalina">
    
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
    
    <Engine name="Catalina" defaultHost="localhost">

      <Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true">

      </Host>
    </Engine>
  </Service>
</Server>

  • Connector表示一些連接請求信息,包括強端口,超時時間,重定向端口,http協議版本;
  • 可以根據Host來指定虛擬主機;
  • 可以根據appBase來指定自己的項目的路徑;

可以根據這個xml來配置tomcat端口以及訪問的域名以及包路徑等;

我們根據這個可以自定義自己的server.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
    <Service name="Catalina">
        <!--        啓動端口-->
        <Connector port="8080"/>
        <Engine>
            <!--      虛擬主機-->
            <Host name="localhost"
                  appBase="/Users/pilgrim/Desktop/Mini-tomcat-main/TomcatDemo/src/webapps"
                  unpackWARs="true" autoDeploy="true">
            </Host>
        </Engine>
    </Service>
</Server>

2 web項目文件夾信息

Java Web 打包後文件目錄

圖片來自網絡

<font color=#999AAA >代碼如下(示例):

然後我們可以根據這個圖自定義一個web工程,如下圖所示我已經建好了

簡易版的web工程 在這裏插入圖片描述

這裏建了兩個工程web_Demo和web_Demo2 內容如下圖所示: web_Demo包 web.xml配置servlet信息

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
    <servlet-name>testServlet</servlet-name>
    <servlet-class>server.MyServlet1</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>testServlet</servlet-name>
    <url-pattern>/api/test1</url-pattern>
</servlet-mapping>
</web-app>

請求的MyServlet1 字節碼文件

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package server;

import com.udeam.util.HttpUtil;
import com.udeam.v2.bean.Request;
import com.udeam.v2.bean.Response;
import com.udeam.v3.inteface.HttpServlet;
import java.io.IOException;

public class MyServlet1 extends HttpServlet {
    public MyServlet1() {
    }

    public void init() throws Exception {
    }

    public void doGet(Request request, Response response) {
        String contents = "<h2> GET 外部部署業務請求 </h2>";
        System.out.println(contents);

        try {
            response.outPutStr(HttpUtil.resp_200(contents));
        } catch (IOException var5) {
            var5.printStackTrace();
        }

    }

    public void doPost(Request request, Response response) {
        String contents = "<h2> Post 外部部署業務請求</h2>";

        try {
            response.outPutStr(HttpUtil.resp_200(contents));
        } catch (IOException var5) {
            var5.printStackTrace();
        }

    }

    public void destory() throws Exception {
    }
}

web_Demo2包內容 web.xml配置servlet信息

<?xml version="1.0" encoding="UTF-8"?>
<web-app>


    <servlet>
        <servlet-name>testServlet</servlet-name>
        <servlet-class>server.MyServlet2</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>testServlet</servlet-name>
        <url-pattern>/api/test2</url-pattern>
    </servlet-mapping>
</web-app>

請求的MyServlet2 字節碼文件

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package server;

import com.udeam.util.HttpUtil;
import com.udeam.v2.bean.Request;
import com.udeam.v2.bean.Response;
import com.udeam.v3.inteface.HttpServlet;
import java.io.IOException;

public class MyServlet2 extends HttpServlet {
    public MyServlet2() {
    }

    public void init() throws Exception {
    }

    public void doGet(Request request, Response response) {
        String cc = "<h3> GET 外部部署MyServlet2業務請求 </h3>";
        System.out.println(cc);

        try {
            response.outPutStr(HttpUtil.resp_200(cc));
        } catch (IOException var5) {
            var5.printStackTrace();
        }

    }

    public void doPost(Request request, Response response) {
        String content = "<h2> Post 外部部署MyServlet2業務請求</h2>";

        try {
            response.outPutStr(HttpUtil.resp_200(content));
        } catch (IOException var5) {
            var5.printStackTrace();
        }

    }

    public void destory() throws Exception {
    }
}

2.初始化項目配置

啓動Tomcat的時候,會根據server.xml裏面的配置監聽端口信息

首先我們在啓動main方法時候加載解析server.xml配置文件,拿到port端口便於之後監聽8080端口 然後根據指定的appBase路徑去加載項目信息如:那個包名(上下文),以及class,解析項目的web.xml拿到請求url信息以及維護好映射關係;

具體流程如下圖

在這裏插入圖片描述

  • 首先啓動Bootstartp類的main方法;
  • 加載解析自定義tomcat的server.xml方法,得到啓動端口,以及項目所在webapps路徑;
  • 解析webapps裏的項目,解析當前項目的context,web.xml得到url映射關係;
  • 最後處理請求,根據客戶端的host以及上下文,還有url定位要處理的servelt然後提供請求返回給客戶端;

需要注意的是在Tomcat server.xml中可以配置多個host,一個host下可以包含多個context也就是多個項目,然後context下是多個請求url

這兒我們僅限於一個host對應多個context,然後對應多個url,再根據url定位servlet;

定義映射類
 public class MapperContext {

    /**
     * 虛擬主機
     */
    private Host host;

    public Host getHost() {
        return host;
    }

    public void setHost(Host host) {
        this.host = host;
    }
}
  • 1 Host這兒就不處理了,這兒用一個localhost請求;

一個host下對應多個Context

public class Host {

    /**
     * 虛擬主機名
     */
    private String hostName;

    /**
     * Context 不同的項目名
     */
    private List<Context> contextList;

    public Host() {
        this.contextList = new ArrayList<>();
    }

    public List<Context> getContextList() {
        return contextList;
    }

    public void setContextList(List<Context> contextList) {
        this.contextList = contextList;
    }

    public String getHostName() {
        return hostName;
    }

    public void setHostName(String hostName) {
        this.hostName = hostName;
    }
}
  • 2 定義Context對應的url和servlet映射關係 一個Context對應多個請求url
public class Context {

    /**
     * 請求url 用來鎖定servlet
     */
    private List<Wrapper> wrappersList;


    /**
     * context name 項目名 也就是上下文名
     */
    String name;
    
    public Context(String name) {
        this.name = name;
        wrappersList = new ArrayList<>();
    }
    public Context() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Wrapper> getWrappersList() {
        return wrappersList;
    }

    public void setWrappersList(List<Wrapper> wrappersList) {
        this.wrappersList = wrappersList;
    }
}

  • 3 請求url 用來鎖定servlet
public class Wrapper {
    private String url;

    /**
     * url對應的servlet實例
     */
    private Object object;

    /**
     * web.xml裏面配置的全限定名
     */
    private String servletClass;

    public String getUrl() {
        return url;
    }

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getServletClass() {
        return servletClass;
    }

    public void setServletClass(String servletClass) {
        this.servletClass = servletClass;
    }
}

加載配置文件

加載 server.xml

獲取端口和虛擬主機,以及webapps下的項目地址,並設置端口,設置虛擬主機到映射類中Host屬性

 public void loadServerXml() throws DocumentException {

        //1 加載解析 server.xml文件
        InputStream resourceAsStream = this.getClass().getResourceAsStream("/conf/server.xml");

        SAXReader saxReader = new SAXReader();
        Document read = saxReader.read(resourceAsStream);
        //獲取跟路徑
        Element rootElement = read.getRootElement();

        Document document = rootElement.getDocument();

        //2 獲取端口
        Element node = (Element) document.selectSingleNode("//Connector");
        String port = node.attributeValue("port");
        this.setPort(Integer.valueOf(port));
        //3 獲取host
        Element element = (Element) document.selectSingleNode("//Host");
        //虛擬主機
        String localhost = element.attributeValue("name");
        //虛擬主機
        Host host = new Host();
        host.setHostName(localhost);
        mapperContext.setHost(host);


        //部署的地址路徑
        String appBase = element.attributeValue("appBase");
        //4 根據這個路徑去解析裏面的項目 映射端口和虛擬主機,項目,以及url->servlet
        parseAppBase(appBase);


    }

解析項目內容

根據appBase路徑去解析每個項目的web.xml和加載class字節碼

在這裏插入圖片描述

解析web.xml

根據appBase路徑去拿到項目名,如web_Demo

第一級 路徑 也就是文件名 即 項目工程名context

可以先拿到 context 然後將其與之後獲取到的class對應起來,同理web.xml也一樣; 不能加載亂了,那個項目下那個web.xml和class要保持一致;

這裏用Map來暫時存儲項目對應的web.xml和class信息

    /**
     * 存儲web項目下的web.xml路徑便於之後解析xml
     */
    private static final Map<String, String> DEMO_XML = new HashMap<>();

    /**
     * 存儲web項目下web的對象路徑
     */
    private static final Map<String, String> DEMO_CLASS = new HashMap<>();

獲取項目名

        File file = new File(path);
        //根據路徑去加載類
        //1 獲取頂級文件名
        File[] files = file.listFiles();

        //設置項目Context
        List<Context> contextList = mapperContext.getHost().getContextList();


        //1 第一級 路徑 也就是文件名 即 項目工程名context
        for (File file1 : files) {
            String name = file1.getName();
            //設置context上下文路徑
            contextList.add(new Context(name));
            //遞歸處理 如果是WEB-INF 和 classes文件則特殊處理
            doFile(file1.getPath(), name);
        }

文件遞歸處理代碼 doFile , 將web.xml和class字節碼與項目對應起來存儲map中

    /**
     * 處理web.xml 和 獲取字節碼
     *
     * @param path
     * @param webDemoName
     */
    static void doFile(String path, String webDemoName) {

        File pathList = new File(path);
        File[] list1 = pathList.listFiles();
        if (list1 == null) {
            return;
        }
        //循環處理每個項目下web.xml
        for (File s : list1) {
            File file1 = new File(s.getPath());
            if (file1.isDirectory()) {
                doFile(file1.getPath(), webDemoName);
            } else {
                if (s.getName().equals("web.xml")) {
                    //保存當前項目下的web.xml
                    DEMO_XML.put(webDemoName, s.getPath());
                }
                //保存字節碼路徑  這裏目前只有一個class文件,其他業務class忽略...
                if (s.getName().endsWith(".class")) {

                    String classPath = s.getPath();
                    DEMO_CLASS.put(webDemoName, classPath);

                }

                //保存html文件
                if (s.getName().endsWith(".html")) {

                    String classPath = s.getPath();
                    DEMO_HTML.put(webDemoName, classPath);

                }
            }
        }

    }

解析web.xml

    /**
     * 讀取解析web.xml
     */
    private void doWebXml() {
        for (Map.Entry<String, String> stringStringEntry : DEMO_XML.entrySet()) {

            String context = stringStringEntry.getKey();
            String value = stringStringEntry.getValue();

            try {
                this.loadServlet(context, value);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }

        }

    }

加載解析web.xml,保存url,Servlet信息存儲到Wrapper集合中

   private void loadServlet(String context, String webXmlPath) throws FileNotFoundException {
        //存儲url  以及 配置servlet 以及請求url
        List<Wrapper> wrappersList = null;
        //獲取上下文
        List<Context> contextList = mapperContext.getHost().getContextList();
        for (Context context1 : contextList) {
            if (context.equals(context1.getName())) {
                wrappersList = context1.getWrappersList();
            }
        }

        //這裏讀取磁盤位置絕對路徑的xml
        InputStream resourceAsStream = new FileInputStream(webXmlPath);


        try {
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(resourceAsStream);
            Element rootElement = document.getRootElement();

            List<Element> selectNodes = rootElement.selectNodes("//servlet");
            for (int i = 0; i < selectNodes.size(); i++) {
                Element element = selectNodes.get(i);
                // <servlet-name>server</servlet-name>
                Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletnameElement.getStringValue();
                Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
                String servletClass = servletclassElement.getStringValue();

                // 根據servlet-name的值找到url-pattern
                Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
                // /server
                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                //servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());

                Wrapper wrapper = new Wrapper();
                wrapper.setServletClass(servletClass);
                wrapper.setUrl(urlPattern);
                //存儲servelt信心
                wrappersList.add(wrapper);

            }


        } catch (DocumentException e) {
            e.printStackTrace();
        }

    }


加載class字節碼,然後實例化根據web.xml中配置的全路徑信息保存在Wrapper類中

這兒的字節碼JVM默認是不能幫我們進行加載的,需要我們自己自定義類加載器加載解析

定義類加載器

參數classPath表示全路徑名如 /a/b/c.class

@SuppressWarnings("all")
public class SunClassloader extends ClassLoader {

	
	@Override
	public Class<?> findClass(String classPath) throws ClassNotFoundException {

 
		try (InputStream in = new FileInputStream(classPath)) {
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			int i = 0;
			while ((i = in.read()) != -1) {
				out.write(i);
			}
			byte[] byteArray = out.toByteArray();
			return defineClass(byteArray, 0, byteArray.length);
		} catch (Exception e) {
			e.printStackTrace();
		}
 
		return null;
 
	}
}

類加載實例化

 /**
     * 類加載實例化
     */
    public static void doNewInstance() {

        //獲取上下文集合
        List<Context> contextList1 = mapperContext.getHost().getContextList();
        //所有的上下文
        List<String> contextList = contextList1.stream().map(Context::getName).collect(Collectors.toList());

        //類加載實例化
        for (Map.Entry<String, String> stringStringEntry : DEMO_CLASS.entrySet()) {
            String webDemoName = stringStringEntry.getKey();
            String classPath = stringStringEntry.getValue();


            //加載class 然後實例化
            SunClassloader sunClazz = new SunClassloader();
            try {
                Class<?> clazz = sunClazz.findClass(classPath);
                //根據url查找項目對應的servlet
                if (contextList.contains(webDemoName)) {
                    contextList1.stream().forEach(x -> {
                        if (x.getName().equals(webDemoName)) {
                            List<Wrapper> wrappersList = x.getWrappersList();

                            //判斷當前類是否在web.xml配置的servlet class裏面
                            wrappersList.stream().forEach(x2 -> {
                                if (classPath.replaceAll("/", ".").contains(x2.getServletClass())) {
                                    //保存實例對象
                                    try {
                                        x2.setObject(clazz.newInstance());
                                    } catch (InstantiationException e) {
                                        e.printStackTrace();
                                    } catch (IllegalAccessException e) {
                                        e.printStackTrace();
                                    }
                                }
                            });

                        }
                    });
                }


            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

請求處理

客戶端請求,根據不同的上下文以及url去映射Mapper中查找servlet然後處理請求,故此我們需要對url進行解析

        List<Context> contextList = mapperContext.getHost().getContextList();
  • 根據這個路徑,得到上下文 web_Demo 以及請求url
   //獲取輸入流
            InputStream inputStream = accept.getInputStream();
            //封裝請求和響應對象
            Request request = new Request(inputStream);
            Response response = new Response(accept.getOutputStream());

            //請求url
            String url = request.getUrl();
            //獲取上下文
            String context = url.substring(0).substring(0, url.substring(1).indexOf("/") + 1);

            //真正請求的url
            String realUrl = url.replace(context, "");

判斷是否存在當前上下文,不存在就404

     boolean falg = false;
            //上下文
            Context context1 = null;
            //判斷上下文
            for (Context con : contextList) {
                String name = con.getName();
                if (context.equalsIgnoreCase("/" + name)) {
                    falg = true;
                    context1 = con;
                    break;
                }
            }
            if (!falg) {
                response.outPutStr(HttpUtil.resp_404());
                return;
            }

然後處理請求


            //獲取wrapper  處理請求
            List<Wrapper> wrappersList = context1.getWrappersList();
            for (Wrapper wrapper : wrappersList) {
                //靜態資源 html 請求
                if (realUrl.equals(wrapper.getUrl()) && url.endsWith(".html")) {
                    //html 暫時沒寫,,同servlet一樣
                    //剩下的當做servlet請求處理
                } else if (realUrl.equals(wrapper.getUrl())) {
                    HttpServlet httpServlet = (HttpServlet) wrapper.getObject();
                    //1 單線程處理
                    MyThread5 myThread = new MyThread5(httpServlet, response, request);
                    threadPoolExecutor.submit(myThread);
                }
            }

啓動類

    /**
     * 啓動入口
     *
     * @param args
     * @throws DocumentException
     */
    public static void main(String[] args) throws DocumentException {
        //啓動tomcat
        Bootstrap bootstrap = new Bootstrap();
        try {
            //加載配置server.xml文件
            bootstrap.loadServerXml();
            bootstrap.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

可以看到項目映射信息已經配置成功

在這裏插入圖片描述

獲取客戶端url和上下文

在這裏插入圖片描述

處理請求

在這裏插入圖片描述

後臺打印 在這裏插入圖片描述

三、總結

<font color=#999AAA >提示:這裏對文章進行總結: 以上就是Tomcat部署項目並且解析內容,本文僅僅簡單介紹Tomcat是如何將項目進行解析根據請求url處理請求,將項目信息存儲實例化加載到的Servlet信息映射起來,請求到來時候根據URL去Mapper映射關係中一層一層去查找到Servlet然後處理請求。

項目結構圖

在這裏插入圖片描述

五個小版本

分別在指定包下如v1,v2,v3,v4,v5每個代表一個版本

  • v1 簡單的返回指定字符串
  • v2 返回靜態頁面
  • v3 單線程處理servelt請求(多個請求會阻塞)
  • v4 多線程處理
  • v5 部署外部項目(多個項目,多線程處理) server包下是測試用生成的class字節碼servlet類,用於測試。

四、代碼地址

倉庫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章