Skip to content
rocketramanPublic

About

No description, website, or topics provided.

Resources

Stars

7 stars

Watchers

1 watching

Forks

Latest commit

 

History

138 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bootable

Maven Central GitHub license

A simple, unopinionated framework for booting applications. Intended to be much simpler than Spring Boot or even modern iterations like Quarkus and Micronaut, but still featureful enough to be useful. Handles stopping, starting, lifecycle management, logging, configuration, and other basic reusable components.

Bootable’s core is plain Kotlin and embeds no dependency-injection framework. Its runtime takes two sets — your application services and your lifecycle controllers — and runs them; how you assemble those sets is entirely your choice. Wire them by hand, use a DI solution provided by Bootable as optional add-on artifacts, or bring your own DI. Bootable ships optional bindings for Kodein-DI (clean DSL syntax but executes at runtime) and Metro (complex annotation-based syntax, but executes and checks for a valid dependency graph at compile-time). These are selectable per module, and you can just as easily integrate with any other DI, or none at all.

Why Not Spring Boot?

An important element of simplicity is to prefer the use of explicit references and configuration. Spring Boot’s runtime discovery and configuration of components and plugins via annotation and reflection is easy but not simple. To use Spring Boot, you have to understand two layers: the libraries underneath Spring, as well as all of Spring Boot’s own auto-magic annotations. Many users of Spring Boot fail to understand the hidden complexities, because they never really understand what Spring Boot’s annotations are doing (ever seen @Transactional on a private method — it silently does nothing at all, potentially making that code non-atomic).

Bootable aims to be both easy and simple. The goal of Bootable is to provide a minimal framework for running Kotlin (or Java?) applications, and then gets out of your way.

Useful reading:

Note
Importing the Metro DI artifact does bring annotations to your application, BUT the annotations are limited to the DI layer — the application’s own behavior is not controlled by annotations.

Features

  • Runs a set of application services and lifecycle controllers — assemble them however you like (plain Kotlin, Kodein, Metro, a service loader, …). Bootable embeds no DI framework; optional bindings for Kodein and Metro are available per module. See the examples/ directory for one project per wiring style.

  • Manages the lifecycle of application services.

  • Allows registration of lifecycle controllers, which can be used to control the lifecycle of application services in various ways. The basic built-in implementation of this is the AppStartStopLifecycleController which starts application services on app start, and shuts them down on app close, in priority order. Another basic implementation, intended to be used in conjunction with AppStartStopLifecycleController, is the StopSignalHandlerLifecycleController, which registers signal handlers, which, when received, cleanly shuts the system down. Other implementations are possible: for example starting/stopping services at runtime based on a command system, like start/stop messages from RPCs or web requests.

Modules

Bootable is modular, and provides a number of modules to help you build your application. The core module is simply com.github.rocketraman.bootable:boot, which provides the basic application lifecycle features of Bootable. Add additional modules as you wish.

Configuration

Hoplite

See hoplite.

Add this module to your project to use Hoplite for configuration.

Environment

Warning
Over-using this abstraction is a code smell.

This module simply exposes a strongly-typed global environment variable to the system, which is initialized from configuration by a lifecycle controller before services start. With vanilla wiring, add environmentInitializer(binder) to the controllers you pass to boot; with Kodein or Metro, this module’s optional bindings provide it for you.

This should be rarely used, but sometimes it is useful to have a way to identify the environment e.g. dev, test, prod, or other. Good use cases are tagging metrics with the environment, or extremely complex environment-specific configuration that can’t be easily abstracted using one of the Configuration modules.

Note that this is also useful in a JavaScript environment, where the environment may be bound to a global variable by something like dotenv-webpack. The environment abstraction may be used to abstract away the underlying details of this binding in a way that can be used in both frontend and backend in the common code source set.

Note
We currently do not publish multi-platform versions of these modules, but may do so in the future.

Logging (Log4j2)

This module provides all the necessary imports to configure Log4j2 as the logging system for the application. It provides good default logging layouts, and supports multiple output types e.g. plain (both color and not), JSON, and GCloud. See LoggingType.

Server HTTP (Ktor)

Provides a basic Ktor service abstraction with Bootable lifecycle integration i.e. the server starts and stops with the application. Does not attempt to configure Ktor in any significant way — this is left to the user. See KtorService.

How to Use

See Wiring Bootable for the full guide, and the projects in the examples directory — there is one per wiring style, so you can copy the one that matches your choice of DI (or no DI).

The essentials, regardless of wiring:

  • Application components that have a lifecycle implement AppService or AdvancedAppService.

  • You assemble a Set<AppService> and a Set<LifecycleController>, and hand them to the boot function, which runs them. With vanilla wiring that is just boot { Bootable(setOf(MyService(…​))) } — lifecycleControllers defaults to defaultLifecycleControllers().

  • defaultLifecycleControllers() provides the built-in start/stop and signal-handling controllers. To run a custom lifecycle — e.g. starting/stopping services on command from an RPC or web request rather than at boot — pass your own controllers instead.

To use a DI framework, depend on the -kodein or -metro artifact of each module you use (e.g. boot-kodein, boot-config-hoplite-kodein, boot-env-kodein, or the -metro equivalents) and let it assemble the sets:

  • Kodein (runtime): import the provided bootableModule / hopliteConfigModule / environmentModule, contribute your services with bindAppService { … }, and pass the resolved sets to boot.

  • Metro (compile-time): declare a @DependencyGraph that extends BootableGraph, contribute your services with @ContributesIntoSet, and boot { createGraph<AppGraph>().bootable }.

Design

Why no built-in DI?

The heavy-weight nature of DI frameworks like Spring has given DI a bit of a bad rap, and many people argue it is unnecessary in Kotlin — that simple constructor injection is enough. This is absolutely true for small projects, but as a project grows the need for modularity arises: consider a multi-module app, each module of which exposes one or more AppService implementations without any central code being aware of them all. Collecting those into a Set<AppService> is exactly what a DI container’s multibindings do well — but it is also easy to do by hand, and different teams reasonably prefer different tools (or none).

Bootable’s core is plain Kotlin: the AppService / LifecycleController interfaces, the built-in controllers, and the main framework class Bootable.kt carry no DI annotations, and Bootable takes its two sets through an ordinary constructor. The seam is those two sets; how you fill them is up to you.

For convenience, each module that has anything to wire ships optional Kodein and Metro bindings as separate -kodein / -metro artifacts (e.g. boot-metro), each depending on the plain module, so you can opt into a framework per module — one ordinary dependency, no Gradle plugin or capability selection — without the core ever depending on one. Kodein has, to my eye, a clean simple DSL syntax, BUT no compile-time graph checking, and is slightly slower to initialize on application startup since it wires the graph at runtime. Metro has a more verbose and complex annotation-based syntax, but the payoff is compile-time verification and construction of the graph.

See these three wiring options:

TODOs

  • ❏ Multiple logging implementations for different scenarios, loggingInit via ServiceLoader?

  • ❏ Create a boot-server-http-ktor-cohort module using Cohort

  • ❏ Update the ktor example to integrate Kompendium — this is outside the scope of Bootable, but it would be a good example of how to integrate Bootable with other libraries

  • ❏ Make multiplatform — some modules are backend only e.g. Hoplite, Log4j2, but others could easily be adapted for multiplatform usage

  • ❏ Integrate more useful modules e.g. JetBrains Expose, postgresql, and so on

Author

Raman Gupta <rocketraman@gmail.com>

About

No description, website, or topics provided.

Resources

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages