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.
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
ClassLoadergets 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
ClassLoaderand stopping/starting an underlying application.
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.
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
/healthendpoint. It must respond successfully when the application is ready to receive requests; usually, you can leave it without any logic. - The
mainmethod must handleInterruptedExceptionby gracefully shutting down the webserver and release all initialized resources. - The
mainmethod 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.
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.
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.
Add plugin dependency at the top of build.mill:
//| mvnDeps:
//| - me.seroperson::mill-live-reload::0.1.1And 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.
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"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)
)
}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"
)
}
}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
)
}
}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() }
}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.
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.
