Spring Boot 开发环境热部署(HotSwap)详解

前言

Spring Boot 提出了多项开箱即用的功能特性,但归根到底还是围绕简化应用的创建、开发、运行。开发环境下我们经常对项目代码进行变动,如果每次都重新启动应用会浪费我们大量时间,为此就产生了多种进行热部署的方案,可以在不重启的情况下使用新的代码。

热部署常用实现方案

然而,在 Java 中实现热部署并不是一件容易的事情。

1. ClassLoader 重新加载
Java 作为一种静态语言,类一经加载到 JVM 中,便无法修改,而且同一个类加载器对于同一个类只能加载一次,因此热部署常用的一种解决方案是创建新的 ClassLoader 加载新的 class 文件,然后替换之前创建的对象。

2. Java Agent
另一种解决方案是使用 Java Agent,Java Agent 可以理解为 JVM 层面的 AOP,可以在类加载时将 class 文件的内容修改为自定义的内容,并且支持修改已加载到 JVM 的 class,不过对于已加载到 JVM 的 class 只能修改方法体,因此具有一定的局限性。

spring-boot-devtools

spring-boot-devtools 快速上手

Spring Boot 通过 Maven 插件 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.RestartConfigurationspring-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));
				}
			};
		}
}

监听器监听到 class 文件变化后通过 Restarter 再次重启了应用,流程与首次重启时类似,不再赘述。

JRebel

除了 spring-boot-devtools,Spring 官方推荐的另一个热部署工具是 JRebel。JRebel 的核心是一个普通的 jar 包,内置了对多种框架的支持,通过 java -jar 启动时指定 -javaagent 即可使用 JRebel,而无需修改代码。同时 JRebel 也提供了多种的 IDE 插件,避免了手动启动指定 agent。

JRebel 在 Idea 中的使用

由于目前大家多使用 Idea 作为 IDE,因此这里介绍下 JRebel 在 Idea 中的使用。

1. 下载
首先在 Idea 的插件市场搜索 JRebel,并选择 JRebel and XRebel,然后 install,之后重启 IDE 使插件生效。
在这里插入图片描述2. 激活
然后点击 Help->JRebel->Activation 进入激活页面。
在这里插入图片描述
选择 Team URL,在 https://jrebel.qekang.com/ 网站可以查找 可用的 Team URL,然后输入任意邮箱即可激活。

3. 项目支持配置
选择 View->Tool Windows->JRebel 对项目进行配置。
在这里插入图片描述勾选项目名称右侧的第一个复选框即可快速开启 JRebel 对项目的支持。此时将在 resources 目录下生成一个 rebel.xml 文件,这个文件用于配置 JRebel 监听的类路径。

4. 自动编译配置

访问 Setting,在 Compiler 页面下勾选 Build project automatically 开启自动构建功能。
在这里插入图片描述访问 Setting 页面,在 System Settings 页面下勾选 Save file if the IDE is idle for
在这里插入图片描述5. 启动项目
然后使用 JRebel 进行 debug 就可以啦,当代码变更触发 IDE 构建后,JRebel 会自动使用新的 class 代码。
在这里插入图片描述在这里插入图片描述

JRebel 实现原理

虽然 JRebel 在 Idea 中的使用方式比较简单,但当我试图探究其实现方式时却发现并没有那么容易。网上的文章前篇一律介绍的是其使用方式,即便其官网也只是简简单单概述为:JRebel 主要在 ClassLoader 级别与 JVM 及应用集成。它不会创建新的 ClassLoader,当监测到 class 文件发生变化时通过扩展类加载器更新应用。

从官网的描述来看,也并没有深入到具体的实现方式上,真是 听君一席话,如听一席话,由于 JRebel 并未开源,并且其提供的 .jar 文件也进行了代码混淆,因此这里只能对其实现方式进行推测,并逐步验证。这里将推测及分析过程分享给大家。

推测:JRebel 通过 Java Agent 进行实现
JRebel 核心为一个普通的 jar 包,并通过 -javaagent 指定这个 jar 包,因此可以猜测它使用到了 Java Agent 的某些特性。

Java Agent 的主要作用为替换加载的 class,运行时修改方法体。由于 JRebel 支持在运行时添加、删除方法,因此 JRebel 必然不是通过运行时修改已加载到 JVM 的类路径下 class 方法体的方式来实现热部署的。那么大概率 JRebel 是修改了某些加载到 JVM 的 class。

推测:JRebel 会在 class 文件发生变化后重新加载 class 文件
推测 JRebel 使用了 Java Agent 之后,我们还是不能了解其主要实现方式,不过当我们的 class 文件发生变动后,JRebel 必然会重新加载变动后的 class 文件,以便执行新的代码,因此我们可以在 ClassLoader 加载类的某个流程上打上断点,以便查看堆栈信息。

Spring Boot 项目的示例代码如下。

@SpringBootApplication
@RestController
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello() {
        return "hallo";
    }

}

不过呢,由于我们误将 hello 拼成了 hallo,因此需要修改代码,改动后成功进入了我们的断点。
在这里插入图片描述JRebel 在 rebel-change-detector-thread 线程监测 class 文件的变动,文件变动后使用 AppClassLoader 加载了 com.zzuhkp.DemoApplication 开头的类,并且类名后还带了 $$M$_jr_ 开头的后缀。可以想到的是同一个 ClassLoader 只能加载一个类,因此 JRebel 对类名进行了修改。这也是官网所描述的,不创建新的 ClassLoader,当 class 发生变化时更新应用。

