对于Java应用程序来说,热部署就是在服务器运行时重新部署项目
热部署在java应用中可以说是非常常见的一个技术了,springboot内部就使用了热部署。
注意,热部署与热加载是不同的技术,热部署一般用在生产环境,而热加载一般用在开发环境。热部署是对整个应用的整体替换,而热加载是对某个class进行替换。
要想实现热部署,我们必须对java的classloader机制有一定的了解,当然了解这些网上有很多好的文章,大家可以去了解一下,我这里只做概述,帮助大家回忆。
1.不同类加载器加载的同一个class文件,在jvm中生成的class对象是不同的,他们之间也不能进行类型转换。
2.双亲委派机制,某个类加载器在加载某个类之前,会先让自己的父类加载器去加载这个类,如果父类加载成功了,自己就不会再进行加载了,没有加载到才由自己尝试进行加载。我们要实现热部署必须要打破这种机制,否则会导致我们修改后的.class文件不能被重新的加载,因为一个类加载器对象只会加载某个类一次,已经加载的类就不会去重新加载class文件了,如果我们自定义的类加载器有双亲委派机制,他会先交给AppClassLoader这个类加载器去进行加载,这就导致了即使我们换了自定义classLoader对象,也不能使被修改过的class重新加载。
3.classloader的全盘委托机制,这个机制决定了我们应用的热部署的关键所在,可以让我们灵活的替换运行中的应用。
什么叫全盘委托机制,即当你使用某个自定义类加载器加载了某个class之后,那么这个class中所关联的其他的类,也都由我们这自定义的类加载器来完成,这个就非常的关键
自定义的类加载器
package com.cp.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author 12130
* @date 2019/11/11
* @time 19:59
*/
public class MyClassLoader extends ClassLoader {
/**
* 当前类加载器的根路径
*/
public String rootPath;
/**
* 当前类加载器能加载的所有的类
*/
public List<String> clazzName;
public MyClassLoader(String rootPath, String... classpath) throws IOException {
this.rootPath = rootPath;
clazzName = new ArrayList<>();
// 扫描并加载类 打破双亲委派机制
for (String path : classpath) {
findClass(new File(rootPath + path));
}
}
private void findClass(File file) throws IOException {
if (file.isDirectory()) {
final File[] files = file.listFiles();
for (File f : files) {
findClass(f);
}
} else {
FileInputStream fin = new FileInputStream(file);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
int d;
while ((d = fin.read()) != -1) {
bout.write(d);
}
fin.close();
byte[] bytes = bout.toByteArray();
String className = pathToClassName(file);
clazzName.add(className);
// 在当前类加载器中进行的define,该class就会被记录在当前类加载器中
defineClass(className, bytes, 0, bytes.length);
}
}
private String pathToClassName(File file) {
String className = file.getAbsolutePath().replace(rootPath, "");
String replace = className.replace(File.separator, ".");
return replace.substring(0, replace.lastIndexOf(".class"));
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 查看当前类是否被当前类加载器加载过
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
if (!clazzName.contains(name)) {
loadedClass = getSystemClassLoader().loadClass(name);
} else {
throw new ClassNotFoundException();
}
}
return loadedClass;
}
}
代表一个应用,用该类作为中介去监听class文件的变化和应用的重新加载
package com.cp.boot;
import com.cp.classloader.MyClassLoader;
import com.cp.listener.ClassFileChangeListener;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author 12130
* @date 2019/11/11
* @time 21:16
*/
public class Application {
/**
* 程序根路径
*/
public String rootPath;
/**
* 热部署程序的包路径,形如 com.cp.app
*/
public String classPath;
private FileAlterationMonitor monitor;
/**
* 需要被热部署的应用
*/
private Object app;
private String hotSwapClassName;
private String initMethodName;
/**
* @param clazz 程序的根路径,绝对路径
* @param hotSwapClassPath 需要被热替换的应用的class路径,形如 com\\cp\\a
* @param hotSwapClassName 热替换应用的启动类
* @param initMethodName 热替换应用的启动方法
* @throws Exception
*/
public Application(Class clazz, String hotSwapClassPath, String hotSwapClassName, String initMethodName) throws Exception {
classPath = hotSwapClassPath;
this.hotSwapClassName = hotSwapClassName;
this.initMethodName = initMethodName;
rootPath = clazz.getResource("/").getPath().substring(1).replace("/", File.separator);
startFileMonitor(rootPath + classPath);
}
public Object getApp() {
return app;
}
/**
* 启动热部署的应用
* @param classLoader 加载该应用使用的类加载器
* @throws Exception
*/
private void start0(ClassLoader classLoader) throws Exception {
final Class<?> aClass = classLoader.loadClass(hotSwapClassName);
app = aClass.newInstance();
Method method = aClass.getMethod(initMethodName);
new Thread(() -> {
try {
method.invoke(app);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}, "application-run-thread").start();
}
public void run() throws Exception {
// 每一次加载都新建一个类加载器来进行加载,这样才能加载到新的class
MyClassLoader classLoader = new MyClassLoader(rootPath, classPath);
start0(classLoader);
}
/**
* 文件变化监听
*
* @param monitorPath 监听的路径
* @throws Exception
*/
private void startFileMonitor(String monitorPath) throws Exception {
IOFileFilter filesFilter = FileFilterUtils.and(
FileFilterUtils.fileFileFilter(),
FileFilterUtils.suffixFileFilter(".class"));
ClassFileChangeListener classFileChangeListener = new ClassFileChangeListener();
classFileChangeListener.setApplication(this);
FileAlterationObserver observer = new FileAlterationObserver(new File(monitorPath), filesFilter);
observer.addListener(classFileChangeListener);
monitor = new FileAlterationMonitor(2000, observer);
// 开始监控
monitor.start();
}
/**
* 关闭程序
*
* @throws Exception
*/
public void close() throws Exception {
if (monitor != null) {
monitor.stop();
}
}
}
自定义的文件变化监听器,以此来监听文件的变化,然后通知上面的Application进行重新的加载
package com.cp.listener;
import com.cp.boot.Application;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import java.io.File;
import java.lang.reflect.Method;
/**
* @author 12130
* @date 2019/11/11
* @time 21:30
*/
public class ClassFileChangeListener extends FileAlterationListenerAdaptor {
private static final String LISTENER_SUFFIX = ".class";
/**
* 用来记录之前服务
*/
private Application application;
@Override
public void onFileChange(File file) {
try {
Method closeMethod;
// 加载启动了新的服务,必须要关闭之前的服务,否则之前的服务也会一直运行
if (application.getApp() != null && (closeMethod = application.getApp().getClass().getMethod("close")) != null) {
closeMethod.invoke(application.getApp());
}
/**
* 重新进行应用的加载
*/
application.run();
} catch (Exception e) {
e.printStackTrace();
}
}
public void setApplication(Application application) {
this.application = application;
}
}
测试启动
public class Main {
/**
* 类加载器有两个机制
* 1.双亲委派机制
* 2.全盘委托机制(一个类加载器加载了一个class之后,该class所引用的所有的类都是该类的类加载器进行加载)
* 也就是说如果我们使用自定义类加载器加载了一个类,那么该类中引用的所有的类以后都交给该自定义类加载器
*/
public static void main(String[] args) throws Exception {
Application app = new Application(Main.class, "com\\cp\\app\\", "com.cp.app.Tomcat", "run");
app.run();
}
}
测试的应用
public class Tomcat implements Closeable {
private boolean state = true;
public void run() {
while (state) {
System.out.println("程序运行中 version-123 ");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void close() throws IOException {
state = false;
}
}