@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類,用於測試。