spring boot热部署原理
类加载器
BootStrapClassLoader,简称B
启动类加载器,由c++实现,负责加载JAVA_HOME/lib目录的文件,他只按照文件名识别,名字不符合的类库即使放到lib下也不会被加载
ExtClassLoader,简称E
扩展类加载器,负责JAVA_HOME/lib/ext目录的文件
AppClassLoader,简称A
应用程序类加载器,负责加载用户类路径指定的类库
AppClassLoader和ExtClassLoader最终全部都是继承自抽象类ClassLoader, 这三个类加载器在宏观下有如下关系
ExtClassLoader是AppClassLoader的父亲,BootStrapClassLoader是ExtClassLoader的父亲,但是他们的父子关系并不是继承,而是组合关系,及如下:
AppClassLoader保留一个parent引用指向ExtClassLoader
ExtClassLoader的parent引用为null,可以当作是一个标记吧,如果一个类加载器的parent是null,就说明他的父亲是BootStrapClassLoader
双亲委派
双亲委派的工作流程如下:
当一个类加载器收到类加载请求,他不会自己去加载,而是委托parent去加载,每一个层级的类加载器都是如此,即最终所有的类都会委托给BootStrapClassLoader加载,有些类可能BootStrapClassLoader加载不了,他就反馈给子类,让子类自己加载,就相当于自下向上走一遍,再自上向下走一遍。
举个例子:如果有一个我们定义得类Test,如果没有自定义类加载器的情况下理应由AppClassLoader加载,此类的类加载流程如下:A委托E加载,E委托B加载,B尝试加载,B告知E无法加载,E尝试加载,E告知A无法加载,A自己加载
双亲委派的好处:保证了系统的class文件不会被篡改,保证了同一个class对象只会存在一份。
假如我们写一个java.lang.String的类,在此类被加载的时候会委托给B加载,B加载的是jdk的java.lang.String,而不是你写的这个类,就保证了不可能篡改,我们自己写的String类会被正常编译,但是永远运行不了。
不同的类加载器加载同一个类所得到的两个class对象是不等的,也就保证了同一个class对象只会存在一份
BEA三个类加载器的父子关系如何体现出来的
首先看看EA的父子关系在哪里体现出来的,不管如何做的,最终肯定都是A的parent引用是E
sun.misc.Launcher的构造方法精简版如下
public Launcher() {
Launcher.ExtClassLoader ecl= Launcher.ExtClassLoader.getExtClassLoader();
//把ecl当作引用传给AppClassLoader,此步会把AppClassLoader的parent置为ecl
this.loader = Launcher.AppClassLoader.getAppClassLoader(ecl);
}
再来看看BE的父子关系体现:
ClassLoader的loadClass精简版如下
protected Class<?> loadClass(String name){
//先从jvm缓存中查找
Class<?> c = findLoadedClass(name);
//缓存没有
if (c == null) {
//委托给父类
if (parent != null) {
c = parent.loadClass(name, false);
}
//父类为null,交给B加载,这里就体现了BE的父子关系
else {
c = findBootstrapClassOrNull(name);
}
//走完了所有类加载器都没有找到,findClass,ClassLoader的默认实现是跑异常
//我们可以重写此方法从磁盘或者网络加载class文件
if (c == null) {
c = findClass(name);
}
}
return c;
}
全盘委托
当一个ClassLoader装载一个类时,除非显示地使用另一个ClassLoader,则该类所依赖及引用的类也由这个CladdLoader载入,这个有一点抽象,待会在热更新会细谈
例子一:从磁盘读取class文件
注意我的MyCL类是直接放在src/main/java下的,在和MyCL同级目录下写一个Test类,里面写一个test方法随便写个输出语句,然后编译一下去target里面找到Test.class文件直接扔到D盘下,然后再把idea的Test类删除掉
public class MyCL extends ClassLoader{
//加载磁盘文件转换成class对象
@Override
public Class<?> findClass(String name) {
File file = new File("d:/Test.class");
InputStream is = null;
byte[] bytes = new byte[(int) file.length()];
try {
is = new FileInputStream(file);
is.read(bytes);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//class文件转对象,并且会把此class对象缓存在当前类加载器的jvm缓存,这样就不用每次都去磁盘找
return this.defineClass(name, bytes, 0, bytes.length);
}
public static void main(String[] args) throws Exception {
MyCL cl = new MyCL();
//把Test类删除后loadClass走双亲委派,经过三次类加载器都没办法加载到Test类,最后交给MyCL的findClass去加载
Class<?> clazz = cl.loadClass("Test");
clazz.getMethod("test").invoke(clazz.newInstance());
System.out.println(clazz.newInstance().getClass().getClassLoader());
}
}
例子二:热更新
public class Test {
public void test(){
System.out.println("version 1");
}
}
public class MyClassLoader extends ClassLoader {
public String rootPath; //rootPath = F:\lry\project\springboot2\target\classes
public List<String> clazzs;
public MyClassLoader(String rootPath,String... classPaths)throws Exception {
this.rootPath = rootPath;
this.clazzs = new ArrayList<>();
//classPath = F:\lry\project\springboot2\target\classes\cl
for(String classPath:classPaths){
scanClassPath(new File(classPath));
}
}
private void scanClassPath(File file) throws Exception {
if(file.isDirectory()){
for (File listFile : file.listFiles()) {
scanClassPath(listFile);
}
}else{
String fileName = file.getName();//MyClassLoader.class
String filePath = file.getPath();//F:\lry\project\springboot2\target\classes\cl\MyClassLoader.class
String suffix = fileName.substring(fileName.lastIndexOf("."));
if(suffix.equals(".class")){
InputStream is = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
is.read(bytes);
//className = cl.MyClassLoader
String className = fileNameToClassName(filePath);
//字节码bytes——>class文件,把clss文件放进jvm缓存,使得findLoadedClass能够找到
defineClass(className,bytes,0,bytes.length);
clazzs.add(className);
}
}
}
private String fileNameToClassName(String filePath) {
String className = filePath.replace(rootPath,"").replaceAll("\\\\",".");
className = className.substring(1,className.lastIndexOf("."));
return className;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//我们要热部署的代码已经被defineClass缓存起来,findLoadedClass都能找到,就不会走下面的双亲委派了
Class<?> loadClass = findLoadedClass(name);
//第一情况 这个类 不需要由我们加载
//第二种情况 这个类需要由我们加载 但是 确实加载不到
if(loadClass==null){
if (!clazzs.contains(name)){
loadClass = getSystemClassLoader().loadClass(name);
}else{
throw new ClassNotFoundException("没有加载到类"+name);
}
}
return loadClass;
}
public static void main(String[] args) throws Exception {
String rootPath = MyClassLoader.class.getResource("/").getPath().replaceAll("%20"," ");
rootPath = new File(rootPath).getPath();
// rootPath = F:\lry\project\springboot2\target\classes
while(true){
//每走一遍MyClassLoader的构造器就会重新加载一次磁盘文件
MyClassLoader cl = new MyClassLoader(rootPath,rootPath+"/cl");
Class<?> aClass = cl.loadClass("cl.Test");
aClass.getMethod("test").invoke(aClass.newInstance());
Thread.sleep(2000);
}
}
}
这段程序的效果就是当Test类动态修改时,不需要重新启动main,程序会自动更新,可以将main启动然后动态修改test方法的输出试试,注意每次修改都要点击一下build project,绿色的小锤子,点的时候如果提示你update啥,都点no
例子三:例二的优化
例二还有几次不足之处,例如while(true)应该改成文件改动监听器,Test类的方法test不支持new Test().test()调用,只能够由MyClassLoader加载出class文件再反射调用,本例针对上例做如下优化。
优化之前我先解释一下为什么例二 Test类的方法test不支持new Test().test()调用,你可以试试在while(true)里这样调用试试,因为全盘委托的原因,new Test().test()这样调用也就意味者main函数所在的类是谁加载的,那么new Test()走的也是那个类加载器,显然是由AppClassLoader加载的
添加文件监听器的依赖
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
public class MyClassLoader extends ClassLoader {
public String rootPath;
public List<String> clazzs;
public MyClassLoader(String rootPath,String... classPaths)throws Exception {
this.rootPath = rootPath;
this.clazzs = new ArrayList<>();
for(String classPath:classPaths){
scanClassPath(new File(classPath));
}
}
private void scanClassPath(File file) throws Exception {
if(file.isDirectory()){
for (File listFile : file.listFiles()) {
scanClassPath(listFile);
}
}else{
String fileName = file.getName();
String filePath = file.getPath();
String suffix = fileName.substring(fileName.lastIndexOf("."));
if(suffix.equals(".class")){
InputStream is = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
is.read(bytes);
String className = fileNameToClassName(filePath);
defineClass(className,bytes,0,bytes.length);
clazzs.add(className);
}
}
}
private String fileNameToClassName(String filePath) {
String className = filePath.replace(rootPath,"").replaceAll("\\\\",".");
className = className.substring(1,className.lastIndexOf("."));
return className;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> loadClass = findLoadedClass(name);
//第一情况 这个类 不需要由我们加载
//第二种情况 这个类需要由我们加载 但是 确实加载不到
if(loadClass==null){
if (!clazzs.contains(name)){
loadClass = getSystemClassLoader().loadClass(name);
}else{
throw new ClassNotFoundException("没有加载到类");
}
}
return loadClass;
}
public static void main(String[] args) throws Exception {
Application.run();
}
}
public class Test {
public void test(){
System.out.println("version 8");
}
}
public class Application {
public static String rootPath;
public static String packagePath = "/cl1";
public static void run() throws Exception {
//f://lry//project//springboot//target/classes
String rootPath = MyClassLoader.class.getResource("/").getPath().replaceAll("%20"," ");
rootPath = new File(rootPath).getPath();
Application.rootPath = rootPath;
//构造器会重新加载rootPath+packagePath下的class文件
MyClassLoader myClassLoader = new MyClassLoader(rootPath,rootPath+packagePath);
//用我们自己的类加载器加载程序入口
startFileListener(rootPath);
start0(myClassLoader);
}
public static void stop() {
System.out.println("程序退出");
//告诉jvm需要gc了
System.gc();
//告诉jvm可以清除对象引用
System.runFinalization();
}
//监听文件改动,一旦文件发生改动,则回调FileListener的onFileChange
public static void startFileListener(String rootPath) throws Exception {
FileAlterationObserver fileAlterationObserver = new FileAlterationObserver(rootPath);
fileAlterationObserver.addListener(new FileListener());
FileAlterationMonitor fileAlterationMonitor = new FileAlterationMonitor(1000);
fileAlterationMonitor.addObserver(fileAlterationObserver);
fileAlterationMonitor.start();
}
public static void start0(MyClassLoader myClassLoader) throws Exception {
Class<?> aClass = myClassLoader.loadClass("cl1.Application");
aClass.getMethod("start").invoke(aClass.newInstance());
//不可以直接调用start,只能用自己的myClassLoader反射调用start,全盘委托机制
//start();
}
public static void start(){
System.out.println("启动我们的应用程序");
new Test().test();
}
}
public class FileListener extends FileAlterationListenerAdaptor{
@Override
public void onFileChange(File file) {
if (file.getName().indexOf(".class")!= -1){
try {
//构造器会重新加载rootPath+packagePath下的class文件
MyClassLoader myClassLoader = new MyClassLoader(Application.rootPath,Application.rootPath+Application.packagePath);
Application.stop();
Application.start0(myClassLoader);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
springboot热部署源码
springboot版本 2.2.2.RELEASE
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnInitializedRestarter
@EnableConfigurationProperties({DevToolsProperties.class})
public class LocalDevToolsAutoConfiguration {
//监听到了ClassPathChangedEvent事件,就stop,重新加载最新的class文件再restart
@Bean
ApplicationListener<ClassPathChangedEvent> restartingClassPathChangedEventListener(FileSystemWatcherFactory fileSystemWatcherFactory) {
return (event) -> {
if (event.isRestartRequired()) {
Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory));
}
};
}
@Bean
FileSystemWatcherFactory fileSystemWatcherFactory() {
return this::newFileSystemWatcher;
}
private FileSystemWatcher newFileSystemWatcher() {
Restart restartProperties = this.properties.getRestart();
//文件监听器
FileSystemWatcher watcher = new FileSystemWatcher(true, restartProperties.getPollInterval(), restartProperties.getQuietPeriod());
//省略
return watcher;
}
//FileSystemWatcher 里面有一个关键方法如下,addListener会由spring ioc过程的afterPropertiesSet方法调用,会添加一个ClassPathFileChangeListener
public void addListener(FileChangeListener fileChangeListener) {
this.listeners.add(fileChangeListener);
}
//ClassPathFileChangeListener里面有一个关键方法如下
public void onChange(Set<ChangedFiles> changeSet) {
boolean restart = this.isRestartRequired(changeSet);
this.publishEvent(new ClassPathChangedEvent(this, changeSet, restart));
}
}
经过上面代码说明可以概括出热部署流程
spring ioc过程注册一个ClassPathFileChangeListener专门监听文件改动事件,当发现文件改动时发布事件ClassPathChangedEvent,当LocalDevToolsAutoConfiguration 监听到了ClassPathChangedEvent事件时就知道文件发生改动就会stop应用,gc,清除对象引用,再重新加载新文件,restart。