掘金 后端 ( ) • 2024-05-02 09:53

技术背景

我们在一个大型项目里,往往启动一次需要加载大量的类和外部资源,如果开发期频繁修改一两个类,甚至是个别方法或语句,重启一次应用代价太大了。导致调试的时间成本极大的增加。

为了解决这个问题,spring boot通过了一个devtools工具库,可以实现项目里的classes文件夹下编译好的类发生修改变动时,自动热更新加载他们,从而实现应用不需要重启,大大的提升了开发效率。

使用方法

比如我们有个类:

@RestControllerpublic class HelloController {    @RequestMapping("/hello")    public String hello() {        return "Hello World, kimmking!";    }}

启动后,执行命令:

% curl http://localhost:8080/helloHello World, kimmking!%

我们想要在修改代码后,不用重启,就实现运行最新的代码。

@RestControllerpublic class HelloController {    @RequestMapping("/hello")    public String hello() {        return "Hello World, devtools!";    }}

在spring-boot项目里添加如下依赖,则可以实现开发环境的热部署。

即不需要重启应用,就能实现修改过的类起作用。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency>

此时重新import maven依赖,再启动应用。

先执行curl访问一次输出kimmking,修改代码,再执行一次还是输出kimmking,没有更新的原因是默认没有重新编译代码。

在IDEA里点"Build - recompile "HelloController.java"",或者 "Build - rebuild project",再访问一次即可发现输出了devtools:

% curl http://localhost:8080/helloHello World, devtools!%

实现效果

实现原理

spring-boot-devtools使用了两个类加载器ClassLoader,一个ClassLoader(base Classloader)加载不会发生更改的类依赖的各个jar),另一个ClassLoader(restart ClassLoader)加载会更改的类自定义的类,项目src下,编译后到classes文件夹)。

后台启动一个文件监听线程(File Watcher),监测的目录中的文件发生变动时,原来的restart ClassLoader被丢弃,将会重新加载新的restart ClassLoader。

因为文件变动后,第三方jar包不再重新加载,只加载自定义的类,加载的类比较少,所以重启比较快

可以`META-INF/spring-devtools.properties`在定义哪些jar使用restart Classloader:

restart:  exclude:    companycommonlibs: "/mycorp-common-[\\w\\d-\\.]+\\.jar"  include:    projectcommon: "/mycorp-myproj-[\\w\\d-\\.]+\\.jar"

使用限制

1、出于安全考虑,官方非常不建议在生产环境使用。默认只能用于开发模式,组件会判断如果用java -jar之类的方式启动应用,则不启用本功能。不过可以使用 `-Dspring.devtools.restart.enabled=true` 在生产环境来强制开启或者关闭此功能。

2、默认情况下,spring boot的maven打包工具在repackage阶段不会把devtools打进去,如果想要打进去,需要添加`excludeDevtools`参数为false。

3、根据加载原理,每次重新使用classloader加载,导致同一个类,比如cn.kimmking.DemoConfig,前后两个Class是不同的,如果缓存了前一个类或者其实例化的对象,使用后者来做转型或者比较,是不相等的。这一点需要特别注意,这些热加载的类,不能“有状态”。

4、对于页面模版、出错信息、静态资源之类的,spring或其框架本身默认会cache住以提升性能,这个时候,开发期热加载就不会有效果,为了解决这个问题,devtools提供了一系列的默认值来关掉这些cache,详见官方文档[1],如果想要这些默认值不生效,可以把spring.devtools.add-properties 设置为false。

5、如果使用mvn命令来编译和运行springboot,比如执行 mvn spring-boot:run 此时maven-compiler-plugin 插件添加fork为true的配置。否则因为类加载器没有隔离的问题,导致热更新不起作用。使用IDE启动不会有此问题(默认fork)了。

6、如果关闭了spring的shutdown hook,即`SpringApplication.setRegisterShutdownHook(false)`,热更新也不会生效。

7、对于使用`AspectJ`处理的类也不生效。

8、默认对于如下路径的资源不会重新加载(spring boot会保证在线生效),/META-INF/maven, /META-INF/resources, /resources, /static, /public, /templates, 可以使用如下方式重载此配置: `spring.devtools.restart.exclude=static/**,public/**`

9、注意 IDEA的settings-Build,Execution,Deployment-Compiler里有个 build project Automatic选项,备注了不能用于run或debug时,这个没有用。高级设置(advanced settings)里有个allow auto-make 。。。running,这个参数测试也无效。存疑。

高级配置

全局配置

可以配置全局的devtools配置,对于所有的应用都生效,默认在 $HOME/.config/spring-boot 文件夹下的如下文件:

  1. spring-boot-devtools.properties
  2. spring-boot-devtools.yaml
  3. spring-boot-devtools.yml

这个路径可以使用SPRING_DEVTOOLS_HOME 环境变量或者 spring.devtools.home 系统属性来配置。

文件变化轮询间隔

可以通过如下两个参数调整轮询间隔(IDE编译和复制文件需要一定时间)和等待其他文件变动时间。

spring.devtools.restart.poll-interval=2s
spring.devtools.restart.quiet-period=1s

主动通知变更

devtools提供了一种主动去通知有变化,去扫描文件变动的方式,就是所谓的触发文件模式。

通过配置:

spring.devtools.restart.trigger-file=.reloadtrigger

然后在src/main/resources下,我们创建一个文件`.reloadtrigger`, 我们可以主动控制这个文件的内容发生变化,此时即可触发devtools进行热更新处理(如果有文件变动)。

远程开发模式使用

devtools可以配合spring boot remote来远程生效。具体参考官方文档引用[1]。

官方文档

  1. 官方文档介绍
  2. 入门介绍

本文使用 文章同步助手 同步