Search code examples
spring-bootgradleintellij-ideaaspectjspring-boot-devtools

SpringBoot restart Application with devtools, aspectj, gradle and IntelliJ


I just disvovered spring-devtools to restart the application when a class file changes.

To trigger the restart i setup IntelliJ like this:

  • Activated "Build project automatically" in "Compiler"
  • Activated "Build project" in "Actions on Save"
  • Activated "Allow auto make even if application is currently running" in "Advanced Settings"
  • Activated "Build and run using Gradle" in "Build Tools/Gradle"

Whenever I change a .java file and hit Strg-S for save, the file gets compiled by IntelliJ and Spring Boot restarts within a few seconds. The .class file is compiled to the build/ directory and not the default out/ directory of IntelliJ. This is correct because of gradle. That's fine and it works as expected.

Properties files

But it does not work with properties files. I have a message.properties file and a ReloadableResourceBundleMessageSource in my Spring Boot Application. I want this file to be reloaded, too.

Whenever I change the messages.properties file and save it, the file gets copied to out/ directory instead of the build/ directory. In the build/ directory it can't get picked up.

It is not picked up by devtools restart mechanism (which could/might be disabled for these files with spring.devtools.restart.exclude)

But it is not picked up by ReloadableResourceBundleMessageSource either as the classpath is -classpath [...]/build/classes/java/main:[...]/build/resources/main

How do I tell IntelliJ to respect the gradle build Directory for resource files?

AspectJ

We are using AspectJ compile-time-weaving (ctw). But the IntelliJ auto-compile mechanism does not take into account our gradle aspectj configuration. So sometimes the compilation is just wrong. This happens if you have a @Transactional or @Async annotation. This is quite annoying.

Reloads twice

If the compilation takes some more time, the application restarts already and restarts again as it detects new changes afterwards. this could be solved with a trigger-file with devtools. But you can't configure IntelliJ to touch a trigger file after compiling.

How to solve these problems?


Solution

  • I found a good solution for this problem I would like to share:

    We want to achieve proper compilation and fast reload times with devtools (without jrebel). Proper compilation includes aspectj compile time weaving (ctw) which is something that the internal IntelliJ compile does not handle.

    So automatic compilation was not an option for us. What we are using instead is gradle continuous build

    Adding a continuous task to build.gradle

    import java.time.Instant
    
    tasks.register('compileTrigger') {
        dependsOn tasks.compileJava
        onlyIf {
            !compileJava.state.upToDate
        }
        doLast {
            def file = new File(projectDir, "build/classes/java/main/.reloadTrigger");
            file.createNewFile();
            file.setLastModified(Instant.now().toEpochMilli())
        }
    }
    tasks.register('continuous-build') {
        getProject()
                .getRootProject()
                .getSubprojects()
                .findAll { (it.name != project.name) }
                .forEach {
                    it.tasks.configureEach {
                        enabled = false
                    }
                }
        dependsOn tasks.processResources, tasks.compileTrigger, tasks.processTestResources, tasks.compileTestJava
    }
    

    We decided to use a trigger file because otherwise the application restarts to early if more work is done.

    We decided to exclude all other modules as these have their own reload mechanisms (npm, vue, vite etc). your mileage may vary.

    We only add a trigger file if compileJava was running. All files from process-resources gets picked up by Spring boot automatically if configured properly.

    Adding and configuring devtools

    Add to build.gradle

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

    Adding to application-development.properties:

    spring.devtools.restart.trigger-file=.reloadTrigger
    spring.devtools.restart.poll-interval=400ms
    spring.devtools.restart.quiet-period=1ms
    

    poll-interval and quiet-period are not needed but they save some time (1sec). It would be nice if devtools uses a file watcher and not a polling algorithm.

    Settings in IntelliJ

    • Settings / Compiler / Disable "Build project automatically"
    • Settings / Advanced Settings / Disable "Allow auto-make"
    • Settings / Gradle / Run Test using "IntelliJ"
    • Run/Debug Configurations / Edit Configuration Templates / Junit / Do not build before run

    So we disable all automatic build (which is the default in IntelliJ anyway). But check this options as otherwise gradle and IntelliJ are building at the same time.

    Spring Boot Run Configuration

    Edit your Spring Boot Run Configuration and go to "Modify options" and activate "Do not build before run". As building is done by continuous gradle, it is important to disable it here.

    Gradle "Run Configuration"

    We are adding a Gradle "Run Configuration" in IntelliJ to easily start our continuous build.

    <component name="ProjectRunConfigurationManager">
      <configuration default="false" name="continous-build" type="GradleRunConfiguration" factoryName="Gradle">
        <ExternalSystemSettings>
          <option name="executionName" />
          <option name="externalProjectPath" value="$PROJECT_DIR$" />
          <option name="externalSystemIdString" value="GRADLE" />
          <option name="scriptParameters" value="--continuous" />
          <option name="taskDescriptions">
            <list />
          </option>
          <option name="taskNames">
            <list>
              <option value="continuous-build" />
            </list>
          </option>
          <option name="vmOptions" />
        </ExternalSystemSettings>
        <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
        <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
        <DebugAllEnabled>false</DebugAllEnabled>
        <RunAsTest>false</RunAsTest>
        <method v="2" />
      </configuration>
    </component>
    

    Now we are able to start a continuous build in IntelliJ.

    Compound Run Configuration

    Add a Compound Run Configuration adding gradle and spring-boot run configuration, name it "build-and-run"

    Open IntelliJ, start "build-and-run" and everything gets compiled and reloaded automatically.

    We saved a lot of time with this setup.