public class HelloService2 implements Hello {
- private UserNameService nameService;
+ private final UserNameService nameService;
@Inject
public HelloService2(UserNameService nameService) {
@@ -511,41 +526,11 @@ 2.2.2. Bindings
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 regular objects (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" discussed next.
-
-
2.2.3. Provider Module Methods
+
2.2.3. Provider Methods in Modules
-
Any method of a module class annotated with @Provides
is treated as an object declaration. E.g.:
+
If the service construction requires additional assembly logic, you can create simple "provider" methods in the module class. Any method of a module class annotated with @Provides
is treated a "provider" method. E.g.:
@@ -572,7 +557,7 @@
2.2.3. Provider Module Methods
2 |
- configure(..) method can be empty, as the service was already bound via provideHello . Of course, it can also contain bindings for unrelated services. |
+ configure(..) method may be empty, or contain other bindings |
@@ -582,31 +567,68 @@ 2.2.3. Provider Module Methods
-
2.2.4. Injection in Collections
+
2.2.4. Provider Objects
+
+
If more complex logic or lots of dependencies are required, instead of a provider method, you can create a provider class that implements Provider<MyService>
. E.g.:
+
+
+
+
public class HelloService3Provider implements Provider<Hello> {
+
+ @Inject
+ private UserNameService nameService;
+
+ @Override
+ public Hello get() {
+ return new HelloService3(nameService);
+ }
+}
+
+
+
+
It needs to be bound to an object declaration via toProvider(..)
method;
+
+
+
+
@Override
+public void configure(Binder binder) {
+ binder.bind(Hello.class)
+ .toProvider(HelloService3Provider.class)
+ .inSingletonScope();
+}
+
+
+
+
Provider object has the same rules for injection of its own dependencies as regular objects, i.e. either via annotated fields or an annotated constructor.
+
+
+
+
2.2.5. Injection in Collections
-
2.2.5. Injection and Generics
+
2.2.6. Injection and Generics
-
2.3. Configuration and Configurable Factories
+
2.3. Configuration
-
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.
+
Bootique app can accept external configuration in different forms, coming from different sources. Internally, all configuration sources are combined following the rules described below into a single JSON-like tree object, that is used to initialize properties of some application objects.
-
-
2.3.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:
+
+
Common configuration formats are JSON or YAML. For simplicity, we’ll focus on YAML, but the two are interchangeable. Other sources are shell variables, Java properties and even cloud secrets managers. No matter which source (or combination of them) you use, all of them end up in a single tree object whose nodes match the structures of the application Java objects they will ultimately be converted to.
+
+
+
Here is an example YAML file:
+
+
+
+
log:
level: warn
appenders:
- type: file
@@ -617,134 +639,206 @@ 2.3.1. Configuration via YAML Files
context: /myapp
connectors:
- port: 12009
-
+
+
+
By convention, the top-level keys correspond to the names of modules that use a given configuration. In the example above, log
subtree configures bootique-logback
module, while jetty
configures bootique-jetty
.
+
+
+
+
+
+ |
+ For most standard modules, configuration formats are described in the module documentation. But each Bootique application contains a -H command that displays the full config structure for that app’s collection of modules. The output of this command is always the most accurate reference. |
+
+
+
+
+
+
2.3.1. Configuration Basics
-
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.
+
Consider the following YAML:
+
+
+
+
my:
+ intProperty: 55
+ stringProperty: 'Hello, world!'
+
-
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:
+
and the following object with matching property names:
-
public class MyFactory {
+ @BQConfig (1)
+public class MyObject {
+
+ final SomeOtherService soService;
private int intProperty;
private String stringProperty;
+ @Inject
+ public MyObject(SomeOtherService soService) { (2)
+ this.soService = soService;
+ }
+
+ @BQConfigProperty (3)
public void setIntProperty(int i) {
this.intProperty = i;
}
+ @BQConfigProperty (4)
public void setStringProperty(String s) {
this.stringProperty = s;
}
- // factory method
- public MyService createMyService(SomeOtherService soService) {
- return new MyServiceImpl(soService, intProperty, stringProperty);
+ public void doSomething() {
+ // ..
}
}
+
+
+
+
+ 1 |
+ The optional BQConfig annotation helps to include this object in the output of the -H command |
+
+
+ 2 |
+ Objects created from configuration can also inject any dependencies just like other Bootique objects |
+
+
+ 3 |
+ A setter used to load intProperty into the object. The optional BQConfigProperty annotation ensures the property is included in the output of the -H command |
+
+
+ 4 |
+ A setter used to load stringProperty into the object. The optional BQConfigProperty annotation ensures the property is included in the output of the -H command |
+
+
+
+
-
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:
+
To create MyObject
and load configuration values into it, we can use the following API:
-
public class MyModule extends BaseModule {
+ @Singleton
+@Provides
+public MyObject createMyService(ConfigurationFactory configFactory) {
+ return configFactory.config(MyObject.class, "my");
+}
+
+
+
+
In this example, MyObject
obtains its properties separately from constructor injection and configuration. ConfigurationFactory
is the object available in the core Bootique that holds the main config tree and serves as a factory for configuration-aware objects. The structure of MyObject
is very simple, but it can be as complex as needed, containing nested objects, arrays, maps, etc. Internally Bootique uses Jackson framework to bind YAML/JSON to Java objects, so all the features of Jackson can be used to craft configuration.
+
+
+
+
2.3.2. Object Factories
+
+
Very often configuration-aware objects are not retained by the app, but are created only to serve as factories of other objects, and then discarded. For instance, we can turn MyObject
above into MyFactory
that creates an object called MyService
:
+
+
+
+
@BQConfig
+public class MyFactory {
- @Singleton
- @Provides
- public MyService createMyService(
- ConfigurationFactory configFactory,
- SomeOtherService service) {
+ private int intProperty;
+ private String stringProperty;
- return config(MyFactory.class, configFactory).createMyService(service);
+ @BQConfigProperty
+ public void setIntProperty(int i) {
+ this.intProperty = i;
+ }
+
+ @BQConfigProperty
+ public void setStringProperty(String s) {
+ this.stringProperty = s;
+ }
+
+ // factory method
+ public MyService createMyService(SomeOtherService soService) {
+ return new MyServiceImpl(soService, intProperty, stringProperty);
}
}
-
-
A sample configuration that will work with our module may look like this:
-
-
my:
- intProperty: 55
- stringProperty: 'Hello, world!'
+
@Singleton
+@Provides
+public MyService provideMyService(
+ ConfigurationFactory configFactory,
+ SomeOtherService service) {
+
+ return configFactory.config(MyFactory.class, "my").createMyService(service);
+}
-
A few points to note here:
+
Also, here instead of injecting SomeOtherService
into the factory, we injected it in the provider method. This is just another flavor. This is not specific to factories of course.
-
-
- -
-
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.
-
+
+
+
+
+ |
+ All configuration-aware objects in Bootique standard modules are factories. We think this is the best pattern, and we recommend it to everyone, as it separates the actual application object from its configuration and factory code. This results in more "compact" and fully immutable objects. Though of course, the choice is ultimately left to the user. |
+
+
+
-
2.3.2. Configuration File Loading
+
2.3.3. Configuration Loading
-
A config file can be passed to a Bootique app via DI (those are usually coming from classpath) or on the command line:
+
Configuration is multi-layered and can come from different sources:
-
-
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.
-
+ Config files (defined in the module code or passed on the command line)
-
-
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");
-
-
+ Java properties
-
-
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.
+ Environment variables
-
-
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.
-
+ Cloud secrets managers
+ -
+
Custom configuration loaders
+
+
The load sequence of the sources is roughly this: (1) config files from modules → (2) config files from command line → (3) properties → (4) env variables. The sources with the higher load order override values from the sources with the lower order.
+
-
2.3.3. Configuration via Properties
+
2.3.4. Configuration Files from Modules
+
+
One way to pass a config file to a Bootique app in the code.
+
+
+
+
BQCoreModule.extend(binder).addConfig("classpath:com/foo/default.yml");
+
+
+
+
Such configuration should be known at compile time and is usually embedded in the app, and referenced via the special classpath:…
URL. A primary motivation for this style is to provide application default configuration. More than one configuration can be contributed. Individual modules might load their own defaults independently of each other. The order of multiple configs loaded via this mechanism is undefined and should not be relied upon.
+
+
+
+
2.3.5. Configuration Files from CLI
+
+
Config files can be specified on the command line with the --config
option. It takes a file (or a URL) as a parameter. You can use --config
multiple times to specify more than one file. The files will be loaded in the order of their appearance on the command line.
+
+
+
+
2.3.6. Configuration Properties
YAML file can be thought of as a set of nested properties. E.g. the following config
@@ -756,13 +850,10 @@
2.3.3. Configuration via Properties
-
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.
+
can be represented as two properties (my.prop1
, my.prop2
) that are assigned some values. Bootique takes advantage of this structural equivalence and allows to defined individual configuration values via Java properties. All configuration properties are prefixed with bq.
. This "namespace" helps to avoid random naming conflicts with properties otherwise present in the system.
-
Properties can be provided to Bootique via BQCoreModule extender:
+
Properties can be provided via BQCoreModule
extender:
@@ -777,34 +868,43 @@
2.3.3. Configuration via Properties
-
Alternatively they can be loaded from system properties. E.g.:
+
or come from system properties:
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.
+
+
+
+
+ |
+ According to the Bootique authors' opinion, properties is the worst mechanism for app configuration, as there are so many better options. However, there is a common valid use case for them - building configurations dynamically in the code. |
+
+
+
-
2.3.4. Configuration via Environment Variables
+
2.3.7. Configuration 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:
+
Bootique allows to use environment variables to specify/override configuration values. Variables work similar to JVM properties, but have a number of advantages:
-
-
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.
+ Provide customized application environment without changing the launch script
+ -
+
A natural approach for CI/CD, container and cloud environments
-
-
They provide customized application environment without changing the launch script and are ideal for containerized and other virtual environments.
+ Good for credentials. Unlike YAML, vars usually don’t end up in version control, and unlike Java properties, they are not visible in the process list
-
-
They are more user-friendly and appear in the app help.
+ Appear explicitly 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):
+
To declare variables associated with configuration values, use the following API (notice that no bq.
prefix is necessary here to identify the configuration value):
@@ -830,9 +930,6 @@
2.3.4. Configuration via Envir
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
@@ -847,7 +944,42 @@ 2.3.4. Configuration via Envir
-
2.3.5. Polymorphic Configuration Objects
+
2.3.8. CLI Configuration Aliases
+
+
It is possible to make fixed 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");
+
+
+
+
Also, it is possible to link a single configuration value to a named CLI 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.3.9. 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.
@@ -941,74 +1073,7 @@
2.3.5. Polymorphic Configuration Obj
-
2.4. 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.4.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.4.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.5. Commands
+
2.4. 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.:
@@ -1035,7 +1100,7 @@
2.5. Commands
-
2.5.1. Writing Commands
+
2.4.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:
@@ -1068,7 +1133,7 @@
2.5.1. Writing Commands
-
public class MyModule extends BaseModule {
+ public class MyModule implements BQModule {
@Override
public void configure(Binder binder) {
@@ -1101,7 +1166,7 @@ 2.5.1. Writing Commands
-
2.5.2. Injection in Commands
+
2.4.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:
@@ -1118,7 +1183,7 @@
2.5.2. Injection in Commands
-
2.5.3. Decorating Commands
+
2.4.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:
@@ -1151,9 +1216,9 @@
2.5.3. Decorating Commands
-
2.6. Options
+
2.5. Options
-
2.6.1. Simple Options
+
2.5.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:
@@ -1183,7 +1248,7 @@
2.6.1. Simple Options
-
2.6.2. Configuration Options
+
2.5.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.
@@ -1227,15 +1292,15 @@
2.6.2. Configuration Options
-
2.7. Logging
+
2.6. Logging
-
2.7.1. Loggers in the Code
+
2.6.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.7.2. Configurable Logging with Logback
+
2.6.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:
@@ -1269,7 +1334,7 @@
2.7.2. Configurable Logging with Log
-
2.7.3. BootLogger
+
2.6.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.
diff --git a/docs/3.x/bootique-docs/index.toc.html b/docs/3.x/bootique-docs/index.toc.html
index 43095598..882ad4f7 100644
--- a/docs/3.x/bootique-docs/index.toc.html
+++ b/docs/3.x/bootique-docs/index.toc.html
@@ -12,12 +12,11 @@
2. Programming
3. Testing