打工人也想有个属于自己的 Tomcat

上一篇文章 介绍了作为程序猿必备的 Tomcat 基础知识,这一篇咱们就来说说,如果我想自己写一个简易版的 Servlet 服务器应该怎么做?

一、实现梳理

这个阶段我们要大致把任务进行一下拆分,以便于后面更好的去发现问题、分析问题、解决问题。那么现在静下心想想,Servlet 服务器最需要解决的问题是什么?

  1. 如何获取请求?

    获取请求的方式可以选择 Socket,通过监听指定的服务器端口来拦截请求。

  2. 如何处理请求?

    反射,肯定是需要用到反射的。项目启动时通过反射去吧 web 项目中的 Servlet 实例化进容器,等用到的时候直接调用。

  3. 如果请求过多导致请求处理不及时,响应缓慢怎么处理?

    线程,让一个请求对应一个线程。

  4. webapps 里部署的项目中的 .class 文件无法被默认类加载器加载怎么办?

    自定义一个类加载来加载指定的文件就是了。

二、代码实现

2-1 项目结构

在这里插入图片描述

2-2 关键代码

Bootstrap 这个类是项目的启动类,里面主要定义了项目的初始化和启动 Socket 监听功能。

Bootstrap.java

import com.idol.web.container.ServletContainer;
import com.idol.web.servlet.http.HttpServlet;
import com.idol.web.util.Const;
import loader.MyClassLoader;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import processor.RequestProcessor;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.*;

/**
 * @author Supreme_Sir
 * @version 1.0
 * @className Bootstrap
 * @description 项目启动类
 * @date 2020/11/22 20:35
 **/
public class Bootstrap {
    
    
    /**
     * 线程池
     */
    private ThreadPoolExecutor threadPoolExecutor;
    /**
     * Servlet 容器
     */
    private ServletContainer servletContainer;
    /**
     * socket 监听端口
     */
    private static final Integer PORT = 8080;
    /**
     * 自定义类加载器
     */
    private MyClassLoader classLoader;

    public static void main(String[] args) throws IOException {
    
    
        Bootstrap bootstrap = new Bootstrap();
        // 初始化所有 web 项目
        bootstrap.initServlet();
        // 开始监听请求
        bootstrap.start();
    }

    /**
     * 启动 Socket 监听
     *
     * @throws IOException
     */
    public void start() throws IOException {
    
    
        // 创建监听指定端口的 ServerSocket
        ServerSocket serverSocket = new ServerSocket(PORT);
        do {
    
    
            // 开启对指定端口的阻塞监听
            Socket socket = serverSocket.accept();
            // 接收到 Socket 请求后进行处理。
            // 使用多线程处理请求,可避免因单线程环境下某一请求处理时间过长导致的后续请求处理不及时的问题
            RequestProcessor processor = new RequestProcessor(socket, servletContainer);
            threadPoolExecutor.execute(processor);
        } while (true);
    }

