Bootique Core - v3.0


1. Overview

1.1. What is Bootique

Bootique is a platform for building container-less runnable Java applications. It is an ideal technology for microservices, as it allows you to create a fully-functional app with minimal-to-no setup. Though it is not limited to a specific kind of application (or to the "micro" size) and can be used for REST services, webapps, runnable jobs, DB migrations, JavaFX GUI apps or really anything else.

Unlike traditional container-based apps, Bootique allows you to control your main() method and create Java apps that behave like simple executable commands:

java -jar my.jar [arguments]

Each Bootique app can be started with a YAML configuration, or configured with shell variables and is ideally suited for Docker / cloud deployments.

Compared to other products in this space Bootique has a focus on modularity and clean pluggable architecture. It is built on top of a dependency injection container, and pretty much anything in Bootique can be customized/overridden to your liking.

1.2. Java Version

Java 11 or newer is required.

1.3. Build System

Bootique apps can be built using any Java build system (Ant, Maven, Gradle, etc). Examples in the documentation are based on Maven.

2. Programming

A typical "main" class in a Bootique application looks like this:

public class Application extends BaseModule {

    public static void main(String[] args) {
        Bootique.app(args) (1)
                .autoLoadModules() (2)
                .exec() (3)
                .exit(); (4)
    }
}
1 Bootique class is the "builder" of our app that allows to configure it before startup
2 Enables module auto-loading
3 Creates a BQRuntime object and calls its "run()" method. The outcome depends on what modules the application is made of and what CLI options were passed to it on startup.
4 Exits the app passing the exit code that is the result of the execution to the OS shell that started the app.

This is it in a nutshell. Now let’s discuss the details…​

2.1. BQRuntime and DI

BQRuntime is the object representing a Bootique app. It is the entry point to everything in the app. It holds all other objects ("services"), configuration, commands, etc. Despite its importance, you’d rarely interact with BQRuntime directly (except when writing unit tests), instead it acts as a "container" for your code.

Foundation of BQRuntime functionality is dependency injection (DI) that helps to glue things together even in most complex applications.

If you are familiar with other DI implementations in Java, you will find a lot of similarities. Namely standard injection annotations from javax.inject package (JSR-330). But also quite a few differences. The closest DI implementation to Bootique is good old Guice.

BQRuntime loads its services from "modules", so next let’s discuss modules.

2.2. Modules

Each module is a logical collection of (hopefully related) services. E.g. a standard "bootique-jetty" module integrates Jetty server into your application, "bootique-jdbc" provides a way to work with relational databases, and so on. Bootique team maintains a constantly growing list of such modules. Your own code will also be organized as modules.

At its core a module is simply a bunch of Java classes and resources. What makes it a module is a precense of one special class implementing BQModule. This class is an "assembly descriptor" for the objects in the library. It contains information used by the DI environment to create and configure them. Here is a simple module class (we’ll explain configure(Binder) method a bit later):

public class MyModule implements BQModule {

    @Override
    public void configure(Binder binder) {
        binder.bind(MyService.class).to(MyServiceImpl.class);
    }
}

By convention all classes beloning to a given module are packaged in a dedicated ".jar" file, though nothing would prevent you to keep more than one module in a single .jar.

Java Platform Module System (JPMS) is logically close to Bootique modules, and we would love to fully align the two. Unfortunately it is our experience that forcing the users to adhere to JPMS everywhere would result in lots of limitations and invariable productivity loss, so we are intentionally ignoring possible JPMS synergies.

2.3. Services

As mentioned above, DI manages a subset of application objects that we informally call "services". Here is a rough list of DI functionality around services:

  • It provides a powerful mechanism to assemble service instances, with injection of dependencies, applying external configuration, using custom factories

  • It makes each constructed service available to be injected in other objects that depend on it

  • It enables "programming to interfaces" paradigm, decoupling dependencies from specific implementations

  • It manages each service scope (most often - singleton) and lifecycle (a shutdown callback can be executed if needed)

  • It infers the startup order to satisfy service interdependencies

It is up to you as a programmer to decide which objects are services and which are just regular objects. Common traits of services: (1) are singletons; (2) known to the rest of the application via their "contract" interfaces; (3) require complex configuration and assembly; (4) expensive to create.

2.4. Injection and Bindings

Services can annotate their fields and/or constructors with @Inject to indicate which internal variables they want to be injected by the DI container. Do demonstrate this we will show a couple of implementations of a simple "hello" service that returns a greeting:

public interface Hello {

    String sayHello();
}

An implementation of this service would need another service called UserNameService that returns a user name and needs to be injected. Here is a field injection flavor:

public class HelloService1 implements Hello {

    @Inject
    private UserNameService nameService;

    @Override
    public String sayHello() {
        return "Hello, " + nameService.getUserName() + "!";
    }
}

And here is a constructor flavor:

public class HelloService2 implements Hello {

    private UserNameService nameService;

    @Inject
    public HelloService2(UserNameService nameService) {
        this.nameService = nameService;
    }

    @Override
    public String sayHello() {
        return "Hello, " + nameService.getUserName() + "!";
    }
}

Both flavors are identical in terms of functionality. Constructor variety is more verbose, but arguably results in a cleaner API, as the object can be created if needed without a DI environment. Regardless of which flavor was selected, the implementation needs to be bound in the module class:

@Override
public void configure(Binder binder) {
    binder.bind(Hello.class)
            .to(HelloService2.class)
            .inSingletonScope();
}

This creates a permanent association between Hello interface and a specific implementation. Hello can now be injected into other services. A single instance of HelloService2 is created automatically by the DI container, with a real object transparently provided for the "nameService".

If the service construction requires additional logic, instead of binding service implementation, you can create and bind a Java class that implements Provider<Service>. E.g.:

public class HelloService3Provider implements Provider<Hello> {

    @Inject
    private UserNameService nameService;

    @Override
    public Hello get() {
        return new HelloService3(nameService);
    }
}
@Override
public void configure(Binder binder) {
    binder.bind(Hello.class)
            .toProvider(HelloService3Provider.class)
            .inSingletonScope();
}

Provider class has the same rules for injection of its own dependencies as services (either annotated fields or annotated constructor).

The benefit of the provider is that it can implement custom assembly logic, which is separate from the service itself. It also allows the service it creates to be completely free from injection annotations. Still creating a provider class may be an overkill, and there is a simpler way to define small bits of assembly code - "provider methods" that are declared right in the module class:

public class HelloModule3 implements BQModule {

    @Singleton
    @Provides
    Hello provideHello(UserNameService nameService) {  (1)
        return new HelloService3(nameService);
    }

    @Override
    public void configure(Binder binder) { (2)
    }
}
1 Provider method that does both assembly and binding of the Hello service.
2 "configure" method can be empty, as the service was already bound via provideHello. Of course it can also contain bindings for unrelated services.

A few notes on the provideHello method. What makes it a provider is the @Provides annotation. Its access modifier is irrelevant (can be "public", "protected", etc.). As mentioned above, this method does both assembly and binding of the "Hello" service. The return type (Hello) is used as the binding key. And while there is no @Inject annotation in use anywhere, injection in fact also happens here - all method arguments ("nameService" in this case) are injected by the DI environment.

2.5. Advanced Injection

2.5.1. Injection in Collections

TODO

2.5.2. Injection and Generics

TODO

2.6. Module Providers and Auto-Loading

For a module to be included in BQRuntime, it has be explicitly mentioned when starting a Bootique app (Bootique.app(args).module(MyModule.class)). Though this is highly impractical in real apps, and most often than not the alternative auto-loading mechanism is used. To enable auto-loading, call Bootique.autoLoadModules() as a part of the startup sequence. Also modules themselves need to be created in a certain way to be eligible for auto-loading:

  1. There must be a special per-module class that implements io.bootique.BQModuleProvider:

public class MyModuleProvider implements BQModuleProvider {

    @Override
    public BQModule module() {
        return new MyModule();
    }
}

In fact, you don’t need a separate class. The module itself can implement this interface (see a discussion of BaseModule superclass).

  1. create a file META-INF/services/io.bootique.BQModuleProvider with the only line being the name of your provider implementor. E.g.:

com.foo.MyModuleProvider
All standard Bootique modules are auto-loadable.

BQModuleProvider has other methods that you can optionally implement that provide Bootique with extra metatada and other module information. Inspect the interface to see what they are.

2.7. BaseModule

Both module and provider classes can be combined in one to reduce the boilerplate code. To do that you simply subclass your module from BaseModule:

public class MyOtherModule extends BaseModule {

    @Override
    public void configure(Binder binder) {
        binder.bind(MyService.class).to(MyServiceImpl.class);
    }
}

For auto-loading you’d put it in META-INF/services/io.bootique.BQModuleProvider:

com.foo.MyOtherModule

Inheriting from BaseModule provides other advantages that we’ll discuss in the "Configuration" section and is a recommended approach to writing modules since Bootique 2.0.

2.8. Configuration and Configurable Factories

Bootique Modules obtain their configuration in a form of "factory objects". We’ll show some examples shortly. For now let’s focus on the big picture, namely the fact that Bootique app configuration is multi-layered and roughly follows the sequence of "code - config files (contributed) - config files (CLI) - overrides". "Code" is the default values that are provided in constructors of factory objects. Config files overlay those defaults with their own values. Config files can be either contributed in the code, or specified on the command line. Files is where the bulk of configuration usually stored. Finally, config values may be further overridden via Java properties and/or environment variables.

2.8.1. Configuration via YAML Files

Format of configuration file can be either JSON or YAML. For simplicity we’ll focus on YAML format, but the two are interchangeable. Here is an example config file:

log:
  level: warn
  appenders:
    - type: file
      logFormat: '%c{20}: %m%n'
      file: target/logback/debug.log

jetty:
  context: /myapp
  connectors:
    - port: 12009

While not strictly required, as a rule the top-level keys in the file belong to configuration objects of individual modules. In the example above "log" subtree configures bootique-logback module, while "jetty" subtree configures bootique-jetty. For standard modules refer to module-specific documentation on the structure of the supported configuration (or run your app -H flag to print supported config to the console). Here we’ll discuss how to build your own configuration-aware module.

Bootique allows each Module to read its specific configuration subtree as an object of the type defined in the Module. Very often such an object is written as a factory that contains a bunch of setters for configuration properties, and a factory method to produce some "service" a Module is interested in. Here is an example factory:

public class MyFactory {

    private int intProperty;
    private String stringProperty;

    public void setIntProperty(int i) {
        this.intProperty = i;
    }

