上一篇文章 介绍了作为程序猿必备的 Tomcat
基础知识,这一篇咱们就来说说,如果我想自己写一个简易版的 Servlet
服务器应该怎么做?
一、实现梳理
这个阶段我们要大致把任务进行一下拆分,以便于后面更好的去发现问题、分析问题、解决问题。那么现在静下心想想,Servlet
服务器最需要解决的问题是什么?
-
如何获取请求?
获取请求的方式可以选择
Socket
,通过监听指定的服务器端口来拦截请求。 -
如何处理请求?
反射,肯定是需要用到反射的。项目启动时通过反射去吧
web
项目中的Servlet
实例化进容器,等用到的时候直接调用。 -
如果请求过多导致请求处理不及时,响应缓慢怎么处理?
线程,让一个请求对应一个线程。
-
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
下项目中的 Servlet
被 JVM
加载。
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>
三、结果展示
上面的三个类已将本项目的核心内容进行了展示,接下来就是看一下效果。
四、不足之处
- 虽然做出来个大概的样子,但是项目所用的
IO
模型为阻塞式的BIO
,性能不好。 - 自定义类加载器没有打破双亲委派模型,导致自定义类加载器去加载不同项目中的,具有相同全限定类名的类的时候会报如下的错误。
java.lang.LinkageError: loader (instance of loader/MyClassLoader): attempted duplicate class definition for name
源码
-------------------------------------- 吾志所向 一往无前 再接再厉 越挫越勇 --------------------------------------