    /**
     * 初始化项目中的所有 Servlet
     */
    private void initServlet() {
    
    
        String miniCatAbsoulteRootPath = Bootstrap.class.getResource("/").getPath();
        File rootDir = new File(miniCatAbsoulteRootPath);
        // 找到 webapps 文件夹
        File[] listFiles = rootDir.listFiles(new FileFilter() {
    
    
            @Override
            public boolean accept(File pathname) {
    
    
                return Const.WEBAPPS_DIR.getVal().equals(pathname.getName());
            }
        });
        // 拿到 webapps 下的所有项目目录   eg: web1  web2  web3
        if (listFiles.length == 0) {
    
    
            return;
        } else {
    
    
            listFiles = listFiles[0].listFiles(new FileFilter() {
    
    
                @Override
                public boolean accept(File pathname) {
    
    
                    return pathname.isDirectory();
                }
            });
        }
        try {
    
    
            for (File file : listFiles) {
    
    
                if (!file.isDirectory()) {
    
    
                    continue;
                }
                // 拼接项目 web.xml 配置文件
                String webXml = miniCatAbsoulteRootPath + Const.WEBAPPS_DIR.getVal() + File.separator
                        + file.getName() + File.separator + "web.xml";
                File xmlFile = new File(webXml);
                if (!xmlFile.exists()) {
    
    
                    continue;
                }

                InputStream inputStream = new FileInputStream(xmlFile);
                SAXReader saxReader = new SAXReader();
                Document document = saxReader.read(inputStream);
                // 获取 web.xml 根节点
                Element root = document.getRootElement();
                // 获取跟节点下所有 Servlet 标签
                List<Element> selectNodes = root.selectNodes("//servlet");
                for (Element node : selectNodes) {
    
    
                    // 获取 servlet-name 标签中的值  eg: myServlet
                    Element servletNameElement = (Element) node.selectSingleNode("//servlet-name");
                    String servletName = servletNameElement.getStringValue();

                    // 获取 servlet-class 标签中的值   eg: com.idol.web.servlet.MyServlet_Web1
                    Element servletClassElement = (Element) node.selectSingleNode("//servlet-class");
                    String servletClass = servletClassElement.getStringValue();

                    // 获取与 servlet-name 标签值相同的 servlet-mapping 中 url-pattern 中的值   eg: /hello
                    Element servletMapping = (Element) root.selectSingleNode(
                            "/web-app/servlet-mapping[servlet-name='" + servletName + "']");
                    String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();

                    // 通过自定义类加载器实例化 HttpServlet 对象
                    HttpServlet httpServlet = (HttpServlet) classLoader.findClass(file.getName()
                            + "." + servletClass).newInstance();
                    // 将项目名称、URL 路径和 HttpServlet 实例放入 Servlet 容器
                    servletContainer.put(file.getName(), urlPattern.substring(1), httpServlet);
                }
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    public Bootstrap() {
    
    
        // 定义一个线程池
        // 线程池中常驻线程数
        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 = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
                keepAliveTime, unit, workQueue, threadFactory, handler);
        // 获取 servlet 容器
        servletContainer = ServletContainer.getInstance();
        // 创建自定义类加载器
        classLoader = new MyClassLoader();
    }
}

RequestProcessor 请求处理类,该类继承了 Thread 对象。结合 Bootstrap 启动类中的线程池,有效解决了单线程请求阻塞的问题。

RequestProcessor.java

import com.idol.web.bean.Request;
import com.idol.web.bean.Response;
import com.idol.web.container.ServletContainer;
import com.idol.web.servlet.http.HttpServlet;
import com.idol.web.util.StaticResourceUtil;

import java.net.Socket;

/**
 * @author Supreme_Sir
 * @version 1.0
 * @className processor.RequestProcessor
 * @description 请求处理器
 * @date 2020/11/20 22:33
 **/
public class RequestProcessor extends Thread {
    
    
    private Socket socket;
    private ServletContainer container;

    @Override
    public void run() {
    
    
        try {
    
    
            // 将 Socket 中的属性封装进自定义 Request 对象和 Response 对象
            Request request = new Request(socket.getInputStream());
            Response response = new Response(socket.getOutputStream());
            // 判断 Servlet 容器中是否包含指定请求方法
            if (container.containsKey(request.getWebName(), request.getResourcePath())) {
    
    
                HttpServlet httpServlet = container.get(request.getWebName(), 
                                                        request.getResourcePath());
                // 处理请求
                httpServlet.service(request, response);
            } else {
    
    
                // 获取指定静态资源文件如果没找到也相应 404页面
                String fileAbsolutePath = StaticResourceUtil.getFileAbsolutePath(request.getWebName(),
                	request.getResourcePath());
                response.outPutHtml(fileAbsolutePath);
            }
            socket.close();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    public RequestProcessor(Socket socket, ServletContainer servletContainer) {
    
    
        this.socket = socket;
        this.container = servletContainer;
    }
}

MyClassLoader 自定义类加载器,继承自 ClassLoader 类。该类通过双亲委派模型进行加载指定路径下的类文件。帮助 webapps 下项目中的 ServletJVM 加载。

MyClassLoader.java

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * @author Supreme_Sir
 * @version 1.0
 * @className MyClassLoader
 * @description 自定义类加载器
 * @date 2020/11/24 21:40
 **/
public class MyClassLoader extends ClassLoader {
    
    

    /**
     * 通过双亲委派模型加载指定类
     * @param name 项目名.类的全限定类名   eg:  web1.com.idol.web.servlet.MyServlet_Web1
     */
    @Override
    public Class<?> findClass(String name) {
    
    
        Class clazz = null;
        int idx = name.indexOf(".");
        // 获取项目名
        String projectName = name.substring(0, idx);
        // 获取 Servlet 全限定类名
        name = name.substring(idx + 1);
        // 拼接 class 文件所在路径
        String absolutePath = MyClassLoader.class.getResource("/").getPath() + "webapps" + File.separator
                + projectName + File.separator + name.replaceAll("\\.", "/") + ".class";
        // 获取 class 文件字节数组
        byte[] bytes = getByte(new File(absolutePath));

        if (bytes != null) {
    
    
            // 调用父类中的 defineClass 函数定义该类
            clazz = defineClass(name, bytes, 0, bytes.length);
        }
        return clazz;
    }

    /**
     * 获取指定 class 文件的字节数组
     * @param classFile 要加载的 class 文件
     */
    private byte[] getByte(File classFile) {
    
    
        FileInputStream in = null;
        ByteArrayOutputStream out = null;
        byte[] result = null;
        try {
    
    
            in = new FileInputStream(classFile);
            out = new ByteArrayOutputStream();

            byte[] buffer = new byte[1024];
            int size = 0;

            while ((size = in.read(buffer)) != -1) {
    
    
                out.write(buffer, 0, size);
            }
            result = out.toByteArray();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            try {
    
    
                in.close();
                out.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
        return result;
    }
}

web.xml 该文件为部署在容器中项目的配置文件。

web.xml

<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
    <servlet>
        <servlet-name>myServlet</servlet-name>
        <servlet-class>com.idol.web.servlet.MyServlet_Web1</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>myServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

三、结果展示

上面的三个类已将本项目的核心内容进行了展示,接下来就是看一下效果。

在这里插入图片描述

四、不足之处

  1. 虽然做出来个大概的样子,但是项目所用的 IO 模型为阻塞式的 BIO,性能不好。
  2. 自定义类加载器没有打破双亲委派模型,导致自定义类加载器去加载不同项目中的,具有相同全限定类名的类的时候会报如下的错误。
java.lang.LinkageError: loader (instance of  loader/MyClassLoader): attempted  duplicate class definition for name

源码

源码下载

-------------------------------------- 吾志所向 一往无前 再接再厉 越挫越勇 --------------------------------------

猜你喜欢

转载自blog.csdn.net/Supreme_Sir/article/details/111050113