    public void setStringProperty(String s) {
        this.stringProperty = s;
    }

    // factory method
    public MyService createMyService(SomeOtherService soService) {
        return new MyServiceImpl(soService, intProperty, stringProperty);
    }
}

The factory contains configuration property declarations, as well as public setters for these properties. (You may create getters as well. It is not required, but may be useful for unit tests, etc.). Now let’s take a look at the Module class:

public class MyModule extends BaseModule {

    @Singleton
    @Provides
    public MyService createMyService(
            ConfigurationFactory configFactory,
            SomeOtherService service) {

        return config(MyFactory.class, configFactory).createMyService(service);
    }
}

A sample configuration that will work with our module may look like this:

my:
  intProperty: 55
  stringProperty: 'Hello, world!'

A few points to note here:

  • Subclassing from BaseModule provides a few utilities, such as a shorter "config" method and a default configuration key ("my" in this case. See the next bullet).

  • Calling our module "MyModule" and extending from BaseModule gives it access to the protected "configPrefix" property that is initialized to the value of "my" based on the module class name. The naming convention here is to use the Module simple class name without the "Module" suffix and converted to lowercase.

  • @Provides annotation is a Bootique DI way of marking a BQModule method as a "provider" for a certain type of injectable service. All its parameters are themselves injectable objects.

  • ConfigurationFactory is the class used to bind a subtree of the app YAML configuration to a given Java object (in our case - MyFactory). The structure of MyFactory is very simple here, but it can be as complex as needed, containing nested objects, arrays, maps, etc. Internally Bootique uses Jackson framework to bind YAML to a Java class, so all the features of Jackson can be used to craft configuration.

2.8.2. Configuration File Loading

A config file can be passed to a Bootique app via DI (those are usually coming from classpath) or on the command line:

  • Contributing a config file via DI:

    BQCoreModule.extend(binder).addConfig("classpath:com/foo/default.yml");

    A primary motivation for this style is to provide application default configuration, with YAML files often embedded in the app and read from the classpath (as suggested by the "classpath:.." URL in the example). More than one configuration can be contributed. E.g. individual modules might load their own defaults. Multiple configs are combined in a single config tree by the runtime. The order in which this combination happens is undefined, so make sure there are no conflicts between them. If there are, consider replacing multiple conflicting configs with a single config.

  • Conditionally contributing a config file via DI. It is possible to make DI configuration inclusion conditional on the presence of a certain command line option:

    OptionMetadata o = OptionMetadata.builder("qa")
            .description("when present, uses QA config")
            .build();
    
    BQCoreModule.extend(binder)
            .addOption(o)
            .mapConfigResource(o.getName(), "classpath:a/b/qa.yml");
  • Specifying a config file on the command line. Each Bootique app supports --config option that takes a configuration file as parameter. To specify more than one file, use --config option multiple times. Configurations will be loaded and merged together in the order of their appearance on the command line.

  • Specifying a single config value via a custom option:

    OptionMetadata o1 = OptionMetadata.builder("db")
            .description("specifies database URL")
            .valueOptionalWithDefault("jdbc:mysql://127.0.0.1:3306/mydb")
            .build();
    
    BQCoreModule.extend(binder)
            .addOption(o1)
            .mapConfigPath(o1.getName(), "jdbc.mydb.url");

    This adds a new --db option to the app that can be used to set JDBC URL of a datasource called "mydb". If value is not specified, the default one will be used.

2.8.3. Configuration via Properties

YAML file can be thought of as a set of nested properties. E.g. the following config

my:
  prop1: val1
  prop2: val2

can be represented as two properties ("my.prop1", "my.prop2") being assigned some values. Bootique takes advantage of this structural equivalence and allows defining configuration via properties as an alternative (or more frequently - an addition) to YAML. If the same "key" is defined in both YAML file and a property, ConfigurationFactory would use the value of the property (in other words properties override YAML values).

To turn a given property into a configuration property, you need to prefix it with “bq.”. This "namespace" makes configuration explicit and helps to avoid random naming conflicts with properties otherwise present in the system.

Properties can be provided to Bootique via BQCoreModule extender:

class MyModule implements BQModule {
    public void configure(Binder binder) {

        BQCoreModule.extend(binder)
                .setProperty("bq.my.prop1", "valX")
                .setProperty("bq.my.prop2", "valY");
    }
}

Alternatively they can be loaded from system properties. E.g.:

java -Dbq.my.prop1=valX -Dbq.my.prop2=valY -jar myapp.jar

Though generally this approach is sneered upon, as the authors of Bootique are striving to make Java apps look minimally "weird" in deployment, and "-D" is one of those unintuitive "Java-only" things. Often a better alternative is to define the bulk of configuration in YAML, and pass values for a few environment-specific properties via shell variables (see the next section) or bind them to CLI flags.

2.8.4. Configuration via Environment Variables

Bootique allows to use environment variables to specify/override configuration values. While variables work similar to JVM properties, using them has advantages in certain situations:

  • They may be used to configure credentials, as unlike YAML they won’t end up in version control, and unlike Java properties, they won’t be visible in the process list.

  • They provide customized application environment without changing the launch script and are ideal for containerized and other virtual environments.

  • They are more user-friendly and appear in the app help.

To declare variables associated with configuration values, use the following API (notice that no "bq." prefix is necessary here to identify the configuration value):