问题:JRebel 如何替换新的 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 会自动使用无参的构造方法实例化对象。

那么 JRebel 又是怎么替换旧对象的呢?我们知道,对于注解驱动的 Spring MVC,Controller 方法会被当做一个 handler 处理请求,如果新添加一个 Controller 方法,那么它必然被注册为 handler 才能处理请求。我们添加一个 hello2 的方法,并在注册 handler 的流程上打断点。

    @GetMapping("/hello2")
    public String hello2() {
        return str;
    }

当访问 /hello2 果然进入了断点。
在这里插入图片描述从堆栈信息来看,多出了 JRebel 的相关类,可以断定,JRebel 对 Spring 的某些 class 做出了修改,当 class 发生变动后,JRebel 自动使用新的 class 实例化的对象注册到 Spring 内部。

JRebel 小结
从上述推测和验证的过程来看,JRebel 对热部署的支持利用 Java Agent 修改了 Spring 的某些 class,当应用的 class 发生变化时,JRebel 自动加载新的 class 文件,并利用 Spring 的API 替换 Spring 中的旧对象,从而支持了 Spring 的热部署。

总结

由于 spring-boot-devtools 会引入新的依赖,并且 class 文件变更会引起应用重启,而 JRebel 只会加载变动的 class 并利用 Spring 的 API 替换新的对象,因此 JRebel 比 spring-boot-devtools 会快上不少,相对来说比较个人比较支持使用 JRebel。

那么你对热部署还有哪些想法呢?请留言讨论。如果文章对你有些许帮助,欢迎点赞支持。

  • 14
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
### 回答1: 1. 快速开发:Spring Boot 提供了一些开箱即用的功能,比如自动配置、内嵌服务器等,使得开发者可以更快速地开发应用程序。 2. 简化配置:Spring Boot 自动配置了许多常用的配置,减少了开发者的配置工作量。 3. 更好的可维护性:Spring Boot 的自动配置和约定大于配置等特性,使得应用程序更加易于维护。 4. 更好的性能:Spring Boot 的内嵌服务器和自动配置等特性,使得应用程序运行更加高效。 5. 更好的部署:Spring Boot 可以将应用程序打包成一个可执行的 jar 包,方便部署和运行。 6. 更好的生态系统:Spring Boot 集成了许多常用的框架和库,使得开发者可以更加方便地使用这些工具。 ### 回答2: Spring Boot相对于Spring框架的优势主要体现在以下几个方面: 1.简化配置:Spring Boot提供了自动配置的特性,通过分析项目的依赖和配置,可以自动配置Spring应用程序所需的各种组件,大幅减少了开发人员的配置工作。 2.内嵌服务器:Spring Boot内置了多种常用的服务器容器(如Tomcat、Jetty等),能够方便地创建独立运行的应用程序,无需外部服务器的支持。 3.快速开发:Spring Boot提供了快速开发的工具和命令行界面,可以极大地提高开发效率。例如,可以使用Spring Initializr快速创建一个基于Spring Boot的项目,快速搭建起整个项目的框架。 4.自动化依赖管理:Spring Boot通过约定大于配置的原则,简化了对依赖管理的处理。它可以根据项目所需的功能和特性,自动导入相应的库和依赖,简化了依赖管理的工作。 5.微服务支持:Spring Boot天然支持构建微服务架构。通过Spring Boot可以方便地创建和管理多个微服务,同时也提供了与其他微服务框架(如Spring Cloud)的集成支持。 6.监控和管理:Spring Boot提供了一些额外的功能,用于监控和管理应用程序。例如,可以用Actuator监控应用程序运行状态,获取关键指标和元数据,并暴露一个RESTful API供外部访问。 总而言之,相对于传统的Spring框架,Spring Boot具有更加简化、快速、便捷的特点,提供了一套更加优雅和现代的开发方式,可以大大提高开发效率和便利性。 ### 回答3: Spring Boot相对于传统的Spring框架具有几个明显的优势。 首先,Spring Boot大大简化了Spring应用程序的开发和部署。传统的Spring项目需要手动配置许多组件和依赖关系,而Spring Boot采用了自动配置的机制,通过一些默认配置和约定,减少了开发者的配置工作。同时,Spring Boot内置了一个嵌入式的应用服务器,可以方便地将应用程序打包成一个可执行的JAR文件,简化了部署过程。 其次,Spring Boot提供了强大的开发工具和快速开发的能力。Spring Boot集成了一系列常用的开发工具,例如自动重载(LiveReload)和热部署HotSwap),可以大大提高开发效率。此外,Spring Boot还支持面向生产环境的特性,如性能监控、健康检查和安全管理等,可以帮助开发者更好地构建可靠的应用程序。 另外,Spring Boot提供了一种更加现代化和灵活的方式来开发和扩展Spring应用程序。Spring Boot采用了基于约定的配置方式,开发者只需遵循规范和约定,即可获得一些默认的配置和行为。此外,Spring Boot还允许开发者通过配置文件、注解和外部属性等方式来进行自定义配置,满足个性化需求。借助Spring Boot的自动装配特性,开发者可以更加方便地集成第三方库和组件,快速构建复杂的应用程序。 综上所述,Spring Boot相对于传统的Spring框架具有简化开发和部署、提供强大的开发工具和快速开发能力,以及更加现代化和灵活的开发方式等优势。这使得Spring Boot成为了许多开发者首选的框架,特别适用于构建微服务和快速Web应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏cool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值