美文网首页
大佬说:怎么手写一个Tomcat然后、给这个Tomcat 部署项

大佬说:怎么手写一个Tomcat然后、给这个Tomcat 部署项

作者: 喊我小王吧 | 来源:发表于2020-12-20 12:08 被阅读0次

    @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类,用于测试。

    四、代码地址

    仓库

    相关文章

      网友评论

          本文标题:大佬说:怎么手写一个Tomcat然后、给这个Tomcat 部署项

          本文链接:https://www.haomeiwen.com/subject/xexknktx.html