Assuming we have a gradle project with subprojects.

.
β”œβ”€β”€ app
β”‚   ...
β”‚   └── build.gradle.kts
β”œβ”€β”€ lib
β”‚   ...
β”‚   └── build.gradle.kts
└── settings.gradle.kts
// settings.gradle.kts
rootProject.name = "myproject"
include("app")
include("lib")
// app/build.gradle.kts

dependencies {
  developmentOnly("org.springframework.boot:spring-boot-devtools")
  implementation(project(":lib"))
}

Now, we run :app:bootRun. Every time, we run :app:compileKotlin for kotlin project (or :app:compileJava for java project), the app is reloaded with the changes in app subproject => everything is fine, hot-reload works perfectly.

When we run :lib:compileKotlin, the app is reloaded. But, the changes of lib subproject are not up-to-date.

As far as I understand, Spring Boot split into 2 classpaths. One for the app subproject (classpath 1) and one for all other dependencies (classpath 2).

It would be nice, if there's a way to tell Spring Boot to move specific dependencies from "classpath 2" to "classpath 1". In this case, we want to tell Spring Boot that it should reload also compiled files for dependency :lib.

I am not expert here, but I imagine something similar to this in application.properties:

spring.devtools.restart.additional-hot-reload-packages=com.company.myproject.lib

If the machenism of spring hot reload requires this confiuration before loading application.properties, we could allow configuration via environment variables.

This way, the configuration will work regardless of maven, gradle, IDEs or whatever tools.

Comment From: wilkinsona

This should already be possible. Please see the relevant section of the reference documentation for details. Duplicates https://github.com/spring-projects/spring-boot/issues/3316.

Comment From: xuanswe

This should already be possible. Please see the relevant section of the reference documentation for details.

Oh, thanks for your promt reply!

I will check how it work.

Comment From: xuanswe

@wilkinsona Still not working.

I added restart.include.lib=/lib-[\\s\\S]+\.jar to app/resources/META-INF/spring-devtools.properties.

Then I create a breakpoint to org.springframework.boot.devtools.settings.DevToolsSettings#isRestartInclude() method. The breakpoint returns true for the file path of the :lib jar. So, I think everything is configured correctly.

I add spring.devtools.restart.additional-paths to the build dir of the :lib Now, If I compile the :lib, the :app is reloaded but still without the changes. If I run :lib:build, which creates a new jar file for :lib, I get error: Caused by: java.io.EOFException: Unexpected end of ZLIB input stream Note that, in :lib, I have classes with Spring annotation @Service.

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [com.....Application]
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:178) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:398) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:283) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:344) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:115) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:745) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:565) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.0.2.jar:3.0.2]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730) ~[spring-boot-3.0.2.jar:3.0.2]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:432) ~[spring-boot-3.0.2.jar:3.0.2]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-3.0.2.jar:3.0.2]
    at com.....DevApplication.main(DevApplication.kt:92) ~[development/:?]
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[?:?]
    at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
    at java.lang.reflect.Method.invoke(Method.java:568) ~[?:?]
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) ~[spring-boot-devtools-3.0.2.jar:3.0.2]
Caused by: java.io.EOFException: Unexpected end of ZLIB input stream
    at java.util.zip.ZipFile$ZipFileInflaterInputStream.fill(ZipFile.java:446) ~[?:?]
    at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:158) ~[?:?]
    at java.io.FilterInputStream.read(FilterInputStream.java:132) ~[?:?]
    at org.springframework.asm.ClassReader.readStream(ClassReader.java:322) ~[spring-core-6.0.4.jar:6.0.4]
    at org.springframework.asm.ClassReader.<init>(ClassReader.java:287) ~[spring-core-6.0.4.jar:6.0.4]
    at org.springframework.core.type.classreading.SimpleMetadataReader.getClassReader(SimpleMetadataReader.java:56) ~[spring-core-6.0.4.jar:6.0.4]
    at org.springframework.core.type.classreading.SimpleMetadataReader.<init>(SimpleMetadataReader.java:48) ~[spring-core-6.0.4.jar:6.0.4]
    at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:103) ~[spring-core-6.0.4.jar:6.0.4]
    at org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory.createMetadataReader(ConcurrentReferenceCachingMetadataReaderFactory.java:86) ~[spring-boot-3.0.2.jar:3.0.2]
    at org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory.getMetadataReader(ConcurrentReferenceCachingMetadataReaderFactory.java:73) ~[spring-boot-3.0.2.jar:3.0.2]
    at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:81) ~[spring-core-6.0.4.jar:6.0.4]
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:187) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:297) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:243) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:196) ~[spring-context-6.0.4.jar:6.0.4]
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:164) ~[spring-context-6.0.4.jar:6.0.4]
    ... 16 more

Caused by: java.io.EOFException: Unexpected end of ZLIB input stream

Comment From: wilkinsona

