阶段二模块一

作业要求

开发 Minicat V 4.0,在已有 Minicat 基础上进一步扩展,模拟出 webapps 部署效果。磁盘上放置一个 webapps 目录,webapps 中可以有多个项目,例如 demo1、demo2、demo3... 每个项目中含有 Servlet,可以根据请求 url 定位对应 Servlet 进一步处理。

作业具体要求参考以下链接文档:

https://gitee.com/lagouedu/alltestfile/raw/master/tomcat/Tomcat%E4%BD%9C%E4%B8%9A%E5%A4%A7%E9%A2%98.pdf

作业资料说明:

  1. 提供资料:工程代码和自己的 webapps 以及访问路径、功能演示和原理讲解视频。

  2. 讲解内容包含:题目分析、实现思路、代码讲解。

  3. 效果视频验证:实现模拟 Tomcat 多项目部署效果,访问多个不同项目可获得动态返回的内容。

分析

  1. 添加 server.xml,需要在开启端口监听之前开始解析该配置文件,因为存在端口配置信息。
  2. 封装 Mapper 组件体系,源码中采用的是数组的形式,但是这里我使用的是 Map 集合的形式进行存储,方便查找。
  3. 接收到请求的时候,需要通过 Mapper 体系逐级锁定。

代码

  1. server.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <Server>
        <Service>
            <Connector port="8080"/>
            <Engine>
                <Host name="localhost" base="C:/Users/Learner/webapps"/>
            </Engine>
        </Service>
    </Server>
    
  2. Mapper

    package cn.worstone.bean;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class Mapper {
        private Map<String, Host> hostMap = new HashMap<>();
    
        public Map<String, Host> getHostMap() {
            return hostMap;
        }
    }
    
  3. Host

    package cn.worstone.bean;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class Host {
        private String name;
        private String path;
        private Map<String, Context> contextMap = new HashMap<>();
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPath() {
            return path;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    
        public Map<String, Context> getContextMap() {
            return contextMap;
        }
    }
    
  4. Context

    package cn.worstone.bean;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class Context {
        private String name;
        private Map<String, Wrapper> wrapperMap = new HashMap<>();
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Map<String, Wrapper> getWrapperMap() {
            return wrapperMap;
        }
    }
    
  5. Wrapper

    package cn.worstone.bean;
    
    import cn.worstone.servlet.HttpServlet;
    
    public class Wrapper {
        private HttpServlet servlet;
    
        public HttpServlet getServlet() {
            return servlet;
        }
    
        public void setServlet(HttpServlet servlet) {
            this.servlet = servlet;
        }
    }
    

    Mapper 组件基本上都是使用 Map 进行封装,为了方便查找。

  6. Request

    package cn.worstone.bean;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    /**
     * 把请求信息封装为 Request 对象 (根据 InputStream 输入流进行封装)
     */
    public class Request {
        // 请求方式
        private String method;
        // 请求路径
        private String url;
        // 虚拟主机
        private String host;
    
        private InputStream inputStream;
    
        public String getMethod() {
            return method;
        }
    
        public void setMethod(String method) {
            this.method = method;
        }
    
        public String getUrl() {
            return url;
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    
        public String getHost() {
            return host;
        }
    
        public void setHost(String host) {
            this.host = host;
        }
    
        public InputStream getInputStream() {
            return inputStream;
        }
    
        public void setInputStream(InputStream inputStream) {
            this.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);
            String context = new String(bytes);
            // System.out.println("请求信息: ");
            // System.out.println(context);
            // 获取第一行请求信息
            String[] contextArr = context.split("\\n");
            String[] firstArr = contextArr[0].split(" ");
            this.method = firstArr[0];
            this.url = firstArr[1];
            System.out.println("Method: " + this.method);
            System.out.println("Url: " + this.url);
            // 获取第二行请求信息
            String[] secondArr = contextArr[1].split(" ");
            this.host = secondArr[1].replace("\r", "");
            System.out.println("Host: " + this.host);
        }
    }
    

    Request 中新增了一个虚拟主机 host,用来请求的虚拟主机信息,同时在处理请求的方法中,新增了虚拟主机信息的处理,注意这个地方别忘记处理 "\r"

  7. Response

    package cn.worstone.bean;
    
    import cn.worstone.util.HttpProtocolUtil;
    import cn.worstone.util.StaticResourceUtil;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    
    /**
     * 封装 Response 对象, 需要 OutputStream
     * 该对象提供核心方法, 返回 HTML
     */
    public class Response {
        private OutputStream outputStream;
    
        public Response() {
        }
    
        public Response(OutputStream outputStream) {
            this.outputStream = outputStream;
        }
    
        /**
         * 输出指定字符内容
         *
         * @param context
         * @throws IOException
         */
        public void output(String context) throws IOException {
            this.outputStream.write(context.getBytes());
        }
    
        /**
         * 根据 url 获取静态资源绝对路径, 进一步根据绝对路径读取该静态资源文件, 最终通过输出流输出
         *
         * @param path
         */
        public void outputHtml(String path) throws IOException {
            // 获取静态资源绝对路径
            String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);
            outputByAbsolutePath(absoluteResourcePath);
        }
    
        /**
         * 根据绝对路径获取静态资源文件, 并输出
         *
         * @param absolutePath
         * @throws IOException
         */
        public void outputByAbsolutePath(String absolutePath) throws IOException {
            // 处理文件路径包含中文的情况
            absolutePath = java.net.URLDecoder.decode(absolutePath, "utf-8");
    
            // 获取静态资源文件
            File file = new File(absolutePath);
            if (file.exists() && file.isFile()) {
                // 读取静态资源文件, 输出静态资源
                StaticResourceUtil.outputStaticResource(new FileInputStream(file), this.outputStream);
            } else {
                // 输出 404 信息
                output(HttpProtocolUtil.getHttpHeader404());
            }
        }
    }
    

    Response 中新增了一个方法 outputByAbsolutePath,用于通过绝对路径获取静态资源文件,并且添加了中文文件路径处理代码。由于方法中的内容与 outputHtml 方法十分相似,故此对 outputHtml 方法进行修改。

  8. RequestProcessor

    package cn.worstone.runable;
    
    import cn.worstone.bean.*;
    import cn.worstone.servlet.HttpServlet;
    import cn.worstone.util.HttpProtocolUtil;
    
    import java.io.InputStream;
    import java.net.Socket;
    import java.util.Map;
    
    public class RequestProcessor implements Runnable {
    
        private Socket socket;
    
        private Map<String, HttpServlet> servletMap;
    
        private Mapper mapper;
    
        public RequestProcessor(Socket socket, Map<String, HttpServlet> servletMap, Mapper mapper) {
            this.socket = socket;
            this.servletMap = servletMap;
            this.mapper = mapper;
        }
    
        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
    
                // 封装 Request 对象以及 Response 对象
                Request request = new Request(inputStream);
                Response response = new Response(socket.getOutputStream());
    
                // 解析 URL
                Host host = this.mapper.getHostMap().get(request.getHost());
                if (host != null) {
                    String url = request.getUrl();
                    int index = url.indexOf("/", 1);
                    index = index == -1 ? url.length() : index;
                    String contextStr = url.substring(0, index);
                    Context context = host.getContextMap().get(contextStr);
                    if (context != null) {
                        String wrapperStr = url.substring(index);
                        Wrapper wrapper = context.getWrapperMap().get(wrapperStr);
                        if (wrapper != null) {
                            HttpServlet httpServlet = wrapper.getServlet();
                            httpServlet.service(request, response);
                        } else {
                            response.outputByAbsolutePath(host.getPath() + request.getUrl());
                        }
                    } else {
                        // 不是 webapps 里面的请求
                        if (servletMap.get(request.getUrl()) == null) {
                            response.outputHtml(request.getUrl());
                        } else {
                            HttpServlet httpServlet = servletMap.get(request.getUrl());
                            httpServlet.service(request, response);
                        }
                    }
                } else {
                    // 输出 404 信息
                    response.output(HttpProtocolUtil.getHttpHeader404());
                }
    
                // 必须在线程里面关闭
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    因为需要解析 webapps 文件夹下的项目,所以新增了一个 Mapper 参数,其中存储 webapps 下的映射信息。并且修改了请求处理部分代码。目前逻辑如下:

在这里插入图片描述

  1. BootStrap

    package cn.worstone.server;
    
    import cn.worstone.bean.Context;
    import cn.worstone.bean.Host;
    import cn.worstone.bean.Mapper;
    import cn.worstone.bean.Wrapper;
    import cn.worstone.runable.RequestProcessor;
    import cn.worstone.servlet.HttpServlet;
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Element;
    import org.dom4j.io.SAXReader;
    
    import java.io.*;
    import java.lang.reflect.InvocationTargetException;
    import java.net.*;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.*;
    
    /**
     * Minicat 启动类
     */
    public class Bootstrap {
    
        // 监听端口号
        private int port = 8080;
    
        public int getPort() {
            return port;
        }
    
        public void setPort(int port) {
            this.port = port;
        }
    
        /**
         * Minicat 程序启动入口
         *
         * @param args
         */
        public static void main(String[] args) {
            Bootstrap bootstrap = new Bootstrap();
            try {
                // 启动 Minicat
                bootstrap.start();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * Minicat 启动时需要初始化展开的操作
         */
        public void start() throws Exception {
            // Minicat 1.0 版本
            // 需求: 浏览器请求 http://localhost:8080, 返回一个固定字符串到页面 "Hello Minicat !"
            /*
            ServerSocket server = new ServerSocket(this.port);
            System.out.println("Minicat start on port: " + this.port);
            while (true) {
                Socket socket = server.accept();
                // 接收 socket 请求, 获取输出流
                OutputStream outputStream = socket.getOutputStream();
                String context = "Hello Minicat !";
                String responseStr = HttpProtocolUtil.getHttpHeader200(context.getBytes().length) + context;
                outputStream.write(responseStr.getBytes());
                outputStream.close();
                socket.close();
            }
            */
    
            // Minicat 2.0 版本
            // 需求: 封装 Request Response 对象, 返回 HTML 静态资源文件
            /*
            while (true) {
                Socket socket = server.accept();
                InputStream inputStream = socket.getInputStream();
    
                // 封装 Request 对象以及 Response 对象
                Request request = new Request(inputStream);
                Response response = new Response(socket.getOutputStream());
    
                response.outputHtml(request.getUrl());
                socket.close();
            }
            */
    
            // Minicat 3.0 版本
            // 需求: 可以请求动态资源 (Servlet)
            // 加载解析相关配置 server.xml
            /*
            loadServlet();
    
            while (true) {
                Socket socket = server.accept();
                InputStream inputStream = socket.getInputStream();
    
                // 封装 Request 对象以及 Response 对象
                Request request = new Request(inputStream);
                Response response = new Response(socket.getOutputStream());
    
                if (servletMap.get(request.getUrl()) == null) {
                    response.outputHtml(request.getUrl());
                } else {
                    HttpServlet httpServlet = servletMap.get(request.getUrl());
                    httpServlet.service(request, response);
                }
    
                socket.close();
            }
            */
    
            // 多线程改造 (不使用线程池)
            /*
            loadServlet();
    
            while (true) {
                Socket socket = server.accept();
                RequestProcessor processor = new RequestProcessor(socket, this.servletMap);
                processor.start();
            }
            */
    
            // 多线程改造 (使用线程池)
            /*
            loadServlet();
    
            // 创建线程池
            int corePoolSize = 10;
            int maximumPoolSize = 50;
            long keepAliveTime = 100;
            TimeUnit unit = TimeUnit.SECONDS;
            BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
            ThreadFactory threadFactory = Executors.defaultThreadFactory();
            RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
            ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    
            while (true) {
                Socket socket = server.accept();
                RequestProcessor processor = new RequestProcessor(socket, this.servletMap);
                executor.execute(processor);
            }
            */
    
            // Minicat 4.0 版本
            // 要求: 模拟 webapps 部署效果
            loadServer();
            loadServlet();
    
            ServerSocket server = new ServerSocket(this.port);
            System.out.println("Minicat start on port: " + this.port);
    
            // 创建线程池
            int corePoolSize = 10;
            int maximumPoolSize = 50;
            long keepAliveTime = 100;
            TimeUnit unit = TimeUnit.SECONDS;
            BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
            ThreadFactory threadFactory = Executors.defaultThreadFactory();
            RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
            ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    
            while (true) {
                Socket socket = server.accept();
                RequestProcessor processor = new RequestProcessor(socket, this.servletMap, this.mapper);
                executor.execute(processor);
            }
        }
    
        private Mapper mapper = new Mapper();
    
        private void loadServer() {
            InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("server.xml");
            SAXReader reader = new SAXReader();
    
            try {
                Document document = reader.read(inputStream);
                Element rootElement = document.getRootElement();
                // 解析 Connector
                loadConnector(rootElement);
                // 解析 Host
                loadHost(rootElement);
    
            } catch (DocumentException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 解析 Connector
         *
         * @param rootElement
         */
        private void loadConnector(Element rootElement) {
    
            Element connectorElement = (Element) rootElement.selectSingleNode("//Connector");
            if (connectorElement != null) {
                String connector = connectorElement.attributeValue("port");
                if (connector != null && !"".equals(connector)) {
                    this.port = Integer.parseInt(connector);
                }
            }
        }
    
        /**
         * 解析 Host
         *
         * @param rootElement
         */
        private void loadHost(Element rootElement) {
    
            List<Element> hostElements = rootElement.selectNodes("//Host");
            for (Element hostElement : hostElements) {
                Host host = new Host();
                String name = hostElement.attributeValue("name");
                String path = hostElement.attributeValue("base");
                host.setName(name + ":" + this.port);
                host.setPath(path);
    
                File base = new File(path);
                if (base.exists() && base.isDirectory()) {
                    File[] files = base.listFiles(new FileFilter() {
                        @Override
                        public boolean accept(File file) {
                            return file.isDirectory();
                        }
                    });
                    for (File file : files) {
                        loadContext(host, file);
                    }
                }
    
                // 添加 Host
                this.mapper.getHostMap().put(name + ":" + this.port, host);
            }
        }
    
        /**
         * 解析 Context
         *
         * @param host
         * @param directory
         */
        private void loadContext(Host host, File directory) {
            Context context = new Context();
            context.setName(directory.getName());
            File[] files = directory.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    return "web.xml".equals(name);
                }
            });
            if (files != null && files.length > 0) {
                File file = files[0];
                if (file.exists() && file.isFile()) {
                    loadWrapper(context, file);
                }
            }
    
            host.getContextMap().put("/" + directory.getName(), context);
        }
    
        /**
         * 解析 Wrapper
         *
         * @param context
         * @param file
         */
        private void loadWrapper(Context context, File file) {
            SAXReader reader = new SAXReader();
    
            try {
                Document document = reader.read(file);
                List<Map<String, String>> properties = parse(document);
                for (Map<String, String> property : properties) {
                    String urlPattern = property.get("urlPattern");
                    String servletClass = property.get("servletClass");
                    HttpServlet httpServlet = loadClassByFilePath(file.getParentFile().getAbsolutePath(), servletClass);
                    Wrapper wrapper = new Wrapper();
                    wrapper.setServlet(httpServlet);
                    context.getWrapperMap().put(urlPattern, wrapper);
                }
            } catch (DocumentException e) {
                e.printStackTrace();
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
    
        }
    
        /**
         * 使用 URLClassLoader 加载 class 文件
         *
         * @param dir
         * @param servletClass
         * @return
         * @throws ClassNotFoundException
         * @throws NoSuchMethodException
         * @throws InvocationTargetException
         * @throws InstantiationException
         * @throws IllegalAccessException
         * @throws MalformedURLException
         */
        private HttpServlet loadClassByFilePath(String dir, String servletClass) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, MalformedURLException {
            URL[] urls = new URL[]{new URL("file:" + dir + "/")};
            URLClassLoader urlClassLoader = URLClassLoader.newInstance(urls);
            return (HttpServlet) urlClassLoader.loadClass(servletClass).getDeclaredConstructor().newInstance();
        }
    
        private Map<String, HttpServlet> servletMap = new HashMap<>();
    
        /**
         * 加载解析 server.xml, 初始化 Servlet
         */
        private void loadServlet() {
            InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
            SAXReader reader = new SAXReader();
    
            try {
                Document document = reader.read(inputStream);
                List<Map<String, String>> properties = parse(document);
                for (Map<String, String> property : properties) {
                    String urlPattern = property.get("urlPattern");
                    String servletClass = property.get("servletClass");
                    this.servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).getDeclaredConstructor().newInstance());
                }
            } catch (DocumentException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    
        private List<Map<String, String>> parse(Document document) {
            List<Map<String, String>> properties = new ArrayList<>();
            Element rootElement = document.getRootElement();
            List<Element> nodes = rootElement.selectNodes("//servlet");
            for (Element element : nodes) {
                Map<String, String> property = new HashMap<>(8);
                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 servletMappingElement = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
                String urlPattern = servletMappingElement.selectSingleNode("url-pattern").getStringValue();
    
                property.put("urlPattern", urlPattern);
                property.put("servletClass", servletClass);
                properties.add(property);
            }
            return properties;
        }
    
    }
    

    新增了解析 server.xml 相关方法,以及通过 URLClassLoader 类加载器加载非 classpath 下的类信息。因为解析 web.xml 文件代码相似,故此将其解析部分代码抽象为了 parse 方法。

问题

这次作业遇到做多的问题还是关于 URLClassLoader 的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值