作业要求
开发 Minicat V 4.0,在已有 Minicat 基础上进一步扩展,模拟出 webapps
部署效果。磁盘上放置一个 webapps
目录,webapps
中可以有多个项目,例如 demo1、demo2、demo3...
每个项目中含有 Servlet,可以根据请求 url 定位对应 Servlet 进一步处理。
作业具体要求参考以下链接文档:
作业资料说明:
-
提供资料:工程代码和自己的 webapps 以及访问路径、功能演示和原理讲解视频。
-
讲解内容包含:题目分析、实现思路、代码讲解。
-
效果视频验证:实现模拟 Tomcat 多项目部署效果,访问多个不同项目可获得动态返回的内容。
分析
- 添加 server.xml,需要在开启端口监听之前开始解析该配置文件,因为存在端口配置信息。
- 封装 Mapper 组件体系,源码中采用的是数组的形式,但是这里我使用的是 Map 集合的形式进行存储,方便查找。
- 接收到请求的时候,需要通过 Mapper 体系逐级锁定。
代码
-
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>
-
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; } }
-
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; } }
-
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; } }
-
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 进行封装,为了方便查找。
-
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"
。 -
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
方法进行修改。 -
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
下的映射信息。并且修改了请求处理部分代码。目前逻辑如下:
-
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 的。