前言
热部署常用实现方案
1. ClassLoader 重新加载
ClassLoader 加载新的 class 文件,然后替换之前创建的对象。2. Java Agent
Java Agent,Java Agent 可以理解为 JVM 层面的 AOP,可以在类加载时将 class 文件的内容修改为自定义的内容,并且支持修改已加载到 JVM 的 class,不过对于已加载到 JVM 的 class 只能修改方法体,因此具有一定的局限性。spring-boot-devtools
spring-boot-devtools 快速上手
spring-boot-devtools 提供对热部署的支持,只要将这个依赖添加到类路径,当类路径下的 class 发生变化时就会自动重启应用上下文,从而使用新的 class 文件中的代码。这个插件的坐标如下。<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>optional 避免依赖传递,同时 spring-boot-maven-plugin 打包时也会忽略 spring-boot-devtools 插件。spring-boot-devtools 功能特性
spring-boot-devtools 作为一个开发环境的插件,不仅支持热部署,具体来说有以下特性。-
将第三方库(如 thymeleaf、freemarker)缓存相关的属性配置到 Environment,以便开发环境禁用缓存。 -
类路径下的 class 文件发生变更时触发 ApplicationContext重启。 -
内嵌 LiveReload 服务器,资源发生变化时触发浏览器刷新。 -
支持全局配置,所有的 Spring Boot 应用的 spring-boot-devtools插件使用同一套配置,如指定检查 class 文件变化的轮训时间。 -
支持远程触发热部署(不推荐使用)。
spring-boot-devtools 实现原理
spring-boot-devtools 支持添加配置用来修改自身行为,通常情况下我们使用默认配置即可,不再赘述配置相关内容。下面我们把重点放到 spring-boot-devtools 热部署的具体实现上。spring-boot-devtools 热部署使用了 ClassLoader 重新加载 的实现方式,具体来说使用两类 ClassLoader,一类是加载第三方库的 CladdLoader,另一类是加载应用类路径下 class 的自定义 RestartClassLoader,应用类路径下 class 变化会触发应用重新启动,由于不需要重新加载第三方库的 class,因此相比重新启动整个应用速度上会快一些。spring-boot-devtools 利用 Spring Boot 应用自动装配的特性,在 spring.factories 文件中添加了很多配置。
1. SpringApplication 启动时触发应用重启
spring-boot-devtools 通过 RestartApplicationListener 监听 SpringApplication 的启动,监听到启动时关闭当前线程,并重启应用,重启时使用自定义的 RestartClassLoader 加载应用类路径下的 class。监听 Spring Boot 应用启动的核心代码如下。public class RestartApplicationListener implements ApplicationListener<ApplicationEvent>, Ordered {
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartingEvent) {
onApplicationStartingEvent((ApplicationStartingEvent) event);
}
... 省略部分代码
}
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
String enabled = System.getProperty(ENABLED_PROPERTY);
if (enabled == null || Boolean.parseBoolean(enabled)) {
String[] args = event.getArgs();
DefaultRestartInitializer initializer = new DefaultRestartInitializer();
boolean restartOnInitialize = !AgentReloader.isActive();
// 初始化 Restarter
Restarter.initialize(args, false, initializer, restartOnInitialize);
} else {
Restarter.disable();
}
}
}
RestartApplicationListener 监听到 SpringApplication 启动事件后开始对 Restarter 进行初始化,Restarter 是重启应用的核心类,Restarter 初始化过程仅仅实例化自身并调用其初始化方法,初始化的核心代码如下。public class Restarter {
protected void initialize(boolean restartOnInitialize) {
preInitializeLeakyClasses();
if (this.initialUrls != null) {
this.urls.addAll(Arrays.asList(this.initialUrls));
if (restartOnInitialize) {
this.logger.debug("Immediately restarting application");
immediateRestart();
}
}
}
private void immediateRestart() {
try {
// 等待新线程执行结束
getLeakSafeThread().callAndWait(() -> {
start(FailureHandler.NONE);
cleanupCaches();
return null;
});
} catch (Exception ex) {
this.logger.warn("Unable to initialize restarter", ex);
}
// 再通过抛出异常的方式退出主线程
SilentExitExceptionHandler.exitCurrentThread();
}
}
Restarter 首先收集类路径的 URL,然后立即调用 #immediateRestart 方法重启应用,待新线程重启应用后再通过抛出异常的方式关闭 main 线程。启动应用的核心代码如下。public class Restarter {
private Throwable doStart() throws Exception {
Assert.notNull(this.mainClassName, "Unable to find the main class to restart");
URL[] urls = this.urls.toArray(new URL[0]);
ClassLoaderFiles updatedFiles = new ClassLoaderFiles(this.classLoaderFiles);
// 使用新的类加载器加载变化的类
ClassLoader classLoader = new RestartClassLoader(this.applicationClassLoader, urls, updatedFiles, this.logger);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Starting application " + this.mainClassName + " with URLs " + Arrays.asList(urls));
}
return relaunch(classLoader);
}
}
Restarter 先根据类路径下 URL 收集文件系统中的 class 文件到 ClassLoaderFiles,然后使用新的类加载器 RestartClassLoader 对应用重启,剩下的就很简单了,直接调用 main 方法即可。2. 类路径 class 文件变化时触发应用重启
ClassLoader 重启应用,对开发者而言,最重要的就是 class 文件发生变化时重启应用了。自动配置类位于 LocalDevToolsAutoConfiguration.RestartConfiguration,spring-boot-devtools 提供了一个 ClassPathFileSystemWatcher bean 用于监听 class 文件的变化。@Configuration(proxyBeanMethods = false)
@ConditionalOnInitializedRestarter
@EnableConfigurationProperties(DevToolsProperties.class)
public class LocalDevToolsAutoConfiguration {
@Lazy(false)
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true)
static class RestartConfiguration {
@Bean
@ConditionalOnMissingBean
ClassPathFileSystemWatcher classPathFileSystemWatcher(FileSystemWatcherFactory fileSystemWatcherFactory,
ClassPathRestartStrategy classPathRestartStrategy) {
URL[] urls = Restarter.getInstance().getInitialUrls();
ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher(fileSystemWatcherFactory,
classPathRestartStrategy, urls);
watcher.setStopWatcherOnRestart(true);
return watcher;
}
}
}
ClassPathFileSystemWatcher 实现了 InitializingBean 接口,会在初始化时启动一个线程监听 class 文件的变化,然后发送一个 ClassPathChangedEvent 事件,因此 spring-boot-devtools 还提供了一个对应的监听器。@Configuration(proxyBeanMethods = false)
@ConditionalOnInitializedRestarter
@EnableConfigurationProperties(DevToolsProperties.class)
public class LocalDevToolsAutoConfiguration {
@Lazy(false)
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true)
static class RestartConfiguration {
@Bean
ApplicationListener<ClassPathChangedEvent> restartingClassPathChangedEventListener(
FileSystemWatcherFactory fileSystemWatcherFactory) {
return (event) -> {
if (event.isRestartRequired()) {
// 类路径发生变化时重启应用上下文
Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory));
}
};
}
}
Restarter 再次重启了应用,流程与首次重启时类似,不再赘述。JRebel
spring-boot-devtools,Spring 官方推荐的另一个热部署工具是 JRebel。JRebel 的核心是一个普通的 jar 包,内置了对多种框架的支持,通过 java -jar 启动时指定 -javaagent 即可使用 JRebel,而无需修改代码。同时 JRebel 也提供了多种的 IDE 插件,避免了手动启动指定 agent。JRebel 在 Idea 中的使用
1. 下载
JRebel and XRebel,然后 install,之后重启 IDE 使插件生效。2. 激活
Help->JRebel->Activation 进入激活页面。https://jrebel.qekang.com/ 网站可以查找 可用的 Team URL,然后输入任意邮箱即可激活。3. 项目支持配置
View->Tool Windows->JRebel 对项目进行配置。rebel.xml 文件,这个文件用于配置 JRebel 监听的类路径。4. 自动编译配置
Build project automatically 开启自动构建功能。System Settings 页面下勾选 Save file if the IDE is idle for。5. 启动项目
JRebel 实现原理
ClassLoader 级别与 JVM 及应用集成。它不会创建新的 ClassLoader,当监测到 class 文件发生变化时通过扩展类加载器更新应用。推测:JRebel 通过 Java Agent 进行实现
-javaagent 指定这个 jar 包,因此可以猜测它使用到了 Java Agent 的某些特性。Java Agent 的主要作用为替换加载的 class,运行时修改方法体。由于 JRebel 支持在运行时添加、删除方法,因此 JRebel 必然不是通过运行时修改已加载到 JVM 的类路径下 class 方法体的方式来实现热部署的。那么大概率 JRebel 是修改了某些加载到 JVM 的 class。推测:JRebel 会在 class 文件发生变化后重新加载 class 文件
Java Agent 之后,我们还是不能了解其主要实现方式,不过当我们的 class 文件发生变动后,JRebel 必然会重新加载变动后的 class 文件,以便执行新的代码,因此我们可以在 ClassLoader 加载类的某个流程上打上断点,以便查看堆栈信息。@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/hello")
public String hello() {
return "hallo";
}
}
rebel-change-detector-thread 线程监测 class 文件的变动,文件变动后使用 AppClassLoader 加载了 com.zzuhkp.DemoApplication 开头的类,并且类名后还带了 $$M$_jr_ 开头的后缀。可以想到的是同一个 ClassLoader 只能加载一个类,因此 JRebel 对类名进行了修改。这也是官网所描述的,不创建新的 ClassLoader,当 class 发生变化时更新应用。问题:JRebel 如何替换新的 class 的?
@SpringBootApplication
@RestController
public class DemoApplication {
private String str;
public DemoApplication() {
this.str = "你好";
}
public DemoApplication(String str) {
this.str = "你好呀";
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/hello")
public String hello() {
return str;
}
}
/hello 接口,发现返回 你好 二字,可以看出 JRebel 会自动使用无参的构造方法实例化对象。handler 处理请求,如果新添加一个 Controller 方法,那么它必然被注册为 handler 才能处理请求。我们添加一个 hello2 的方法,并在注册 handler 的流程上打断点。@GetMapping("/hello2")
public String hello2() {
return str;
}
JRebel 小结
总结
spring-boot-devtools 会引入新的依赖,并且 class 文件变更会引起应用重启,而 JRebel 只会加载变动的 class 并利用 Spring 的 API 替换新的对象,因此 JRebel 比 spring-boot-devtools 会快上不少,相对来说比较个人比较支持使用 JRebel。