@TOC
前言
提示:上篇我们自定义了简单的Tomcat,但是不能去部署web项目,当然也不能从根据url进行访问:
阅读本文前请先阅读: 自定义一个简单的Tomcat 即:自定义一个简单的Tomcat 可以访问静态页面,返回字符串等;
提示:如何在自定义Tomcat中部署外部的web项目呢?
一、怎么部署项目?
示例:通常我们部署项目是在Tomcat的webapps下面将打好的1个或多个war包进去,也可以配置响应的上下文以及具体的项目路径,然后tomcat会根据指定的路径去访问,这期间Tomcat是怎么来根据这个路径去解析这些项目?怎么去根据不同的url去找到不同的项目以及处理不同的请求?
二、分析以及思路
1.Tomcat的配置文件
精简后的server.xml
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
</Service>
</Server>
- Connector表示一些连接请求信息,包括强端口,超时时间,重定向端口,http协议版本;
- 可以根据Host来指定虚拟主机;
- 可以根据appBase来指定自己的项目的路径;
可以根据这个xml来配置tomcat端口以及访问的域名以及包路径等;
我们根据这个可以自定义自己的server.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<!-- 启动端口-->
<Connector port="8080"/>
<Engine>
<!-- 虚拟主机-->
<Host name="localhost"
appBase="/Users/pilgrim/Desktop/Mini-tomcat-main/TomcatDemo/src/webapps"
unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
</Service>
</Server>
2 web项目文件夹信息
Java Web 打包后文件目录
图片来自网络<font color=#999AAA >代码如下(示例):
然后我们可以根据这个图自定义一个web工程,如下图所示我已经建好了
简易版的web工程
在这里插入图片描述
这里建了两个工程web_Demo和web_Demo2
内容如下图所示:
web_Demo包
web.xml配置servlet信息
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>testServlet</servlet-name>
<servlet-class>server.MyServlet1</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>testServlet</servlet-name>
<url-pattern>/api/test1</url-pattern>
</servlet-mapping>
</web-app>
请求的MyServlet1 字节码文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package server;
import com.udeam.util.HttpUtil;
import com.udeam.v2.bean.Request;
import com.udeam.v2.bean.Response;
import com.udeam.v3.inteface.HttpServlet;
import java.io.IOException;
public class MyServlet1 extends HttpServlet {
public MyServlet1() {
}
public void init() throws Exception {
}
public void doGet(Request request, Response response) {
String contents = "<h2> GET 外部部署业务请求 </h2>";
System.out.println(contents);
try {
response.outPutStr(HttpUtil.resp_200(contents));
} catch (IOException var5) {
var5.printStackTrace();
}
}
public void doPost(Request request, Response response) {
String contents = "<h2> Post 外部部署业务请求</h2>";
try {
response.outPutStr(HttpUtil.resp_200(contents));
} catch (IOException var5) {
var5.printStackTrace();
}
}
public void destory() throws Exception {
}
}
web_Demo2包内容
web.xml配置servlet信息
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>testServlet</servlet-name>
<servlet-class>server.MyServlet2</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>testServlet</servlet-name>
<url-pattern>/api/test2</url-pattern>
</servlet-mapping>
</web-app>
请求的MyServlet2 字节码文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package server;
import com.udeam.util.HttpUtil;
import com.udeam.v2.bean.Request;
import com.udeam.v2.bean.Response;
import com.udeam.v3.inteface.HttpServlet;
import java.io.IOException;
public class MyServlet2 extends HttpServlet {
public MyServlet2() {
}
public void init() throws Exception {
}
public void doGet(Request request, Response response) {
String cc = "<h3> GET 外部部署MyServlet2业务请求 </h3>";
System.out.println(cc);
try {
response.outPutStr(HttpUtil.resp_200(cc));
} catch (IOException var5) {
var5.printStackTrace();
}
}
public void doPost(Request request, Response response) {
String content = "<h2> Post 外部部署MyServlet2业务请求</h2>";
try {
response.outPutStr(HttpUtil.resp_200(content));
} catch (IOException var5) {
var5.printStackTrace();
}
}
public void destory() throws Exception {
}
}
2.初始化项目配置
启动Tomcat的时候,会根据server.xml里面的配置监听端口信息
首先我们在启动main方法时候加载解析server.xml配置文件,拿到port端口便于之后监听8080端口
然后根据指定的appBase路径去加载项目信息如:那个包名(上下文),以及class,解析项目的web.xml拿到请求url信息以及维护好映射关系;
具体流程如下图
在这里插入图片描述- 首先启动Bootstartp类的main方法;
- 加载解析自定义tomcat的server.xml方法,得到启动端口,以及项目所在webapps路径;
- 解析webapps里的项目,解析当前项目的context,web.xml得到url映射关系;
- 最后处理请求,根据客户端的host以及上下文,还有url定位要处理的servelt然后提供请求返回给客户端;
需要注意的是在Tomcat server.xml中可以配置多个host,一个host下可以包含多个context也就是多个项目,然后context下是多个请求url
这儿我们仅限于一个host对应多个context,然后对应多个url,再根据url定位servlet;
定义映射类
public class MapperContext {
/**
* 虚拟主机
*/
private Host host;
public Host getHost() {
return host;
}
public void setHost(Host host) {
this.host = host;
}
}
- 1 Host这儿就不处理了,这儿用一个localhost请求;
一个host下对应多个Context
public class Host {
/**
* 虚拟主机名
*/
private String hostName;
/**
* Context 不同的项目名
*/
private List<Context> contextList;
public Host() {
this.contextList = new ArrayList<>();
}
public List<Context> getContextList() {
return contextList;
}
public void setContextList(List<Context> contextList) {
this.contextList = contextList;
}
public String getHostName() {
return hostName;
}
public void setHostName(String hostName) {
this.hostName = hostName;
}
}
- 2 定义Context对应的url和servlet映射关系
一个Context对应多个请求url
public class Context {
/**
* 请求url 用来锁定servlet
*/
private List<Wrapper> wrappersList;
/**
* context name 项目名 也就是上下文名
*/
String name;
public Context(String name) {
this.name = name;
wrappersList = new ArrayList<>();
}
public Context() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Wrapper> getWrappersList() {
return wrappersList;
}
public void setWrappersList(List<Wrapper> wrappersList) {
this.wrappersList = wrappersList;
}
}
- 3 请求url 用来锁定servlet
public class Wrapper {
private String url;
/**
* url对应的servlet实例
*/
private Object object;
/**
* web.xml里面配置的全限定名
*/
private String servletClass;
public String getUrl() {
return url;
}
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
public void setUrl(String url) {
this.url = url;
}
public String getServletClass() {
return servletClass;
}
public void setServletClass(String servletClass) {
this.servletClass = servletClass;
}
}
加载配置文件
加载 server.xml
获取端口和虚拟主机,以及webapps下的项目地址,并设置端口,设置虚拟主机到映射类中Host属性
public void loadServerXml() throws DocumentException {
//1 加载解析 server.xml文件
InputStream resourceAsStream = this.getClass().getResourceAsStream("/conf/server.xml");
SAXReader saxReader = new SAXReader();
Document read = saxReader.read(resourceAsStream);
//获取跟路径
Element rootElement = read.getRootElement();
Document document = rootElement.getDocument();
//2 获取端口
Element node = (Element) document.selectSingleNode("//Connector");
String port = node.attributeValue("port");
this.setPort(Integer.valueOf(port));
//3 获取host
Element element = (Element) document.selectSingleNode("//Host");
//虚拟主机
String localhost = element.attributeValue("name");
//虚拟主机
Host host = new Host();
host.setHostName(localhost);
mapperContext.setHost(host);
//部署的地址路径
String appBase = element.attributeValue("appBase");
//4 根据这个路径去解析里面的项目 映射端口和虚拟主机,项目,以及url->servlet
parseAppBase(appBase);
}
解析项目内容
根据appBase路径去解析每个项目的web.xml和加载class字节码
在这里插入图片描述解析web.xml
根据appBase路径去拿到项目名,如web_Demo
第一级 路径 也就是文件名 即 项目工程名context
可以先拿到 context 然后将其与之后获取到的class对应起来,同理web.xml也一样;
不能加载乱了,那个项目下那个web.xml和class要保持一致;
这里用Map来暂时存储项目对应的web.xml和class信息
/**
* 存储web项目下的web.xml路径便于之后解析xml
*/
private static final Map<String, String> DEMO_XML = new HashMap<>();
/**
* 存储web项目下web的对象路径
*/
private static final Map<String, String> DEMO_CLASS = new HashMap<>();
获取项目名
File file = new File(path);
//根据路径去加载类
//1 获取顶级文件名
File[] files = file.listFiles();
//设置项目Context
List<Context> contextList = mapperContext.getHost().getContextList();
//1 第一级 路径 也就是文件名 即 项目工程名context
for (File file1 : files) {
String name = file1.getName();
//设置context上下文路径
contextList.add(new Context(name));
//递归处理 如果是WEB-INF 和 classes文件则特殊处理
doFile(file1.getPath(), name);
}
文件递归处理代码 doFile , 将web.xml和class字节码与项目对应起来存储map中
/**
* 处理web.xml 和 获取字节码
*
* @param path
* @param webDemoName
*/
static void doFile(String path, String webDemoName) {
File pathList = new File(path);
File[] list1 = pathList.listFiles();
if (list1 == null) {
return;
}
//循环处理每个项目下web.xml
for (File s : list1) {
File file1 = new File(s.getPath());
if (file1.isDirectory()) {
doFile(file1.getPath(), webDemoName);
} else {
if (s.getName().equals("web.xml")) {
//保存当前项目下的web.xml
DEMO_XML.put(webDemoName, s.getPath());
}
//保存字节码路径 这里目前只有一个class文件,其他业务class忽略...
if (s.getName().endsWith(".class")) {
String classPath = s.getPath();
DEMO_CLASS.put(webDemoName, classPath);
}
//保存html文件
if (s.getName().endsWith(".html")) {
String classPath = s.getPath();
DEMO_HTML.put(webDemoName, classPath);
}
}
}
}
解析web.xml
/**
* 读取解析web.xml
*/
private void doWebXml() {
for (Map.Entry<String, String> stringStringEntry : DEMO_XML.entrySet()) {
String context = stringStringEntry.getKey();
String value = stringStringEntry.getValue();
try {
this.loadServlet(context, value);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
加载解析web.xml,保存url,Servlet信息存储到Wrapper集合中
private void loadServlet(String context, String webXmlPath) throws FileNotFoundException {
//存储url 以及 配置servlet 以及请求url
List<Wrapper> wrappersList = null;
//获取上下文
List<Context> contextList = mapperContext.getHost().getContextList();
for (Context context1 : contextList) {
if (context.equals(context1.getName())) {
wrappersList = context1.getWrappersList();
}
}
//这里读取磁盘位置绝对路径的xml
InputStream resourceAsStream = new FileInputStream(webXmlPath);
try {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
List<Element> selectNodes = rootElement.selectNodes("//servlet");
for (int i = 0; i < selectNodes.size(); i++) {
Element element = selectNodes.get(i);
// <servlet-name>server</servlet-name>
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 servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
// /server
String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
//servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
Wrapper wrapper = new Wrapper();
wrapper.setServletClass(servletClass);
wrapper.setUrl(urlPattern);
//存储servelt信心
wrappersList.add(wrapper);
}
} catch (DocumentException e) {
e.printStackTrace();
}
}
加载class字节码,然后实例化根据web.xml中配置的全路径信息保存在Wrapper类中
这儿的字节码JVM默认是不能帮我们进行加载的,需要我们自己自定义类加载器加载解析
定义类加载器
参数classPath表示全路径名如 /a/b/c.class
@SuppressWarnings("all")
public class SunClassloader extends ClassLoader {
@Override
public Class<?> findClass(String classPath) throws ClassNotFoundException {
try (InputStream in = new FileInputStream(classPath)) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
byte[] byteArray = out.toByteArray();
return defineClass(byteArray, 0, byteArray.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
类加载实例化
/**
* 类加载实例化
*/
public static void doNewInstance() {
//获取上下文集合
List<Context> contextList1 = mapperContext.getHost().getContextList();
//所有的上下文
List<String> contextList = contextList1.stream().map(Context::getName).collect(Collectors.toList());
//类加载实例化
for (Map.Entry<String, String> stringStringEntry : DEMO_CLASS.entrySet()) {
String webDemoName = stringStringEntry.getKey();
String classPath = stringStringEntry.getValue();
//加载class 然后实例化
SunClassloader sunClazz = new SunClassloader();
try {
Class<?> clazz = sunClazz.findClass(classPath);
//根据url查找项目对应的servlet
if (contextList.contains(webDemoName)) {
contextList1.stream().forEach(x -> {
if (x.getName().equals(webDemoName)) {
List<Wrapper> wrappersList = x.getWrappersList();
//判断当前类是否在web.xml配置的servlet class里面
wrappersList.stream().forEach(x2 -> {
if (classPath.replaceAll("/", ".").contains(x2.getServletClass())) {
//保存实例对象
try {
x2.setObject(clazz.newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
});
}
});
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
请求处理
客户端请求,根据不同的上下文以及url去映射Mapper中查找servlet然后处理请求,故此我们需要对url进行解析
- servlet配置的请求路径是:http://localhost:8080/web_Demo/api/test1
- 获取上下文
List<Context> contextList = mapperContext.getHost().getContextList();
- 根据这个路径,得到上下文 web_Demo 以及请求url
//获取输入流
InputStream inputStream = accept.getInputStream();
//封装请求和响应对象
Request request = new Request(inputStream);
Response response = new Response(accept.getOutputStream());
//请求url
String url = request.getUrl();
//获取上下文
String context = url.substring(0).substring(0, url.substring(1).indexOf("/") + 1);
//真正请求的url
String realUrl = url.replace(context, "");
判断是否存在当前上下文,不存在就404
boolean falg = false;
//上下文
Context context1 = null;
//判断上下文
for (Context con : contextList) {
String name = con.getName();
if (context.equalsIgnoreCase("/" + name)) {
falg = true;
context1 = con;
break;
}
}
if (!falg) {
response.outPutStr(HttpUtil.resp_404());
return;
}
然后处理请求
//获取wrapper 处理请求
List<Wrapper> wrappersList = context1.getWrappersList();
for (Wrapper wrapper : wrappersList) {
//静态资源 html 请求
if (realUrl.equals(wrapper.getUrl()) && url.endsWith(".html")) {
//html 暂时没写,,同servlet一样
//剩下的当做servlet请求处理
} else if (realUrl.equals(wrapper.getUrl())) {
HttpServlet httpServlet = (HttpServlet) wrapper.getObject();
//1 单线程处理
MyThread5 myThread = new MyThread5(httpServlet, response, request);
threadPoolExecutor.submit(myThread);
}
}
启动类
/**
* 启动入口
*
* @param args
* @throws DocumentException
*/
public static void main(String[] args) throws DocumentException {
//启动tomcat
Bootstrap bootstrap = new Bootstrap();
try {
//加载配置server.xml文件
bootstrap.loadServerXml();
bootstrap.start();
} catch (IOException e) {
e.printStackTrace();
}
}
可以看到项目映射信息已经配置成功
在这里插入图片描述获取客户端url和上下文
在这里插入图片描述处理请求
在这里插入图片描述后台打印
在这里插入图片描述
三、总结
<font color=#999AAA >提示:这里对文章进行总结:
以上就是Tomcat部署项目并且解析内容,本文仅仅简单介绍Tomcat是如何将项目进行解析根据请求url处理请求,将项目信息存储实例化加载到的Servlet信息映射起来,请求到来时候根据URL去Mapper映射关系中一层一层去查找到Servlet然后处理请求。
项目结构图
在这里插入图片描述五个小版本
分别在指定包下如v1,v2,v3,v4,v5每个代表一个版本
- v1 简单的返回指定字符串
- v2 返回静态页面
- v3 单线程处理servelt请求(多个请求会阻塞)
- v4 多线程处理
- v5 部署外部项目(多个项目,多线程处理)
server包下是测试用生成的class字节码servlet类,用于测试。
网友评论