spring boot热部署原理

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。

发布了164 篇原创文章 · 获赞 81 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/LiuRenyou/article/details/104619320
今日推荐