从事Java Web编程的朋友都知道,一个web应用的组成必须要有web.xml,因为这个是web应用程序的入口,通常我们会选择一种Web容器来发布我们的Web项目,比如Tomcat、Weblogic等容器服务程序。

这些程序都遵从一个标准的Web工程标准,他们启动时都会去主动读取指定位置的web.xml文件,web.xml中的各个节点都是既定的标准,Web容器会去按照规则读取配置信息将各种Class加载到jvm中,然后通过ServerSocket创建一个服务器对象监听指定的端口,接收请求并返回请求内容。

很多从事多年Java Web开发的伙伴都没有了解过这些Web容器,只知道按照标准创建Web工程,然后把工程放到容器里,启动容器就可以访问到工程了,对诸如Tomcat等容器服务程序仅仅停留在使用层面,并且说起这些容器程序往往带有一种朝圣的心态觉得这些容器很神秘。

其实我们做开发的,除了做好自己的本职工作之外,要对很多东西保持好奇并且要有探究的精神,说白了所有的程序都是从我们平时使用的简单的代码,根据一定的业务逻辑标准写出来的,通过探究逐渐完善我们在程序世界里的世界观,就好比馒头是用面粉做出来的,面条、面包、蛋糕等等都是用面粉做出来的,只是做的方式和手段以及步骤组合不一样,这样一来我们看待任何程序都可以一眼看穿他的基本套路。

好,说这么多废话,今天的主题是编写一个简单的Web容器,叫他TomDog好了,他的功能很简单,结构也很简单,代码只有4个类,然后有一个文件夹webapps,只要把符合我们定义规范的项目代码放进来,启动TomDog就可以访问到项目代码了。

先看一下我的TomDog的目录结构:

再看一下需要在TomDog中运行的web项目的结构,需要运行的项目都放在webapps文件夹中:

1. 需要运行的项目目录中必须有WEB-INF文件夹
2. 需要运行的项目目录的WEB-INF文件夹下必须有web.xml,这个文件中包含个该web项目的配置信息
3. 需要运行的项目目录的WEB-INF文件夹下必须有classes文件夹,这个文件夹下放的就是代码的包路径及class文件

然后来看一下testDemo1中的web.xml中的内容

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
	<app-name>testDemo1</app-name>
	<request>
		<url>/test1</url>
		<classpath>com.test.controller.Test1Controller</classpath>
		<method>test1</method>
	</request>
	<request>
		<url>/test2</url>
		<classpath>com.test.controller.Test1Controller</classpath>
		<method>test2</method>
	</request>
	<request>
		<url>/test3</url>
		<classpath>com.test.controller.Test2Controller</classpath>
		<method>test3</method>
	</request>
	<request>
		<url>/test4</url>
		<classpath>com.test.controller.Test2Controller</classpath>
		<method>test4</method>
	</request>
</web-app>

怎么样?是不是看不懂?看不懂就对了,就是为了让你看不懂!其实这里面配置的意思使用过servlet的应该都能猜个差不多,我为什么不直接用servlet的规则来写是因为我想告诉大家,所有的规则都是我们自己想出来的,我们觉得怎么写方便,规则就怎么定(=^ω^=)。

当然了现在servlet已经过时了大家都使用springmvc、struts等这种控制层框架,其实熟知这类框架原理的就明白,这些框架只是简化了开发配置,原理还是一样的,你使用springmvc你始终要在web.xml中通过servlet的方式配置一个你springmvc的配置文件,所以这么来看springmvc就是简化了不需要大家手动去一个一个的写servlet和servlet-mapping,而是直接可以通过注解实现请求和请求方法的映射关系。

好多余的话题不扩展了,现在来解释一下这段配置,app-name中配置的内容叫做项目名,每个request都是一个请求,其中的url是请求的路径,classpath是请求对应的java类,method是该请求执行的方法。

下面看下这个Test1Controller中的代码:

public class Test1Controller {
	
	public Object test1(){
		return "HTTP/1.1 200\r\n" +  
                "Content-Type: text/html\r\n" +  
                "Content-Length: 50\r\n" +  
                "Connection: keep-alive\r\n" + 
                "\r\n" +  
                "<h1>testDemo1 test1 controller do success</h1>";
	}
	
	public Object test2(){
		return "HTTP/1.1 200\r\n" +  
                "Content-Type: text/html\r\n" +  
                "Content-Length: 50\r\n" + 
                "Connection: keep-alive\r\n" + 
                "\r\n" +  
                "<h1>testDemo1 test2 controller do success</h1>";
	}
	
}