class MyModule implements BQModule {
    public void configure(Binder binder) {

        BQCoreModule.extend(binder)
                .declareVar("my.prop1", "P1")
                .declareVar("my.prop2", "P2");
    }
}

So now a person running the app may set the above configuration as

export P1=valX
export P2=valY

Moreover, explicitly declared vars will automatically appear in the application help, assisting the admins in configuring your app

(TODO: document BQConfig and BQConfigProperty config factory annotations required for the help generation to work)

$ java -jar myapp-1.0.jar --help
...
ENVIRONMENT
      P1
           Sets value of some property.

      P2
           Sets value of some other property.

2.8.5. Polymorphic Configuration Objects

A powerful feature of Jackson is the ability to dynamically create subclasses of the configuration objects. Bootique takes full advantage of this. E.g. imagine a logging module that needs "appenders" to output its log messages (file appender, console appender, syslog appender, etc.). The framework might not be aware of all possible appenders its users might come up with in the future. Yet it still wants to have the ability to instantiate any of them, based solely on the data coming from YAML. Moreover, each appender will have its own set of incompatible configuration properties. In fact this is exactly the situation with bootique-logback module.

Here is how you ensure that such a polymorphic configuration is possible. Let’s start with a simple class hierarchy:

public abstract class BaseType {
    // ...
}

public class ConcreteType1 extends BaseType {
    // ...
}

public class ConcreteType2 extends BaseType {
    // ...
}

Now let’s create a matching set of factories to create one of the concrete subtypes of BaseType. Let’s use Jackson annotations to link specific types of symbolic names to be used in YAML below:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
        property = "type",
        defaultImpl = ConcreteTypeFactory1.class)
public abstract class BaseTypeFactory implements PolymorphicConfiguration {

    public abstract BaseType create();
}

@JsonTypeName("type1")
public class ConcreteTypeFactory1 extends BaseTypeFactory {

    @Override
    public BaseType create() {
        return new ConcreteType1();
    }
}

@JsonTypeName("type2")
public class ConcreteTypeFactory2 extends BaseTypeFactory {

    @Override
    public BaseType create() {
        return new ConcreteType2();
    }
}

After that we need to create a service provider file called META-INF/service/io.bootique.config.PolymorphicConfiguration where all the types participating in the hierarchy are listed (including the supertype):

com.foo.BaseTypeFactory
com.foo.ConcreteTypeFactory1
com.foo.ConcreteTypeFactory2

This should be enough to work with configuration like this:

my:
  type: type2
  someVar: someVal

The service of BaseType is bound in Bootique DI using the standard ConfigurationFactory approach described above. Depending on the YAML config, one of the subclasses of BaseType will be created:

@Provides
public BaseType provideBaseType(ConfigurationFactory configFactory) {

    return configFactory
            .config(BaseTypeFactory.class, "my")
            .create();
}

If another module decides to create yet another subclass of BaseType, it will need to create its own META-INF/service/io.bootique.config.PolymorphicConfiguration file and add a new factory name there.

2.9. Using Modules

Modules often depend on other "upstream" modules. They can interact with upstream modules in a few ways:

  • Use upstream services - inject objects defined in an upstream module in this module objects

  • Contribute extensions - inject module’s objects to collections and maps from the upstream module, thus extending upstream module behavior.

Let’s use BQCoreModule as an example of an upstream module, as it is available in all apps.

2.9.1. Injecting Other Module’s Services

You can inject any services declared in other modules. E.g. BQCoreModule defines an array of CLI arguments passed to the main() method, that can be injected in our module:

public class MyService {

    @Args (1)
    @Inject
    private String[] args;

    public String getArgsString() {
        return Stream.of(args).collect(joining(" "));
    }
}
1 Since there can potentially be more than one String[] in a DI container, Bootique @Args annotation is used to disambiguate the array that we are referring to.

2.9.2. Contributing to Other Modules

Bootique DI supports Collection bindings, intended to contribute objects defined in a downstream module to collections/maps used by services in upstream modules. Bootique hides DI API complexities, usually providing "extenders" in each module. E.g. the following code adds MyCommand the app set of commands:

public class MyModule implements BQModule {

    @Override
    public void configure(Binder binder) {
        BQCoreModule.extend(binder).addCommand(MyCommand.class);
    }
}

Here we obtained an extender instance via a static method on BQCoreModule. Most standard modules define their own extenders accessible via "extend(Binder)". This is a pattern you might want to follow in your own modules.

2.10. Application Class

A class containing the "main()" method is informally called "application". Bootique does not impose any additional requirements on this class. You decide what to put in it. It can be limited to just "main()", or be a REST API endpoint, or something else entirely.

Though the most common approach is to turn the application class into a Module. After all a Bootique app is a collection of Modules, and the application class would represent that one main Module to rule them all:

public class Application extends BaseModule {

    public static void main(String[] args) {
        Bootique.app(args)
                .autoLoadModules()
                .module(Application.class) (1)
                .exec()
                .exit();
    }

    public void configure(Binder binder) {
        // load app-specific services; redefine standard ones
    }
}
1 Explicitly adds Application to the list of runtime modules. Since the superclass of Application is BaseModule, you may put the application class in META-INF/services/io.bootique.BQModuleProvider to make it auto-loadable and remove this line.

