Spring Boot 提出了多项开箱即用的功能特性,但归根到底还是围绕简化应用的创建、开发、运行。开发环境下我们经常对项目代码进行变动,如果每次都重新启动应用会浪费我们大量时间,为此就产生了多种进行热部署的方案,可以在不重启的情况下使用新的代码。
然而,在 Java 中实现热部署并不是一件容易的事情。
Java 作为一种静态语言,类一经加载到 JVM 中,便无法修改,而且同一个类加载器对于同一个类只能加载一次,因此热部署常用的一种解决方案是创建新的 ClassLoader
加载新的 class 文件,然后替换之前创建的对象。
另一种解决方案是使用 Java Agent
,Java Agent
可以理解为 JVM 层面的 AOP,可以在类加载时将 class 文件的内容修改为自定义的内容,并且支持修改已加载到 JVM 的 class,不过对于已加载到 JVM 的 class 只能修改方法体,因此具有一定的局限性。
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
作为一个开发环境的插件,不仅支持热部署,具体来说有以下特性。
Environment
,以便开发环境禁用缓存。ApplicationContext
重启。spring-boot-devtools
插件使用同一套配置,如指定检查 class 文件变化的轮训时间。虽然 spring-boot-devtools
支持添加配置用来修改自身行为,通常情况下我们使用默认配置即可,不再赘述配置相关内容。下面我们把重点放到 spring-boot-devtools
热部署的具体实现上。
spring-boot-devtools
热部署使用了 ClassLoader
重新加载 的实现方式,具体来说使用两类 ClassLoader
,一类是加载第三方库的 CladdLoader
,另一类是加载应用类路径下 class 的自定义 RestartClassLoader
,应用类路径下 class 变化会触发应用重新启动,由于不需要重新加载第三方库的 class,因此相比重新启动整个应用速度上会快一些。
那具体到代码层面怎么实现的呢?spring-boot-devtools
利用 Spring Boot 应用自动装配的特性,在 spring.factories
文件中添加了很多配置。
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 {
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 收集文件系统中的 class 文件到 ClassLoaderFiles
,然后使用新的类加载器 RestartClassLoader
对应用重启,剩下的就很简单了,直接调用 main 方法即可。
除了首次应用启动时切换 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));
}
};
}
}
监听器监听到 class 文件变化后通过 Restarter
再次重启了应用,流程与首次重启时类似,不再赘述。
除了 spring-boot-devtools
,Spring 官方推荐的另一个热部署工具是 JRebel。JRebel 的核心是一个普通的 jar 包,内置了对多种框架的支持,通过 java -jar
启动时指定 -javaagent
即可使用 JRebel,而无需修改代码。同时 JRebel 也提供了多种的 IDE 插件,避免了手动启动指定 agent。
由于目前大家多使用 Idea 作为 IDE,因此这里介绍下 JRebel 在 Idea 中的使用。
首先在 Idea 的插件市场搜索 JRebel,并选择 JRebel and XRebel
,然后 install,之后重启 IDE 使插件生效。
然后点击 Help->JRebel->Activation
进入激活页面。
选择 Team URL,在 https://jrebel.qekang.com/
网站可以查找 可用的 Team URL,然后输入任意邮箱即可激活。扩展:[最全的java面试题库]
选择 View->Tool Windows->JRebel
对项目进行配置。
勾选项目名称右侧的第一个复选框即可快速开启 JRebel 对项目的支持。此时将在 resources 目录下生成一个 rebel.xml
文件,这个文件用于配置 JRebel 监听的类路径。
访问 Setting,在 Compiler 页面下勾选 Build project automatically
开启自动构建功能。
访问 Setting 页面,在 System Settings
页面下勾选 Save file if the IDE is idle for
。
然后使用 JRebel 进行 debug 就可以啦,当代码变更触发 IDE 构建后,JRebel 会自动使用新的 class 代码。
虽然 JRebel 在 Idea 中的使用方式比较简单,但当我试图探究其实现方式时却发现并没有那么容易。网上的文章前篇一律介绍的是其使用方式,即便其官网也只是简简单单概述为:JRebel 主要在 ClassLoader
级别与 JVM 及应用集成。它不会创建新的 ClassLoader
,当监测到 class 文件发生变化时通过扩展类加载器更新应用。
从官网的描述来看,也并没有深入到具体的实现方式上,真是 听君一席话,如听一席话,由于 JRebel 并未开源,并且其提供的 .jar 文件也进行了代码混淆,因此这里只能对其实现方式进行推测,并逐步验证。这里将推测及分析过程分享给大家。
JRebel 核心为一个普通的 jar 包,并通过 -javaagent
指定这个 jar 包,因此可以猜测它使用到了 Java Agent
的某些特性。
Java Agent
的主要作用为替换加载的 class,运行时修改方法体。由于 JRebel 支持在运行时添加、删除方法,因此 JRebel 必然不是通过运行时修改已加载到 JVM 的类路径下 class 方法体的方式来实现热部署的。那么大概率 JRebel 是修改了某些加载到 JVM 的 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,因此需要修改代码,改动后成功进入了我们的断点。扩展:[最全的java面试题库]
JRebel 在 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 会自动使用无参的构造方法实例化对象。
那么 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 对热部署的支持利用 Java Agent 修改了 Spring 的某些 class,当应用的 class 发生变化时,JRebel 自动加载新的 class 文件,并利用 Spring 的API 替换 Spring 中的旧对象,从而支持了 Spring 的热部署。
由于 spring-boot-devtools
会引入新的依赖,并且 class 文件变更会引起应用重启,而 JRebel 只会加载变动的 class 并利用 Spring 的 API 替换新的对象,因此 JRebel 比 spring-boot-devtools
会快上不少,相对来说比较个人比较支持使用 JRebel。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/xWe3IaZRht_9VLvWljA0Ow
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。