这里方法很简单,就是直接返回一个html代码,当前其中包含一些header的信息比如状态啦,长度啦之类的。

那么此时我如果启动TomDog,我给TomDog的默认配置的启动端口是8091,此时我只要在浏览器访问地址如下:

http://localhost:8091/testDemo1/test1

我就可以看到浏览器给我的返回结果如下图:

看到了吧,到这里一个简单的WEB工程标准被我们定义出来了,当然功能很简陋,但是麻雀虽小五脏俱全嘛!

好吧,热闹看完了,下面稍微讲下TomDog的实现方法,其实很简单,下面是启动的main方法:

public static void main(String[] args) {

		try {
			// 初始化項目配置信息
			initUrl();
			// 创建一个服务器对象,端口8091
			ServerSocket serverSocket = new ServerSocket(8091);
			// 创建一个客户端对象,这里的作用是用作多线程,必经服务器服务的不是一个客户端
			Socket client = null;
			boolean flag = true;
			while (flag) {
				System.out.println("服务器已启动,等待客户端请求。。。。");
				// accept是阻塞式方法,对新手来说这里很有可能出错,下面的注意事项我会说到
				client = serverSocket.accept();
				// 创建一个线程,每个客户端对应一个线程
				new Thread(new ResponseThread(client, APPURL)).start();
			}
			client.close();
			serverSocket.close();
			System.out.println("服务器已关闭。");
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

1. 初始化所有项目的请求,启动main方法时会去读取webapps下面的文件夹,并且按照我们的规范读取到web.xml,然后按照规范读取app-name然后读取各个request中的url,将app-name和request中的url拼接成一个字符串作为一个完整请求的url,将这个字符串做为key,classpath+method作为value存入一个全局的Map中。

2. 通过ServerSocket创建一个服务器对象并监听8091端口,然后通过Socket创建一个客户端对象,如果有请求发来serverSocket.accept()会得到一个Socket客户端请求,此时将该Socket传入我们写好的线程中即可。

3. 线程中干了什么事情呢?很简单,就是去获取本次请求的url然后去我们初始化的Map中找,找到后根据该Map的value找到这个请求的处理类,将他放到类加载器中通过反射机制执行该请求对应的方法,将方法的返回值通过输出流的方式写出去这样浏览器上就会得到响应。

看吧是不是很简单?下面我贴一下TomDog四个java类的完整代码出来,仅供参考,不需要深究使用的技术和性能,毕竟我们又不是要真的写一个Tomcat,还是要站在巨人的肩膀上的嘛!

StartServer.java

package com.tom.dog;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class StartServer {

	// 存放tomdog发布的全部项目的请求url
	public static Map<String, Map<String, String>> APPURL = new HashMap<>();

	public static void main(String[] args) {

		try {
			// 初始化項目配置信息
			initUrl();
			// 创建一个服务器对象,端口8091
			ServerSocket serverSocket = new ServerSocket(8091);
			// 创建一个客户端对象,这里的作用是用作多线程,必经服务器服务的不是一个客户端
			Socket client = null;
			boolean flag = true;
			while (flag) {
				System.out.println("服务器已启动,等待客户端请求。。。。");
				// accept是阻塞式方法,对新手来说这里很有可能出错,下面的注意事项我会说到
				client = serverSocket.accept();
				// 创建一个线程,每个客户端对应一个线程
				new Thread(new ResponseThread(client, APPURL)).start();
			}
			client.close();
			serverSocket.close();
			System.out.println("服务器已关闭。");
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	private static void initUrl() throws ParserConfigurationException, SAXException, IOException {
		// 成员变量没有数据时才走
		if (APPURL.size() != 0) {
			return;
		}

		// 读取TomDog下的全部项目
		File dir = new File(Configer.WEBAPPS);
		if (!dir.exists()) {
			dir.mkdirs();
		}
		// 获取webapps下的全部项目
		File[] dirfiles = dir.listFiles(new FileFilter() {

			public boolean accept(File file) {
				return file.isDirectory();
			}
		});

		// 沒有任何項目直接return
		if (dirfiles == null) {
			return;
		}

		// 循环项目获取项目根目录下的web.xml配置信息
		for (File file : dirfiles) {
			File webinfo = new File(file.getPath() + "\\" + Configer.WEBINF + "\\" + Configer.WEBXML);
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			Document doc = builder.parse(webinfo);
			// 获取项目名称
			String appNameStr = doc.getElementsByTagName(Configer.APPNAME).item(0).getFirstChild().getNodeValue();
			APPURL.put(appNameStr, new HashMap<String, String>());
			// 获取该项目的请求,类似servlet
			NodeList nl = doc.getElementsByTagName(Configer.REQUEST);
			// 封装项目和相对应的请求
			for (int i = 0; i < nl.getLength(); i++) {
				String urlStr = doc.getElementsByTagName(Configer.URL).item(i).getFirstChild().getNodeValue();
				String classpathStr = doc.getElementsByTagName(Configer.CLASSPATH).item(i).getFirstChild().getNodeValue();
				String methodStr = doc.getElementsByTagName(Configer.METHOD).item(i).getFirstChild().getNodeValue();
				APPURL.get(appNameStr).put("/" + appNameStr + urlStr, classpathStr + "." + methodStr);
			}
		}
	}
}

ResponseThread.java

package com.tom.dog;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

public class ResponseThread implements Runnable {

	private Socket client;

	public static Map<String, Map<String, String>> APPURL = new HashMap<>();

	public ResponseThread(Socket client, Map<String, Map<String, String>> appUrl) {
		this.client = client;
		this.APPURL = appUrl;
	}

	@Override
	public void run() {
		// run不需要自己去执行,好像是线程器去执行了来着,可以去看api
		try {
			Object invoke = getResult();
			client.getOutputStream().write(invoke.toString().getBytes());
			client.getOutputStream().flush();
			System.out.println("请求已响应完毕");
		} catch (Exception e) {
			System.out.println("error");
		}

	}

	private Object getResult() throws IOException, InstantiationException, IllegalAccessException,
			NoSuchMethodException, InvocationTargetException {
		BufferedReader in = null;
		String br = null;
		in = new BufferedReader(new InputStreamReader(client.getInputStream()));
		br = in.readLine();
		String url = getUrl(br);
		String appName = getAppName(url);
		String classpath = APPURL.get(appName).get(url);
		String classStr = classpath.substring(0, classpath.lastIndexOf("."));
		String methodStr = classpath.substring(classpath.lastIndexOf(".") + 1, classpath.length());
		String className = classStr.substring(classStr.lastIndexOf(".") + 1, classStr.length());
		String myPath = (Configer.WEBAPPS + url.replace("/", "\\").replace("\\" + methodStr, "") + "\\" + Configer.WEBINF + "\\" + Configer.CLASSES + "\\"
				+ classStr.replace(".", "\\")).replace("\\" + className, "") + "\\" + className + Configer.CLASSPOSTFIX;
		MyClassLoader myClassLoader = new MyClassLoader();
		Class<?> cls = myClassLoader.findClass(myPath + "|" + classStr);
		Object newInstance = cls.newInstance();
		Method method = cls.getMethod(methodStr);
		Object invoke = method.invoke(newInstance, null);
		return invoke;
	}
	
	private String getUrl(String allUrl) {
		String[] urls = allUrl.split(" ");
		String url = urls[1];
		if (url.indexOf("?") == -1) {
			return url;
		}
		String[] u = url.split("?");
		return u[0];
	}

	private String getAppName(String url) {
		String[] p = url.split("/");
		return p[1];
	}
}

MyClassLoader.java

package com.tom.dog;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class MyClassLoader extends ClassLoader {
	
	@Override
	protected Class<?> findClass(String myPath) {
		String[] split = myPath.split("\\|");
		byte[] cLassBytes = null;
		Path path = null;
		try {
			path = Paths.get(split[0]);
			cLassBytes = Files.readAllBytes(path);
		} catch (Exception e) {
			e.printStackTrace();
		}
		Class<?> clazz = defineClass(split[1], cLassBytes, 0, cLassBytes.length);
		return clazz;
	}
	
}

Configer.java

package com.tom.dog;

public class Configer {

	// 发布项目的存放路径
	public static String WEBAPPS = System.getProperty("user.dir") + "\\webapps";

	// 发布项目的初始化文件
	public static String WEBXML = "web.xml";

	// 初始化文件中代表当前应用名称配置的节点
	public static String APPNAME = "app-name";

	// 初始化文件中代表请求配置的节点
	public static String REQUEST = "request";

	// 初始化文件中代表请求url配置的节点
	public static String URL = "url";

	// 初始化文件中代表请求url对应处理类配置的节点
	public static String CLASSPATH = "classpath";

	// 初始化文件中代表请求url对应处理类处理方法配置的节点
	public static String METHOD = "method";
	
	// 项目class包文件夹所属的文件夹名称
	public static String CLASSES = "classes";
	
	// 项目文件的classes文件夹和web.xml的位置
	public static String WEBINF = "WEB-INF";
	
	// 项目文件的class的后缀
	public static String CLASSPOSTFIX = ".class";
}

Ok,以上就是TomDog的全部代码实现啦~~

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