在原生Java web上實現ClassLoader熱部署,熱替換
開頭廢話
一直想實現一個原創的熱部署功能,其實Spring Loader,還有Tomcat都實現了相關的功能,而且實現的熱部署工非常強大,但是這個畢竟是別人的東西,即使再好用,如果不是自己實現一個,這份知識就永遠不屬於自己。
實現開頭
要實現熱替換,一般想到的是實現classLoader(如果您對ClassLoader還不是很瞭解的,請查閱這篇部落格https://www.ibm.com/developerworks/cn/java/j-lo-classloader/ 寫得非常全面),沒錯,我也是通過classLoader去進行實現的,而我要分享的是自定義ClassLoader實現熱部署功能我所遇到的最麻煩的大坑。
最麻煩的大坑
在 Java 中,即使是同一個類檔案,如果是由不同的類載入器例項載入的,那麼它們的型別是不相同的。如果認為是同一個類,就出現ClassCastException異常,也就類轉換異常。這就意味著,我們在實現熱替換的時候,我們自定義的classLoader所生產的類物件,不能被賦值到原專案裡的所對應的類物件。
可能有些讀者會說,這樣的話,讓原來的classLoader重新載入Class檔案不就行了嗎?很抱歉。。ClassLoader物件是不允許相同classs檔案重新載入的。
解決方案
那有沒有辦法讓自定義ClassLoader所生產的類物件賦值給原來對應的物件變數上呢?還真有。。就是我們平時經常會用到的通過介面的方式進行賦值。怎麼做?我們可以定義一個如 IBinService,然後實現這個IBinService的IBinServiceImpl,而我們平時引用IBinService介面而不是IBinServiceImpl,當我們對IBinServiceImpl進行修改,並對原IBinServiceImpl進行熱替換,由於引用IBinServiceImpl是地方是通過IBinService,就能直接進行賦值。(我寫的這段話可能很多讀者會覺得很模糊,您可以通過
可能很多讀者會覺得很奇怪,不是說在 Java 中,即使是同一個類檔案,如果是由不同的類載入器例項載入的,那麼它們的型別是不相同的嗎?我的理解是這樣的:因為我們自定義的classLoader是通過原專案new出來,這一點很關鍵,因為new關鍵字 其實就是等同於在原專案的ClassLoader上載入我們自定義的classLoader,而原ClassLoader就是我們自定義的ClassLoader的父級,其中僅僅指定 IBinServiceImpl類由 我們自定義的載入,而其實現的 IBinService介面檔案會委託給原專案ClassLoader,這樣哪怕IBinService和IBinService是兩個不同的classLoader生成出來的,但是由於他們ClassLoader是父子關係且不是同一個類卻又繼承關係,就滿足的java賦值條件,使得可以進行賦值。
但是我們沒有理由對每個類加上介面實現吧。。
所以我繼續思考,找資料。然後想到為啥那麼多熱部署框架可以做到那麼無縫接入到我們的專案上進行熱部署呢?。。於是我在devtools這個框架搜其原理恍然大悟。。原來devtools使用了【兩個ClassLoader,一個Classloader載入那些不會改變的類(第三方Jar包),另一個ClassLoader載入會更改的類,稱為 restart ClassLoader ,這樣在有程式碼更改的時候,原來的restart ClassLoader 被丟棄,重新建立一個restart ClassLoader】注:網上搜到的。。我是複製過來的。
也就是我們可以讓我們的專案的程式碼檔案全部通過自己定義的ClassLoader進行載入,然後熱部署的時候重新new一個ClassLoader出來,重新載入一個專案的class檔案,將原來的ClassLoader丟棄回收即可。。確實是簡單粗暴的方法。難怪很多大牛說不能再生產區上使用熱部署,因為效能問題和很多潛在問題都無法預料。
拿到了啟發以後,我現在開始實現程式碼了:
程式碼實現
web.xml
要想實現由自定義ClassLoader去載入Servlet,我們就不可以用一個路徑一個Servlet的配置在web.xml上,因為web容器一般都是有自己的classLoader,然後根據web.xml的配置資訊去載入專案的相關類,這種情況下,我們就很難實現我們想要的功能。所以我們簡單粗暴的點,直接用一個通用的Servlet進行管理所有的請求,通過一個Servlet去實現MVC架構。
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>binHotServlet</servlet-name>
<servlet-class>bin.framework.servlet.BinHotServlet</servlet-class>
<init-param>
<param-name>controllerList</param-name>
<param-value>bin.framework.controller.IndexController</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>binHotServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
BinHotServlet.java
package bin.framework.servlet;
import bin.framework.classloader.BinUrlClassLoader;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
public class BinHotServlet extends HttpServlet {
private List<Object> controllerObjList;
private BinUrlClassLoader binUrlClassLoader;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
if(requestURI.indexOf("binHotServlet/hot")!=-1){//只要是這個路徑的就表示進行熱換
clearClsAndObj();
loadObj();
resp.getWriter().print("hot replace complete ! ! !");
}
String[] requestUriPathInfos = requestURI.split("/");
if(requestUriPathInfos.length!=3){
return;
}
String controllerStr = requestUriPathInfos[1];
String controllerMethodStr = requestUriPathInfos[2];
for (Object controllerObj :
controllerObjList) {
Class<?> controllerCls = controllerObj.getClass();
String controllerClsName = controllerCls.getSimpleName();
controllerClsName = toLowerCaseFirstOne(controllerClsName);
if (controllerClsName.equals(controllerStr)) {
try {
Method method = controllerCls.getMethod(controllerMethodStr);
Object obj = method.invoke(controllerObj);
String result = obj.toString();
resp.getWriter().print(result);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
break;
}
}
}
@Override
public void init() throws ServletException {
super.init();
loadObj();
}
/**
* 通過自定義的ClassLoader進行載入專案的class檔案
*/
private void loadObj(){
ServletConfig servletConfig = getServletConfig();
String controllerArrStr = servletConfig.getInitParameter("controllerList");
if (controllerArrStr == null) {
throw new RuntimeException("no Controller set in controlleList !!!!");
}
ClassLoader classLoader = getClass().getClassLoader();
binUrlClassLoader =new BinUrlClassLoader(classLoader);
String[] controllerStrArr = controllerArrStr.split(",");
controllerObjList = new LinkedList<>();
for (int i = 0; i < controllerStrArr.length; i++) {
String controllerStr = controllerStrArr[i];
try {
Class<?> controllerCls = binUrlClassLoader.loadClass(controllerStr);
Object controllerObj = controllerCls.newInstance();
controllerObjList.add(controllerObj);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
/**
* 置空回收ClassLoader和controllerObjList
*/
private void clearClsAndObj(){
binUrlClassLoader=null;
controllerObjList=null;
}
/**
* 轉換類的首字母為小寫
*/
private String toLowerCaseFirstOne(String s) {
if (Character.isLowerCase(s.charAt(0)))
return s;
else
return (new StringBuilder()).append(Character.toLowerCase(s.charAt(0))).append(s.substring(1)).toString();
}
}
首先我是通過過載init方法,在init()方法中獲取web.xml上配置的controller層的類,再去獲取當前BinHotServlet的ClassLoader,注意,此時的classLoader是我們的tomcat用於載入我們專案的classLoader,這個classLoader已經載入了tomcat上所需要的類,如HttpServlet等,我將這個類傳進我們自定義的ClassLoader上,可能有些讀者會問什麼要傳Tomcat的ClassLoader,這是因為我們自定義的ClassLoader需要在載入BinHotServelt的時候,還需要載入HttpServlet,而HttpServlet是Tomcat上的類,並不屬於我們專案的,而我們的ClassLoader在載入BinHotServlet因為繼承了HttpServlet,如果拿不到HttpServlet就會丟擲異常,可能讀者會說我可以讓自定義ClassLoader去載入Tomcat的Java包,我想說,如果是這樣就會出現載入重複的類到記憶體中,除了開銷大意外,還會出現意想不到難以解決的問題,完全吃力不討好。。
在例項化自定義ClassLoader之後,我再通過web.xml的配置給BinHotServlet的controller層的Class類名進行相對應的例項化。再將這些Controller物件存在在controllerObjList集合中。
在這裡,我只是簡單的重寫doPost(),和doGet(),其他的請求方法,我就不再進行重寫,喜歡的讀者可以自己後續衍生,doGet的邏輯,直接呼叫doPost方法。所以我們可以直接看doPost方法。我是通過request的getRequestURI方法,獲取到請求路徑,再判斷請求是不是binHotServlet/hot介面,如果是的話,就直接置空掉當前的自定義ClassLoader和我們的controllerObjList集合,讓GC回收,這裡我只是簡單的置空,我還沒有對GC的回收機制進行一個非常詳細研究,如果有讀者對這方面有建議的話請在下方評論,或者加我qq892550156溝通~,謝謝。接著我就重新呼叫loadObj方法,重新new一個ClassLoader,重新載入專案的類檔案並重新例項化。
如果不是呼叫binHotServlet/hot介面,我們通過 "/" 拆分requestURI,然後第一部分是為controller,第二部分為controller的方法,然後通過controllerObjList進行查找出對應的Controller,通過反射進行呼叫該Controller的對應的方法。
BinUrlClassLoader
package bin.framework.classloader;
import java.io.*;
public class BinUrlClassLoader extends ClassLoader {
private String baseDir;
/**
* 判斷是否已經找到了BinUrlClassLoader的class檔案,主要是為減少判斷的效能消耗
*/
private boolean isFindBinUrlClassLoaderClass = false;
public BinUrlClassLoader(ClassLoader classLoader) {
super(classLoader);
File classPathFile = new File(BinUrlClassLoader.class.getResource("/").getPath());
baseDir = classPathFile.toString();
recursionClassFile(classPathFile);
}
/**
* 遍歷專案的class檔案
*/
private void recursionClassFile(File classPathFile) {
if (classPathFile.isDirectory()) {
File[] files = classPathFile.listFiles();
for (int i = 0; i < files.length; i++) {
File file = files[i];
recursionClassFile(file);
}
} else if (classPathFile.getName().indexOf(".class") != -1) {
getClassData(classPathFile);
}
}
/**
* 獲取類資料
*/
private void getClassData(File classPathFile) {
try {
if (!isFindBinUrlClassLoaderClass && classPathFile.getName().equals(BinUrlClassLoader.class.getSimpleName() + ".class")) {
isFindBinUrlClassLoaderClass = true;
} else {
InputStream fin = new FileInputStream(classPathFile);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int byteNumRead = 0;
while ((byteNumRead = fin.read(buffer)) != -1) {
bos.write(buffer, 0, byteNumRead);
}
byte[] classBytes = bos.toByteArray();
defineClass(getClassName(classPathFile), classBytes, 0, classBytes.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 獲取類檔案
*/
private String getClassName(File classPathFile) {
String classPath = classPathFile.getPath();
String packagePath = classPath.replace(baseDir, "");
String className = packagePath.replace("\\", ".").substring(1);
return className.replace(".class", "");
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if (cls == null) {
// cls = getSystemClassLoader().loadClass(name);
System.out.println(getSystemClassLoader().toString());
System.out.println(getParent());
cls=getParent().loadClass(name);
}
if (cls == null) {
throw new ClassNotFoundException(name);
}
if (resolve) {
resolveClass(cls);
}
return cls;
}
public static void main(String[] args) {
String classPtahStr = BinUrlClassLoader.class.getResource("").getPath();
File file = new File(classPtahStr + BinUrlClassLoader.class.getSimpleName() + ".class");
System.out.println(file.getName());
String baseDir = new File(BinUrlClassLoader.class.getResource("/").getPath()).toString();
System.out.println(baseDir);
String filePath = file.toString();
String packagePath = filePath.replace(baseDir, "");
System.out.println(packagePath);
String classPath = packagePath.replace("\\", ".").substring(1);
System.out.println(classPath);
}
}
主要是通過拿到當前BinUrlClassLoader所在的真實路徑的,從而拿到專案的真實根路徑,然後遞迴專案目錄結構,然後將專案的class檔案載入到BinUrlClassLoader中。
接下來就是我們的MVC熟悉架構,讀者可以不看
IndexController
package bin.framework.controller;
import bin.framework.service.IndexService;
import java.util.List;
public class IndexController {
private IndexService indexService;
public IndexController() {
indexService=new IndexService();
}
public List<String> getUserList(){
List<String> userList = indexService.getUserList();
return userList;
}
}
IndexService
package bin.framework.service;
import bin.framework.dao.IndexDao;
import java.util.List;
public class IndexService {
private IndexDao indexDao;
public IndexService(){
indexDao=new IndexDao();
}
public List<String> getUserList(){
List<String> userList = indexDao.getUserList();
return userList;
}
}
IndexDao
package bin.framework.dao;
import java.util.LinkedList;
import java.util.List;
public class IndexDao {
public List<String> getUserList(){
List<String> userList=new LinkedList<>();
userList.add("XIAO_MING");
userList.add("XIAO_HONG");
userList.add("XIAO_XI");
userList.add("Xiao_BIN");
return userList;
}
}
專案程式碼到此結束。。
怎麼用
我們就拿tomcat來試驗我們的功能,將Tomcat的server.xml上配置好我們的專案路徑:
記得將reloadable設定為false,表示class檔案改變的時候不用重新載入專案。就是禁止Tomcat的熱部署功能。
這是一開始的呼叫的indexController/getUserList
然後我再IndexDao上再加一個Xiao_QING,編譯成IndexDao.class後再將他覆蓋到tomcat的專案中。
再呼叫BinServlet/hot介面
再呼叫回IndexController/getUserList
非常成功~~~
如果您還有疑問或者對我的功能實現有什麼建議,歡迎大家在下方評論,或者加我QQ892550156進行溝通,謝謝