开头废话
一直想实现一个原创的热部署功能,其实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,就能直接进行赋值。(我写的这段话可能很多读者会觉得很模糊,您可以通过https://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/ 该博客的 实现 Java 类的热替换 的内容中看到我这段的实例,因为我在研究ClassLoader的时候就看这篇文章才知道原来还可以这样形式,这篇文章就是我的老师)
可能很多读者会觉得很奇怪,不是说在 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进行沟通,谢谢
献上该功能的GitHub
https://github.com/bin892550156/BinHotReplace
最新方案:
不需要重启服务的热修复框架——这个一个基于Spring的热修复框架,该框架不需要重启服务,是一个针对特殊生产环境的热修复框架。
与dev-tool不一样,该框架不需要刷新Spring容器,也不监听class文件的变化,也不需要覆盖原有的class文件。