springboot-devtools源码-重启流程RestartApplicationListener(1)

1,034 阅读5分钟

大体流程

监听器注册,在发布starting事件时,捕获这个事件,替换classloader

细节

RestartApplicationListener.onApplicationStartingEvent

  1. 创建默认重启初始化器,其中主要维护了classloader加载class文件的路径

    // 有没有设置系统变量,设置了就创建默认的,没有设置初始化器为开发模式
    String enabled = System.getProperty(ENABLED_PROPERTY);
    		RestartInitializer restartInitializer = null;
    		if (enabled == null) {
    			restartInitializer = new DefaultRestartInitializer();
    		}
    		else if (Boolean.parseBoolean(enabled)) {
    			restartInitializer = new DefaultRestartInitializer() {
            // 开发模式classloader
    				@Override
    				protected boolean isDevelopmentClassLoader(ClassLoader classLoader) {
    					return true;
    				}
    
    			};
    		}
    
  2. 有没有使用热重启agent,如果没有,就标记为初始化时重启项目,否则反之,该步骤不做关心

    主要关心Restarter.initialize(args, false, restartInitializer, restartOnInitialize);

    // 有没有创建成功初始化器,创建成功后,判断是否有使用热部署插件,如果有就标记在初始化时重启项目
    if (restartInitializer != null) {
    			String[] args = event.getArgs();
    			// 判断是否有使用热部署初始化器,如果没有,就标记为在初始化时重启项目
    			boolean restartOnInitialize = !AgentReloader.isActive();
          // 重启器初始化
    			Restarter.initialize(args, false, restartInitializer, restartOnInitialize);
    		}
    		else {
    			// 若没有初始化成功初始化器,则禁用重启器
    			Restarter.disable();
    		}
    
  3. Restarter.initialize干了什么?

    参数:args(String[]):main方法的入参;forceReferenceCleanup(boolean):是否强制清除参数;initializer(RestartInitializer):重启初始化器;restartOnInitialize(boolean):是否在重启时初始化

    1. 线程锁住保证只会创建一个单例重启器保存下来‘
          Restarter localInstance = null;
          synchronized (INSTANCE_MONITOR) {
              if (instance == null) {
                // 创建重启器,关注这步骤
                  localInstance = new Restarter(Thread.currentThread(), args, forceReferenceCleanup, initializer);
                  // 赋值单例重启器
                  instance = localInstance;
              }
          }
      
      1. new Restarter干了什么?

        参数:当前线程,main方法的入参,是否强制清除参数,重启初始化器

        		// 创建静默退出控制器,线程相关知识,由于主线程不能直接获取子线程的线程信息,但是每个线程可以通过setUncaughtExceptionHandler 注册一个回调接口
        		// 这里就是创建了一个静默退出的回调器,赋值给了当前线程,当前线程抛出异常后,就能被主线程捕捉到,既然是禁摩退出,所以会在替换完成classloader之
        		// 后该线程抛出异常,实现线程切换
        		SilentExitExceptionHandler.setup(thread);
        		//强制清除参数标记
        		this.forceReferenceCleanup = forceReferenceCleanup;
        		// 初始化路径,从初始化器中获取
        		this.initialUrls = initializer.getInitialUrls(thread);
        		// 获取该线程的入口方法,用于重启
        		this.mainClassName = getMainClassName(thread);
        		// 获取该线程的类加载器——appClassLoader
        		this.applicationClassLoader = thread.getContextClassLoader();
        		// 主线程参数
        		this.args = args;
        		// 线程的回调
        		this.exceptionHandler = thread.getUncaughtExceptionHandler();
        		// 穿件一个泄露安全线程,放到list中,暂时不知道什么用
        		this.leakSafeThreads.add(new LeakSafeThread());
        
    2. 重启器初始化
      		if (localInstance != null) {
      		  // 初始化重启器
      			localInstance.initialize(restartOnInitialize);
      		}
      
    3. localInstance.initialize干了什么?
      1. 提前加载一些类到内存,确保它们不会拿到后续被替换的RestartLoader
      2. 设置上所有class的加载路径
      3. 如果restartOnInitialize(初始化时重启标记)为true,则立即重启(调用immediateRestart())
            // a
            preInitializeLeakyClasses();
            if (this.initialUrls != null) {
              // b
                this.urls.addAll(Arrays.asList(this.initialUrls));
                if (restartOnInitialize) {
                    // c
                    immediateRestart();
                }
            }
        
    4. immediateRestart干了什么?
      1. 拿到4.i.a中设置的泄露安全线程的第一个(队列)
      2. 设置回调,启动并暂停主线程线程,让这个线程先完成再说 start -> join
        1. 在泄露安全线程队列中继续追加一个新的线程对象
        2. 执行回调,回调就是启动方法,若启动失败,则异常退出,启动成功,清除缓存
          getLeakSafeThread().callAndWait(() -> {// 设置回调
          		start(FailureHandler.NONE);
          		cleanupCaches();
          		return null;
          	});
          // 重启完毕,该线程静默退出,注意上面一步虽然开启了线程去重启,但是使用了join是操作串行青睐了,所以
          // 只有当上面一步执行完毕后,才会退出该线程
          SilentExitExceptionHandler.exitCurrentThread();
          ===============================================>>> getLeakSafeThread()
          		try {
          		  // a. 拿到4.i.a中设置的泄露安全线程的第一个(队列)
          			return this.leakSafeThreads.takeFirst();
          		}
          		catch (InterruptedException ex) {
          		  // 中断此线程
          			Thread.currentThread().interrupt();
          			throw new IllegalStateException(ex);
          		}
          ===============================================>>> callAndWait()
                // 设置回调
          			this.callable = callable;
          			// 启动这个线程
          			start();
          			try {
          			// 强制先执行该线程的run返回
          				join();
          				return (V) this.result;
          			}
          			catch (InterruptedException ex) {
          				Thread.currentThread().interrupt();
          				throw new IllegalStateException(ex);
          			}
          ===============================================>>> run()
                  try {
                    // 之前取出来一个,这里再塞回去一个
          				  Restarter.this.leakSafeThreads.put(new LeakSafeThread());
          				  // 执行回调
          				  this.result = this.callable.call();
          			  }
          			  catch (Exception ex) {
          				  ex.printStackTrace();
          				  // 回调报错,异常退出
          				  System.exit(1);
          			  }
          ===============================================>>> this.callable.call()
                    // Restarter重启器的重启方法,传入一个流产方式,当start失败报错时流产,啥也不做直接return
                		start(FailureHandler.NONE);
                		// 无论是流产退出还是成功退出,才会向下执行清理缓存
                		cleanupCaches();
                		return null;
          
    5. Restarter.start干了什么?空壳方法,主要逻辑在doStart,doStart成功,或者被标记为失败就流产时(FailureHandler.NONE)才会退出。
      		do {
      		  // 执行启动方法
      			Throwable error = doStart();
      			// 启动成功,退出循环
      			if (error == null) {
      				return;
      			}
      			// 启动不成功,但是标记为流产,退出循环
      			if (failureHandler.handle(error) == Outcome.ABORT) {
      				return;
      			}
      		}
      		while (true);
      
    6. Restarter.doStart做了什么
      1. 获取class文件加载路径
      2. 获取更新文件
      3. 创建新的RestartClassLoader,将applicationClassLoader设置为他的爸爸,并将class文件加载路径和更新文件、日志记录器赋值给他
      4. 重新启动并返回(relaunch)
        		// a
        		URL[] urls = this.urls.toArray(new URL[0]);
        		// b
        		ClassLoaderFiles updatedFiles = new ClassLoaderFiles(this.classLoaderFiles);
        		// c
        		ClassLoader classLoader = new RestartClassLoader(this.applicationClassLoader, urls, updatedFiles, this.logger);
        		// d
        		return relaunch(classLoader);
        
    7. Restarter.relaunch做了什么?
      1. 创建重启器(线程)

        1. 设置入口方法所在全限定类名,和参数
        2. 设置线程名字
        3. 设置报错捕获器
        4. 标记该线程非守护线程(死了就死了,守护线程等所有非守护线程死了,他才能死)
        5. 设置上下文加载器为restartClassLoad(完成对classLoader的替换),如果系统有安全管理机制,先检查是否可以通过setContextClassLoader设置上下文类加载器。
          		this.mainClassName = mainClassName;
          		this.args = args;
          		setName("restartedMain");
          		setUncaughtExceptionHandler(exceptionHandler);
          		setDaemon(false);
          		setContextClassLoader(classLoader);
          
      2. 启动重启器,重启器强行加入线程 start -> join

        1. run干了什么(注意这个run跑完了才会切主线程)
          try {
            // 通过vii.a.a 设置的入口类和restartClassLoad获取入口类对象
          	Class<?> mainClass = Class.forName(this.mainClassName, false, getContextClassLoader());
          	// 通过入口类对象获取main方法
          	Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
          	// 执行main方法
          	mainMethod.invoke(null, new Object[] { this.args });
          }
          catch (Throwable ex) {
            // 这步先返回到 vii 再返回到 v 中通过doStart执行结果返回,如果没有异常this.error=null;
          	this.error = ex;
          	// 报错就捕获这个异常,通过异常捕获器将其抛出到主线程
          	getUncaughtExceptionHandler().uncaughtException(this, ex);
          }
          
      3. 返回重启器执行结果(是否报错)

        // a
        RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName, this.args,
        		this.exceptionHandler);
        // b
        launcher.start();
        launcher.join();
        // c
        return launcher.getError();