其他
Spring Boot 开发环境热部署方案
前言
热部署常用实现方案
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 文件中的代码。这个插件的坐标如下。<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 应用启动的核心代码如下。@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
初始化过程仅仅实例化自身并调用其初始化方法,初始化的核心代码如下。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 线程。启动应用的核心代码如下。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 文件的变化。@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
还提供了一个对应的监听器。@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
加载类的某个流程上打上断点,以便查看堆栈信息。@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 的?
@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
的流程上打断点。public String hello2() {
return str;
}
JRebel 小结
总结
spring-boot-devtools
会引入新的依赖,并且 class 文件变更会引起应用重启,而 JRebel 只会加载变动的 class 并利用 Spring 的 API 替换新的对象,因此 JRebel 比 spring-boot-devtools
会快上不少,相对来说比较个人比较支持使用 JRebel。END
往期精彩Spring Boot 三步完成日志脱敏
Spring Boot + Prometheus + Grafana 打造可视化监控一条龙
推荐几款 XShell 的替代品
Spring Boot 使用 Sa-Token-Quick-Login 插件实现快速登录认证
关注后端面试那些事,回复【2022面经】
获取最新大厂Java面经