2.10.1. Common Main Class

If all your code is packaged in auto-loadable modules (which is always a good idea), you may not even need a custom main class. io.bootique.Bootique class itself declares a main() method and can be used as an app launcher. This creates some interesting possibilities. E.g. you can create Java projects that have no code of their own and are simply collections of modules declared as compile dependencies. More details on packaging are given in the "Runnable Jar" chapter.

2.11. Commands

Bootique runtime contains a set of commands coming from Bootique core and from all the modules currently in effect in the app. On startup Bootique attempts to map command-line arguments to a single command type. If no match is found, a default command is executed (which is normally a "help" command). To list all available commands, the app can be run with --help option (in most cases running without any options will have the same effect). E.g.:

$ java -jar myapp-1.0.jar --help

NAME
      com.foo.MyApp

OPTIONS
      -c yaml_location, --config=yaml_location
           Specifies YAML config location, which can be a file path or a URL.

      -h, --help
           Prints this message.

      -H, --help-config
           Prints information about application modules and their configuration
           options.

      -s, --server
           Starts Jetty server.

2.11.1. Writing Commands

Most common commands are already available in various standard modules, still often you’d need to write your own. To do that, first create a command class. It should implement io.bootique.command.Command interface, though usually it more practical to extend io.bootique.command.CommandWithMetadata and provide some metadata used in help and elsewhere:

public class MyCommand extends CommandWithMetadata {

    private static CommandMetadata createMetadata() {
        return CommandMetadata.builder(MyCommand.class)
                .description("My command does something important.")
                .build();
    }

    public MyCommand() {
        super(createMetadata());
    }

    @Override
    public CommandOutcome run(Cli cli) {

        // ... run the command here....

        return CommandOutcome.succeeded();
    }
}