Unfortunately, it's hard to diagnose the problem with only a stack trace as there are several moving parts. There could be a problem with the restart configuration or with the classpath (using jars instead of a classes directory, for example). If you can provide a complete, yet minimal sample that reproduces the problem, we can re-open the issue and take a look.

Comment From: xuanswe

@wilkinsona Sure!

I have 2 examples. Both have 2 subprojects: :app and :lib

In the 1st example, :lib is just a normal kotlin project. In the 2nd example, :lib has a class with Spring Boot @Service annocation.

I will split these 2 examples in separate comments in a moment.

Comment From: wilkinsona

Thanks. Please share the example as a separate repository pushed to GitHub or by zipping it up and attaching it to this issue.

Comment From: xuanswe

1st example, :lib is just a normal kotlin project. Error message while reloading, but still reload successfully at the end. See README.md to reproduce bug.

https://github.com/xuan-nguyen-swe/spring-boot-hot-reload-bugs/tree/simple-lib

Comment From: xuanswe

2nd example, :lib has a class with Spring Boot @Service annocation. Spring Boot hot reload doesn't work at all. See README.md to reproduce bug.

https://github.com/xuan-nguyen-swe/spring-boot-hot-reload-bugs/tree/spring-boot-lib

Comment From: wilkinsona

Thank you. Is the problem Kotlin-specific or does it also occur when using Java? If it occurs when using Java, please use Java rather than Kotlin in the samples as it will make problem diagnosis easier.

Comment From: xuanswe

Thank you. Is the problem Kotlin-specific or does it also occur when using Java? If it occurs when using Java, please use Java rather than Kotlin in the samples as it will make problem diagnosis easier.

Let me create a branch with java in a moment.

Comment From: xuanswe

Migrated 2nd example to java https://github.com/xuan-nguyen-swe/spring-boot-hot-reload-bugs/tree/spring-boot-lib-java

Comment From: xuanswe

Migrated 1st example to java https://github.com/xuan-nguyen-swe/spring-boot-hot-reload-bugs/tree/simple-lib-java

Comment From: wilkinsona

Thanks. I can see what's happening now.

As I suspected, your lib project is on the classpath of the application as a jar file rather than as a directory. DevTools does not support watching jar files for changes so the changes will not be noticed. Generally speaking, the need for directories isn't a problem as DevTools is typically used in an IDE where classes directories are on the classpath by default. In your case it as a problem as you're using bootRun to run your application and the project dependency places the lib jar on the classpath.

I think the best way to avoid this problem is to open your project in your IDE and run its main method directly. If you prefer to continue using bootRun, you must configure Gradle to depend on classes where possible rather than jars. Something like this:

configurations {
    runtimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, LibraryElements.CLASSES))
        }
    }
}

Your sample works for me having deleted app/src/main/resources/META-INF/spring-devtools.properties and with app/build.gradle.kts changed to the following:

plugins {
  java
  id("org.springframework.boot") version "3.0.2"
  id("io.spring.dependency-management") version "1.1.0"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
  mavenCentral()
}

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web")
  developmentOnly("org.springframework.boot:spring-boot-devtools")
  implementation(project(":lib"))
}

configurations {
    runtimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, LibraryElements.CLASSES))
        }
    }
}

I haven't managed to reproduce the problem where you see a failure on restart but the changes are then available anyway. I suspect is due to the restart happening while the changes are in progress. You may need to configure the file system watcher to match the performance of your computer.

Comment From: xuanswe

Thanks for checking and propose solution, @wilkinsona!

I would not rely on IDE, so I would like to find a solution that always work regardless of working environment.

So, in short, we have 2 root issues here: 1. Only trigger reload when everything is ready

You suggest to fine tune spring.devtools.restart.poll-interval and spring.devtools.restart.quiet-period. I honestly don't like this, as it doesn't work stable. As I mentioned, I would like to have a solution that always work.

But, what do you think if I configure to use spring.devtools.restart.trigger-file instead? So, I will find a way to update the trigger file only when everything is ready. DevTools will only reload when this file is updated.

  1. DevTools does not support reload jar files

I will check your proposed solution and update the result here later.

Comment From: xuanswe

Anyway, you wrote "DevTools does not support watching jar files for changes so the changes will not be noticed". So, could you explain the purpose of defining jar files in META-INF/spring-devtools.properties? I think I don't understand the usage of this file.

Comment From: wilkinsona

But, what do you think if I configure to use spring.devtools.restart.trigger-file instead?

That sounds like a good option for you.

So, could you explain the purpose of defining jar files in META-INF/spring-devtools.properties?

Some libraries struggle with the two class loaders that DevTools uses. Defining a jar in spring-boot-devtools.properties allows you to control which class loader will load its classes which can fix class loading problems in third-party code. For example, moving a dependency into the restart class loader may prevent a class cast exception due to the same class being loaded by different class loaders.