Skip to content

seroperson/jvm-live-reload

Repository files navigation

♾️ jvm-live-reload

Build Status Sonatype Central Version License

Warning

This project is in an alpha-quality stage. Everything tends to change. If you encounter any issues or it doesn't play well with your setup, please file an issue.

This project aims to provide a consistent live reload experience for any web application (currently you can't yet use it with daemons) on the JVM. It allows you to speed up your development cycle regardless of what framework or library you're using. Read an article ♾️ Live Reloading on JVM for more information on the reloading topic and prerequisites for the creation of this project.

Preview

How it works

The core principle is widely known and already adopted by such giants as Spring, Quarkus, Play, Apache Tapestry (and probably more). Basically, all of them work like this:

  • Starting an application.
  • Watching for project changes.
  • When a change occurs, the application (but not the JVM itself) stops, and the underlying ClassLoader gets dropped.
  • The application starts again with a new modified ClassLoader.

Such approach allows you to boost your development cycle by saving time on JVM startup and system classes initialization. Concrete frameworks can also use some additional boost depending on their own structure and lifecycle.

This project utilizes the general approach, but with minor tweaks to make it framework-agnostic:

  • When run task is called, it starts the reverse-proxy webserver.
  • This proxy starts your underlying application and routes everything into it.
  • When a change occurs, the next request to the proxy will reload the underlying code by re-creating a ClassLoader and stopping/starting an underlying application.

Installation

To get started, first, you'll probably need to do some changes to the application's code and also setup a plugin for your build system. Currently supported build systems are sbt, gradle and mill. We want to cover as much as we can, so more build systems will likely be added later.

Minimum required JDK is 17.

Important

After making all the necessary changes, be sure to read the Configuration section to tweak default settings according to your setup. By default live-reloading proxy will start at :9000 port and your application is expected to listen at :8080. You must now send requests to the proxy, not an application itself.

Changes to the application code

Besides the basic plugin installation flow, there are things you'll probably (check the list of supported frameworks to find exact changes which you must implement according to your framework) need to change in your application to make it live-reloading-ready:

  • Implement a /health endpoint. It must respond successfully when the application is ready to receive requests; usually, you can leave it without any logic.
  • The main method must handle InterruptedException by gracefully shutting down the webserver and release all initialized resources.
  • The main method must only finish when your application is completely stopped.

Note

This plugin makes your application run in a non-forked JVM. If your application somehow relies on running inside an isolated JVM process, you should ensure that it won’t cause any issues. In most cases, however, you probably won’t even notice a difference.

Implementing this logic will also make your application lifecycle more predictable in general, so they are just nice to have besides making an application live-reloading-ready. Read an article ⏹️ Making your JVM application interruptible to know more about interrupting.

Worth to say, that if your framework doesn't support interrupting and/or doesn't allow you to make these changes by yourself right in your codebase, probably it should be supported by the plugin, like frameworks from Scala ecosystem, such as zio or cats-effect (or a framework itself must be fixed, which is preferable). Once again, you can take a look at the list of tested frameworks, although, of course, even if your framework isn't in the list, live-reloading may still work if it implements interrupting and graceful shutdown correctly.

sbt

Add a plugin to project/plugins.sbt using:

addSbtPlugin("me.seroperson" % "sbt-live-reload" % "0.1.1")

And enable the plugin on your web application:

enablePlugins(LiveReloadPlugin)

The command to run your application in live-reloading mode is sbt run.

Gradle

Add a plugin to your build.gradle.kts using:

id("me.seroperson.reload.live.gradle") version "0.1.1"

The command to run your application in live-reloading mode is ./gradlew liveReloadRun.

mill

Add plugin dependency at the top of build.mill:

//| mvnDeps:
//| - me.seroperson::mill-live-reload::0.1.1

And make your application module extend LiveReloadModule:

// ...
import me.seroperson.reload.live.mill.*

object app extends LiveReloadModule, ScalaModule {
  // ...
}

The command to run your application in live-reloading mode is mill app.liveReloadRun.

Fixing the InaccessibleObjectException error

As this plugin uses some internal classes that aren't available without extra configuration, you may encounter errors like this during reloading (you can enable stacktrace displaying using live.reload.debug property):

java.lang.reflect.InaccessibleObjectException: Unable to make static void java.lang.ApplicationShutdownHooks.runHooks() accessible: module java.base does not "opens java.lang" to unnamed module @77e282b6

Then you need either tweak environment variable or add this option to your IDE's Java runtime:

export JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED"

Tuning your webserver

Some webservers may have settings which can slow down your reloading speed. For example, Netty has a quietPeriodSeconds parameter, which sets the period of time before shutdown when the webserver would be idle. zio-http has this parameter set by default to 2 seconds; other frameworks with Netty under the hood may have other slow defaults. So be sure to tweak it if you notice some lags during reloading.

For example, in case of zio-http you can use the predefined "fast shutdown" Netty config:

import zio._
import zio.http._
import zio.http.netty.NettyConfig

object App extends ZIOAppDefault {
  val routes = Routes(/* ... */)

  def run = Server.serve(routes)
    .provide(
      Server.customized,
      ZLayer.succeed(Server.Config.default),
      ZLayer.succeed(NettyConfig.defaultWithFastShutdown)
    )
}

Configuration

This plugin has defaults that should be suitable for most people, but you can change them using environment variables or build configuration.

First, let's check the list of available options:

Key Environment Default Description
live.reload.proxy.http.host LIVE_RELOAD_PROXY_HTTP_HOST 0.0.0.0 The host for the proxy to start on
live.reload.proxy.http.port LIVE_RELOAD_PROXY_HTTP_PORT 9000 The port for the proxy to listen on
live.reload.http.host LIVE_RELOAD_HTTP_HOST localhost The host on which your web application starts
live.reload.http.port LIVE_RELOAD_HTTP_PORT 8080 The port your web application listens on
live.reload.http.health LIVE_RELOAD_HTTP_HEALTH /health Path to your health-check endpoint
live.reload.debug LIVE_RELOAD_DEBUG false Whether to enable/disable debug output

To change variables using build configuration, use the following key for sbt:

liveDevSettings := Seq[(String, String)](
  // Can be plain string or value from auto-imported `DevSettingsKeys` object
  DevSettingsKeys.LiveReloadProxyHttpPort -> "9001",
  DevSettingsKeys.LiveReloadHttpPort -> "8081"
)

And for gradle:

liveReload { settings = mapOf("live.reload.http.port" to "8081") }

And for mill:

import me.seroperson.reload.live.mill.*

object app extends LiveReloadModule, ScalaModule {
 def liveDevSettings: Task[Seq[(String, String)]] = Task.Anon {
    Seq(
      DevSettingsKeys.LiveReloadHttpPort -> "8081"
    )
  }
}

Hooks

So far not every framework implements interrupting and graceful shutdown correctly, which is necessary to be live-reloading-ready. That's why this plugin introduces so-called "hooks". Hooks define how to start and shutdown your application. When reloading occurs, the proxy will call all defined shutdown hooks to stop it, and then it will call all startup hooks to start it again. Both types of hooks are blocking. When shutdown hooks are finished, the application is considered stopped and all its resources are cleaned. Similarly, when startup hooks are finished, the application is considered ready to receive requests.

For example, there is the built-in RestApiHealthCheckStartupHook, which polls the /health endpoint until a successful response. This means that your application will be considered started when its /health endpoint returns 200. Similarly, there is a RestApiHealthCheckShutdownHook, which polls the endpoint until a failure.

The complete list of built-in hooks:

Class Description
RestApiHealthCheckStartupHook Blocks until success on /health endpoint.
RestApiHealthCheckShutdownHook Blocks until failure on /health endpoint.
RuntimeShutdownHook Uses reflection to call all JVM shutdown hooks added by Runtime.addShutdownHook.
ThreadInterruptShutdownHook Calls Thread.interrupt() on the main thread.
IoAppStartupHook (Scala-only) Starts a cats.effect.IOApp. Basically, it just sets an internal property to strip unnecessary logging.
IoAppShutdownHook (Scala-only) Stops a cats.effect.IOApp. Shuts down the underlying cats.effect.unsafe.IORuntime instances.
ZioAppStartupHook (Scala-only) Starts a zio.ZIOApp. Updates context class loader for ZScheduler threads which survive shutdown.
ZioAppShutdownHook (Scala-only) Stops a zio.ZIOApp. Stops all internal executors if possible.

The sbt and mill plugins also provide a set of predefined hooks, so-called hook bundles, which will be automatically used when a plugin finds the corresponding library in a classpath. Currently, supported sets are: ZioAppHookBundle, IoAppHookBundle and CaskAppHookBundle. All available options are defined in HookBundle.scala. You can also override a set of startup/shutdown hooks using the liveStartupHooks and liveShutdownHooks keys. For example:

// The order matters (!)
liveShutdownHooks := Seq[String](
  // Can be plain string or value from auto-imported `HookClassnames` object
  HookClassnames.RestApiHealthCheckShutdown,
  HookClassnames.ThreadInterruptShutdown
)

This way, you can also implement your own hooks. All you need to do is implement the me.seroperson.reload.live.hook.Hook interface and specify it in the build configuration. They will be instantiated automatically using reflection during proxy webserver startup.

To change hooks for the gradle plugin, use the following settings:

liveReload {
  // these are default values
  startupHooks = listOf("me.seroperson.reload.live.hook.RestApiHealthCheckStartupHook")
  shutdownHooks = listOf(
    "me.seroperson.reload.live.hook.ThreadInterruptShutdownHook",
    "me.seroperson.reload.live.hook.RuntimeShutdownHook",
    "me.seroperson.reload.live.hook.RestApiHealthCheckShutdownHook",
  )
}

For mill:

import me.seroperson.reload.live.mill.*

object app extends LiveReloadModule, ScalaModule {
  def liveStartupHooks: Task[Seq[String]] = Task.Anon {
    Seq(
      HookClassnames.RestApiHealthCheckStartup
    )
  }
}

Propagate environment

This plugin provides a feature to propagate custom environment variables to a reloadable application. While with sbt you can make use of sbt-dotenv, which is compatible with the plugin, propagating the environment with mill and gradle can be tricky, as an application starts within the same existing JVM process while reloading. So for such purposes there is the livePropagateEnv setting (propagateEnv for gradle), which accepts Map<String, String> (your custom environment) to pass to an application.

For example, for mill it would look like:

import me.seroperson.reload.live.mill.*

object app extends LiveReloadModule, ScalaModule {
  def forkEnv = Map("BASE_URL" -> "...")
  // Can reuse `forkEnv` or hardcode environment in place
  def livePropagateEnv = Task.Anon { forkEnv() }
}

List of tested frameworks

To minimize any unsuccessful experience, we'll maintain the list of officially tested frameworks and libraries right here.

However, even if a framework isn't listed here, it still may play well. If you have successfully used this plugin, I would appreciate if you could share your project setup in the relevant discussion, even if your setup fully consists of libraries listed below. This would help other users to determine whether their own setup will work.

Framework Version Confirmation Necessary changes to the application code
zio + zio-http + zio-config-typesafe zio 2.1.21, zio-http 3.5.1, zio-config 4.0.5 See zio-* in sbt-test folder. Only /health endpoint.
http4s-ember-server + cats-effect + pureconfig http4s-ember-server 0.23.30, cats-effect 3.6.1, pureconfig 0.17.9 See http4s-* in sbt-test folder. Only /health endpoint.
cask cask 0.9.7 See cask in sbt-test folder. Only /health endpoint.
ktor ktor 3.3.0 LiveReloadKtorTest.kt Everything from this section.
http4k http4k 5.47.0.0, but 6.x should work too LiveReloadHttp4kTest.kt Everything from this section.
javalin, jte javalin 6.7.0, jte 3.2.2 LiveReloadJavalinTest.kt, LiveReloadJavalinJteTest.kt Everything from this section.

Everything was tested under JDK 17, but later versions should work too.

License

A lot of code was initially copied from the playframework project. Many thanks to all the contributors, as without them it would take much more time to implement everything correctly.

MIT License

Copyright (c) 2025 Daniil Sivak

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

About

Providing a consistent live reload experience for any web application on the JVM.

Topics

Resources

License

Stars

Watchers

Forks

Contributors