Spring boot 热部署原理

 2022-08-16
原文地址:https://blog.51cto.com/u_12856278/3042754

类加载器

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类删除掉

202208162342241041.png

    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());
        }
    }

例子二:热更新

202208162342252352.png

    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

202208162342261743.png

例子三:例二的优化

例二还有几次不足之处,例如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。