Bladerunner is an ultra-lightweight library which gives you an enhanced main() method for running your apps, including:
- built-in HOCON configuration file support
- built-in Dagger dependency injection
- built-in logging setup (defaults to log4j2)
- simple threading to run multiple components
In the spirit of getting a first, usable release out quickly I published 0.1.2 this weekend, along with several examples which I'll cover in the blog. But first some background.
HOCON & Typesafe Config
HOCON (Human-Optimized Config Object Notation) is an extended JSON notation developed by Typesafe for configuring the Play framework. There is a Java library, Typesafe Config, as well as implementations in many other languages. It supports variable substitution, includes, type-safe value extraction and much more. In Bladerunner it is used to define the main configuration file which you pass to cloudwall.appconfig.BladeRunner
, the main runner.
Here's a simple configuration file from the examples:
bladerunner {
blades=[
{
component="cloudwall.appconfig.example.hello.HelloWorld1$HelloWorldComponent"
config=hello-world
}
]
}
hello-world {
greeting="Hello, World!"
}
Note the use of = in place of : and the bare strings supported for simple values. The first config block, bladerunner.blades, demonstrates passing in an array of values.
Here's an example of variable substitution within a file:
numThreads = 2
kitchen-sink1 {
id = "Sink#1"
numThreads = ${numThreads}
}
kitchen-sink2 {
id = "Sink#2"
numThreads = ${numThreads}
}
Dagger
Dagger was originally developed by Square as an alternative to Guice & Spring dependency injection. Dagger 2, which is what Bladerunner uses, re-implemented Dagger as a compile-time dependency injector using Java's apt (annotation processor). If you install the apt-idea plugin, you can also get in-line code generation and compilation in IntelliJ:
buildscript {
repositories {
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath 'net.ltgt.gradle:gradle-apt-plugin:0.15'
}
}
apply plugin: 'net.ltgt.apt-idea'
You add both the runtime library and annotation processor as part of the Gradle dependencies block:
dependencies {
compile 'com.google.dagger:dagger:2.14.1',
annotationProcessor 'com.google.dagger:dagger-compiler:2.15'
}
and thereafter you can define modules and components for Dagger.
A @Module
is a collection of objects which you are providing to inject into your main application. Here's the HelloWorld2 module from the Bladerunner examples:
@Module
public class HelloWorldModule2 {
@Provides
GreetingRenderer greetingRenderer(Config config) {
if (config.getBoolean("ansi")) {
return new AnsiGreetingRenderer();
} else {
return new PlaintextGreetingRenderer();
}
}
}
The @Provides
annotation marks greetingRenderer
as providing a dependency which can be injected elsewhere in the program. But where does Config come from? This is some magic offered as part of Bladerunner: if you configure ConfigModule
you'll automatically get the referenced configuration block for your application injected into your module. It looks like this:
@Component(modules={HelloWorldModule2.class, ConfigModule.class})
public interface HelloWorldComponent {
HelloWorld2 helloWorld();
}
This feature is one of the reasons I wrote Bladerunner: I wanted to have the power and type-safety of Dagger's compile-time injection but also wanted flexible programmatic configuration of services. The combination of Typesafe Config & Dagger gives you exactly this.
Coming back to Dagger, the final piece is the injection site -- using constructor injection, we will @Inject
the GreetingRenderer
:
public class HelloWorld2 implements Blade {
private final GreetingRenderer renderer;
private String greeting;
@Inject
public HelloWorld2(GreetingRenderer renderer) {
this.renderer = renderer;
}
Piecing it together: Bladerunner
If you look at HelloWorld2.java one thing is notably absent: a main() method. This is where Bladerunner comes in: rather than implementing a main() method which uses your Dagger @Component
instead you run cloudwall.appconfig.BladeRunner
with a HOCON configuration file that references the @Component
-- the blade[] array in the config file we've seen already. This is the only place Reflection will get used: Bladerunner instantiates the component and calls the appropriate Builder methods to assemble your Component and ultimately create an instance of a Blade
. From here it then runs the Blade, including the callbacks defined in the Blade interface:
public interface Blade extends Runnable {
default void configure(Config config) { }
default void shutdown() { }
}
so HelloWorld2 above, for instance, implements configure to inject the greeting defined in the file.
Logging
The kitchen sink example demonstrates another feature of Bladerunner, automatic configuration of logging configuration. Here we reference a log4j2 config file, as well as instantiating two blades:
bladerunner {
logConfig=/cloudwall/appconfig/example/kitchensink/log4j2.xml
blades=[
{
component="cloudwall.appconfig.example.kitchensink.KitchenSink$KitchenSinkComponent"
config=kitchen-sink1
}
{
component="cloudwall.appconfig.example.kitchensink.KitchenSink$KitchenSinkComponent"
config=kitchen-sink2
}
]
}
Note this is optional: you will get a basic console logger for free if you don't define logConfig.
Feedback welcome
I hope you enjoy Bladerunner! If you find anything wrong with it or think it could be improved, please enter an issue on GitHub.