大佬说:怎么手写一个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类,用于测试。

四、代码地址

仓库

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