The command initializes metadata in constructor and implements the "run" method to run its code. The return CommandOutcome object instructs Bootique what to do when the command finishes. The object contains desired system exit code, and exceptions that occurred during execution. To make the new command available to Bootique, add it to `BQCoreModule’s extender, as was already shown above:

public class MyModule extends BaseModule {

    @Override
    public void configure(Binder binder) {
        BQCoreModule.extend(binder).addCommand(MyCommand.class);
    }
}

To implement a "daemon" command running forever until it receives an OS signal (e.g. a web server waiting for user requests) , do something like this:

@Override
public CommandOutcome run(Cli cli) {

    // ... start some process in a different thread ....

    // now wait till the app is stopped from another thread
    // or the JVM is terminated
    try {
        Thread.currentThread().join();
    } catch (InterruptedException e) {
        // ignore exception or log if needed
    }

    return CommandOutcome.succeeded();
}

2.11.2. Injection in Commands

Commands can inject services, just like most other classes in Bootique. There are some specifics though. Since commands are sometimes instantiated, but not executed (e.g. when --help is run that lists all commands), it is often desirable to avoid immediate instantiation of all dependencies of a given command. So a common pattern with commands is to inject javax.inject.Provider instead of direct dependency:

@Inject
private Provider<SomeService> provider;

@Override
public CommandOutcome run(Cli cli) {
    provider.get().someMethod();
}

2.11.3. Decorating Commands

Each command typically does a single well-defined thing, such as starting a web server, executing a job, etc. But very often in addition to that main thing you need to do other things. E.g. when a web server is started, you might also want to run a few more commands:

  • Before starting the server, run a health check to verify that any external services the app might depend upon are alive.

  • Start a job scheduler in the background.

  • Start a monitoring "heartbeat" thread.

To run all these "secondary" commands when the main command is invoked, Bootique provides command decorator API. First you create a decorator policy object that specifies one or more secondary commands and their invocation strategy (either before the main command, or in parallel with it). Second you "decorate" the main command with that policy:

CommandDecorator extraCommands = CommandDecorator
        .beforeRun(CustomHealthcheckCommand.class)
        .alsoRun(ScheduleCommand.class)
        .alsoRun(HeartbeatCommand.class);

    BQCoreModule.extend(binder).decorateCommand(ServerCommand.class, extraCommands);

Based on the specified policy Bootique figures out the sequence of execution and runs the main and the secondary commands.

2.12. Options

2.12.1. Simple Options

In addition to commands, the app can define "options". Options are not associated with any runnable java code, and simply pass command-line values to commands and services. E.g. the standard “--config” option is used to locate configuration file(s). Unrecognized options cause application startup errors. To be recognized, options need to be "contributed" to Bootique similar to commands:

OptionMetadata option = OptionMetadata
        .builder("email", "An admin email address")
        .valueRequired("email_address")
        .build();

    BQCoreModule.extend(binder).addOption(option);

To read a value of the option, a service should inject io.bootique.cli.Cli object (commands also get this object as a parameter to "run") :

@Inject
private Cli cli;

public void doSomething() {
    Collection<String> emails = cli.optionStrings("email");
    // do something with option values....
}

2.12.2. Configuration Options

While you can process your own options as described above, options often are just aliases to enable certain pieces of configuration. Bootique supports three flavors of associating options with configuration. Let’s demonstrate them here.

  1. Option value sets a config property:

    // Starting the app with "--my-opt=x" will set "jobs.myjob.param" value to "x"
    BQCoreModule.extend(binder)
            .addOption(OptionMetadata.builder("my-opt").build())
            .mapConfigPath("my-opt", "jobs.myjob.param");
  2. Option presence sets a property to a predefined value:

    // Starting the app with "--my-opt" will set "jobs.myjob.param" value to "y"
    BQCoreModule.extend(binder)
            .addOption(OptionMetadata.builder("my-opt").valueOptionalWithDefault("y").build())
            .mapConfigPath("my-opt", "jobs.myjob.param");
  3. Option presence loads a config resource, such as a YAML file:

    // Starting the app with "--my-opt" is equivalent to starting with "--config=classpath:xyz.yml"
    BQCoreModule.extend(binder)
            .addOption(OptionMetadata.builder("my-opt").build())
            .mapConfigResource("my-opt", "classpath:xyz.yml");

The order of config-bound options on the command line is significant, just as the order of “--config” parameters. Bootique merges configuration associated with options from left to right, overriding any preceding configuration if there is an overlap.

2.13. Logging

2.13.1. Loggers in the Code

Standard Bootique modules use SLF4J internally, as it is the most convenient least common denominator framework, and can be easily bridged to other logging implementations. Your apps or modules are not required to use SLF4J, though if they do, it will likely reduce the amount of bridging needed to route all logs to a single destination.

2.13.2. Configurable Logging with Logback

For better control over logging a standard module called bootique-logback is available, that integrates Logback framework in the app. It seamlessly bridges SLF4J (so you keep using SLF4J in the code), and allows to configure logging via YAML config file, including appenders (file, console, etc.) and per class/package log levels. Just like any other module, bootique-logback can be enabled by simply adding it to the pom.xml dependencies, assuming autoLoadModules() is in effect:

Maven
<dependency>
    <groupId>io.bootique.logback</groupId>
    <artifactId>bootique-logback</artifactId>
</dependency>
Gradle
{
  implementation: 'io.bootique.logback:bootique-logback'
}

See bootique-logback module documentation for further details.

2.13.3. BootLogger

To perform logging during startup, before DI environment is available and YAML configuration is processed, Bootique uses a special service called BootLogger, that is not dependent on SLF4J and is not automatically bridged to Logback. It provides an abstraction for writing to stdout / stderr, as well as conditional "trace" logs sent to stderr. To enable Bootique trace logs, start the app with -Dbq.trace as described in the deployment section.

BootLogger is injectable, in case your own code needs to use it. If the default BootLogger behavior is not satisfactory, it can be overridden right in the main(..) method, as unlike other services, you may need to change it before DI is available:

public class Application {
    public static void main(String[] args) {
        Bootique.app(args).bootLogger(new MyBootLogger()).createRuntime().run();
    }
}

3. Testing

3.1. Bootique and Testing

Bootique provides integration for both JUnit 4 and JUnit 5. Both can even be combined in the same project via JUnit 5 "vintage engine". This chapter will focus on JUnit 5 instructions. For JUnit 4 docs, please check documentation for Bootique 1.x.

Bootique is uniquely suitable to be used as a test framework. Within a single test it allows you to start and stop multiple in-process runtimes, each with а distinct set of modules and configurations. Bootique test facilities are controlled with a small number of annotations: @BQTest, @BQTestTool and @BQApp, that will be described below.

This chapter is about the core test framework. For module-specific test APIs (e.g. bootique-jdbc-junit5, bootique-jetty-junit5), check documentation of those modules. They all follow the same base principles described here.

To start using Bootique test extensions, import the following module in the "test" scope:

Maven
<dependency>
    <groupId>io.bootique</groupId>
    <artifactId>bootique-junit5</artifactId>
    <scope>test</scope>
</dependency>
Gradle
{
  testImplementation: 'io.bootique:bootique-junit5'
}

Each test class using Bootique extensions must be annotated with @BQTest:

@BQTest
public class MyTest {
}

Doing that tells Bootique to manage lifecycle of test BQRuntimes and test "tools".

3.2. Test BQRuntimes

BQRuntime is an object representing an entire app (i.e. an object whose state and behavior you are checking in your test). You can declare BQRuntime as a static field of the test class, initialize it the way you would initialize an application (yes, using the same factory method from Bootique class), and annotate with @BQApp to let Bootique start and stop it when appropriate:

@BQApp
final static BQRuntime app = Bootique
        .app("--server", "--config", "classpath:test.yml")
        .autoLoadModules()
        .createRuntime();

The "app" will be started by Bootique before all the tests in this class, and will be shutdown after all the tests are finished. You can declare any number of such "apps" within a test to emulate fairly complex systems. And as you can see in the example, you can pass any command or configuration as arguments to the app(..) method (that emulates a command-line invocation).

Sometimes you just need a runtime instance, and do not want to execute any commands. For this case use @BQApp(skipRun=true):

@BQApp(skipRun = true)
final static BQRuntime app = Bootique
        .app("--config", "classpath:test.yml")
        .autoLoadModules()
        .createRuntime();

3.3. Test Tools and Scopes

Bootique provides a number of test tools that help you customize the apps for the test environment (e.g. TcDbTester tool from bootique-jdbc-junit5-testcontainers starts a test database and supplies a "module" to the app with DataSource configuration to access that DB; JettyTester from bootique-jetty configures HTTP connector to listen on a random unoccupied port and provides a Java client to send requests to the server, etc.).

Tools are declared as fields in a test class, each annotated with @BQTestTool. A tool can exist in a certain scope (as defined by @BQTestTool(value=…​). "Scope" determines when the tool is initialized and shutdown during the test. The following scopes are available:

  • BQTestScope.TEST_CLASS: A scope of a single test class. Roughly corresponds to @BeforeAll / @AfterAll from JUnit. All tools in this scope must be declared as static variables.

  • BQTestScope.TEST_METHOD: A scope of a single test method. Roughly corresponds to @BeforeEach / @AfterEach from JUnit.

  • BQTestScope.GLOBAL: Allows a given tool to be shared with more than one test class. JUnit has no analog of a "global" scope, but it is quite useful for expensive reusable resources (e.g. Docker containers), so Bootique supports it for all the tools.

  • BQTestScope.IMPLIED: This is the default scope of @BQTestTool. If the field is static it is equivalent to TEST_CLASS, if non-static - TEST_METHOD. This way most of the time you don’t need to explicitly define the scope, but just need to declare your tool as static or instance variable.

Some examples:

// implied scope is TEST_CLASS
@BQTestTool
final static MyTool tool1 = new MyTool();
// implied scope is TEST_METHOD
@BQTestTool
final MyTool tool2 = new MyTool();
// explicit GLOBAL scope. The variable must be static
@BQTestTool(value = BQTestScope.GLOBAL)
final static MyTool tool3 = new MyTool();

3.4. BQTestFactory Tool

A very common test tool is BQTestFactory. It allows to create and run BQRuntimes on the fly inside test class methods, and as such is a more flexible alternative to @BQApp annotation:

@BQTestTool
final BQTestFactory testFactory = new BQTestFactory();

BQTestFactory.app() does roughly the same thing as Bootique.app(), with an important difference that all created runtimes will be automatically shut down at the end of the factory scope. E.g. in the following example testFactory would shut down any runtimes created in each test method right after that method finishes (since testFactory is an instance variable, the default scope is TEST_METHOD):

@Test
public void abc() {

    CommandOutcome result = testFactory.app("--server")
            // ensure all classpath modules are included
            .autoLoadModules()
            // add an adhoc module specific to the test
            .module(binder -> binder.bind(MyService.class).to(MyServiceImpl.class))
            .run();
    // ...
}

If you need the runtime instance to poke inside the app’s DI container, you can call createRuntime() instead of run():

@Test
public void xyz() {

    BQRuntime app = testFactory.app("--server")
            .autoLoadModules()
            .createRuntime();
    // ...
}

BQRuntime of course has its own run() method, so you can both inspect the runtime and run a command.

3.5. Common Test Scenarios

Among the things that can be tested are runtime services with real dependencies, standard output of full Bootique applications (i.e. the stuff that would be printed to the console if this were a real app), network services using real network connections (e.g. your REST API’s), and so on. Some examples are given below, outlining the common techniques.

3.5.1. Testing Injectable Services

Services can be obtained from test runtime, their methods called, and assertions made about the results of the call:

@BQApp(skipRun = true)
static final BQRuntime app = Bootique
        .app("--config=src/test/resources/my.yml")
        .createRuntime();

@Test
public void service() {
    MyService service = app.getInstance(MyService.class);
    assertEquals("xyz", service.someMethod());
}

3.5.2. Testing Network Services

If a test command starts a web server or some other network service, it can be accessed via a URL right after running the server. E.g.:

// a tool from "bootique-jetty-junit5". Doesn't require @BQTestTool,
// as it has no lifecycle of its own
static final JettyTester jetty = JettyTester.create();

@BQApp
static final BQRuntime app = Bootique
        .app("--server")
        .module(jetty.moduleReplacingConnectors())
        .createRuntime();

@Test
public void server() {
    Response response = jetty.getTarget().path("/somepath").request().get();
    JettyTester.assertOk(response);
}

3.5.3. Testing Commands

You can run the app in the test and check the values of the exit code and stdin and stderr contents:

@Test
public void command() {

    TestIO io = TestIO.noTrace();
    CommandOutcome outcome = testFactory
            .app("--help")
            .bootLogger(io.getBootLogger())
            .run();

    assertEquals(0, outcome.getExitCode());
    assertTrue(io.getStdout().contains("--help"));
    assertTrue(io.getStdout().contains("--config"));
}

3.5.4. Testing Module Validity

When you are writing your own modules, you may want to check that they are configured properly for autoloading (i.e. META-INF/services/io.bootique.BQModuleProvider is present in the expected place and contains the right provider. There’s a helper class to check for it:

@Test
public void autoLoadable() {
    BQModuleProviderChecker.testAutoLoadable(MyModuleProvider.class);
}

4. Assembly and Deployment

This chapter discusses how to package Bootique apps for deployment/distribution and how to run them. We are going to present two approaches that produce cross-platform runnable applications - "Runnable Jar with Dependencies" and "Runnable Jar with "lib" Folder". They only differ in how dependencies are packaged and referenced.

Ultimately any Bootique app is just a Java app with the main(..) method and hence can be executed using java command. With that understanding you can come up with your own custom packaging strategies.

4.1. Runnable Jar with Dependencies

Under this approach application classes, resources and all classes and resources from dependency jars are packaged in a single "fat" runnable jar. In Maven this can be accomplished with maven-shade-plugin:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <configuration>
                    <createDependencyReducedPom>true</createDependencyReducedPom>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>META-INF/*.SF</exclude>
                                <exclude>META-INF/*.DSA</exclude>
                                <exclude>META-INF/*.RSA</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>${main.class}</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </pluginManagement>
    <plugins>
        <plugin>
            <artifactId>maven-shade-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Make sure you set the main.class property in the POM to the appropriate class value from your application or to io.bootique.Bootique:

<properties>
    <main.class>com.foo.Application</main.class>
</properties>

Once the pom is configured, you can assemble and run the jar. E.g.:

mvn clean package
java -jar target/myapp-1.0.jar

4.2. Runnable Jar with "lib" Folder

"Jar-with-dependencies" packaging described above is extremely convenient. It produces a single file that is easy to move around and execute. It is not without downsides though:

  • It is incompatible with Java Platform Module System (JPMS). Java allows only one module-info.class file per .jar. So if your app or its dependencies contain one or more of those module descriptors, maven-shade-plugin won’t be able to package them properly.

  • It is incompatible with multi-release jar files. Actually there’s no technical reason why maven-shade-plugin can’t repackage such jars correctly, but as of this writing (plugin version 3.2.1) it doesn’t, losing Java version-specific code.

An alternative way of packaging that does not have these limitations is a folder with a runnable application jar at the root level and all dependency jars in the lib/ folder next to it:

my-app-1.0/
    # Runnable jar with classpath in MANIFEST.MF referencing "lib/*"
    my-app-1.0.jar
    # Dependencies folder
    lib/
        bootique-X.X.jar
        slf4j-api-1.7.25.jar
        ...

This folder is usually archived into a single .tar.gz or .zip file. It would then be unpacked on the machine where the application needs to run.

Creating such packaging with Maven involves maven-jar-plugin, maven-dependency-plugin and maven-assembly-plugin. First let’s create the folder structure:

<properties>
    <main.class>com.foo.Application</main.class>
</properties>
...
<build>
    <plugins>
        <plugin>
            <artifactId>maven-jar-plugin</artifactId>
            <configuration>
                <archive>
                    <manifest>
                        <mainClass>${main.class}</mainClass>
                        <addClasspath>true</addClasspath>
                        <classpathPrefix>lib/</classpathPrefix>
                        <useUniqueVersions>false</useUniqueVersions>
                    </manifest>
                </archive>
            </configuration>
        </plugin>
        <plugin>
            <artifactId>maven-dependency-plugin</artifactId>
            <configuration>
                <includeScope>runtime</includeScope>
                <outputDirectory>${project.build.directory}/lib</outputDirectory>
            </configuration>
            <executions>
                <execution>
                    <id>assembly</id>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>copy-dependencies</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

The above configuration places both main jar and "lib/" folder under target/, so you can build and run the app like this:

$ mvn clean package
$ java -jar target/myapp-1.0.jar

To prepare the app for distribution as a single archive, you will need to add an assembly step. Start by creating an assembly.xml descriptor file:

<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 https://maven.apache.org/xsd/assembly-2.0.0.xsd">
    <id>tar.gz</id>
    <formats>
        <format>tar.gz</format>
    </formats>
    <fileSets>
        <fileSet>
            <directory>${project.build.directory}</directory>
            <useDefaultExcludes>true</useDefaultExcludes>
            <outputDirectory>./</outputDirectory>
            <includes>
                <include>${project.artifactId}-${project.version}.jar</include>
                <include>lib/</include>
            </includes>
        </fileSet>
    </fileSets>
</assembly>

Now configure maven-assembly-plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.1.1</version>
    <configuration>
        <appendAssemblyId>false</appendAssemblyId>
        <descriptors>
            <descriptor>assembly.xml</descriptor>
        </descriptors>
        <tarLongFileMode>posix</tarLongFileMode>
    </configuration>
     <executions>
        <execution>
            <id>assembly</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

After you rerun packaging again, you should see my-app-1.0.tar.gz file in the target folder. This file can be sent to the end users or copied to your servers and unpacked there:

$ mvn clean package
$ ls target/*.tar.gz

my-app-1.0.tar.gz
An extra benefit of such packaging is that you can include any additional files with your application distro, such as installation instructions, custom startup scripts, licenses, etc. All of this is configured in assembly.xml.

4.3. Tracing Bootique Startup

To see what modules are loaded, to view full app configuration tree and to trace other events that happen on startup, run your app with -Dbq.trace option. E.g.:

$ java -Dbq.trace -jar target/myapp-1.0.jar --server

You may see an output like this:

Skipping module 'JerseyModule' provided by 'JerseyModuleProvider' (already provided by 'Bootique')...
Adding module 'BQCoreModule' provided by 'Bootique'...
Adding module 'JerseyModule' provided by 'Bootique'...
Adding module 'JettyModule' provided by 'JettyModuleProvider'...
Adding module 'LogbackModule' provided by 'LogbackModuleProvider'...
Merged configuration: {"log":{"logFormat":"[%d{\"dd/MMM/yyyy:HH:mm:ss,SSS\"}]
%t %p %X{txid:-?} %X{principal:-?} %c{1}: %m%n%ex"},"trace":""}
Printing configuration may expose sensitive information, like database passwords, etc. Make sure you use -Dbq.trace for debugging only and don’t leave it on permanently in a deployment environment.