Decomposition¶
Guide for writing re-usable bundles
In dropwizard there is only one decomposition element: ConfiguredBundle.
Note
In pure dropwizard, bundles may also be used within a single application to separate configuration blocks (simplify logic). In guicey, this is not required as classpath scan may be used for extension registration and reduce the amount of required configuration.
This chapter describes only re-usable logic decomposition.
In guicey there are three decomposition elements - guicey bundles (GuiceyBundle
), guice modules (Module
) and dropwizard bundle (ConfiguredBundle
).
Having three options could be confusing.
- There are existing dropwizard modules -
ConfiguredBundle
- Existing guice modules (outdated list, just as an example) -
Module
- And guicey extensions -
GuiceyBundle
All of these modules are supposed to be used together. In some cases, guicey explicitly provides wrapping modules (e.g. the jdbi wrapper around the dropwizard module). Such wrappers provide guice related features and enhancements, impossible in vanilla dropwizard modules.
Guicey bundle¶
As described here, guicey introduces its own bundle because guicey provides additional configuration features. Even a simple use case may have a need to configure guice modules from a bundle.
Prefer GuiceyBundle
over dropwizard Bundle
for developing re-usable modules.
Of course, if module is very generic (does not depend on guice) you can use a pure dropwizard module
(to publish it for wider audience), but almost all bundles rely on guicey features.
Benefits:
- guice support (ability to register guice modules)
- options support
- use sub-configuration objects directly (important for writing generic modules)
- define custom extension types to simplify usage (e.g. like jdbi)
- automatic module loading when jar appear in classpath (e.g. like lifecycle annotations)
- shared state - advanced techniques for bundle communication (e.g. used by GSP and SPA)
- events - internal lifecycle events for fine-tuning lifecycle (again, complex cases only, for example, GSP use it to order bundles logic)
- ability to replace functionality (prevent feature x registration by disabling it and register feature y instead)
Dropwizard bundle¶
It is important to note that there is an important difference between registering a dropwizard bundle directly via the
dropwizard Bootstrap
and registering a bundle through the guicey api - any bundle registered through the guicey api
may be disabled, de-duplicated, and/or
tracked for transitive bundles.
All bundles registered through guicey api will also appear in configuration report.
There is a difference between the order that dropwizard and guicey bundles are initialized:
Dropwizard bundles immediately initialize a transitive bundle.
public class MyBundle implements ConfiguredBundle {
@Override
public void initialize(Bootstrap<?> bootstrap) {
bootstrap.addBundle(new MyOtherBundle());
// line executed after MyOtherBundle init
}
}
Guicey bundle registers a transitive bundle after current bundle, but dropwizard bundles are still immediately initialized.
public class MyBundle implements GuiceyBundle {
@Override
public void initialize(GuiceyBootstrap bootstrap) {
bootstrap
.dropwizardBundles(new DwBundle())
.bundles(new MyOtherBundle());
// line executed before MyOtherBundle init, but after DwBundle init
}
}
Guicey bundle de-duplication logic is further explained here. In short, registered root bundles must be initialized in priority. This avoids situations like:
GuiceBundle.builder()
.bundles(new Bundle1(), new Bundle2(12))
If Bundle2
is unique and Bundle1
transitively installs new Bundle2(1)
(e.g. with different config),
then this transitive bundle would be ignored because the root bundle's init will appear first.
If guicey bundles worked like dropwizard bundles, then the directly registered Bundle2
would be ignored
and remaining instance would have different configuration, introducing a major point of confusion.
Normally, this behaviour should not be an issue as you shouldn't rely on bundles initialization order. But this may be important with shared state.
Bundle vs Module¶
When extracting functionality into re-usable module always start with a bundle. A guice module will likely be required as well.
Logic should be separated as:
- Guice module is responsible for guice bindings and should not be aware of dropwizard.
- Bundle works with dropwizard, extracts required configuration for creating module and do other registrations.
That's an ideal case. But, for example, if you need to apply some bindings based on configuration only then you can do it with pure guice module, like:
public class ModuleConfig {
@JsonProperty
private String something;
}
Module knows that target application (where this re-usable module would be used) will declare this configuration inside its main configuration:
public class AppConfig extends Configuration {
@JsonProperty
private ModuleConfig module;
}
Make module aware of dropwizard stuff:
public class ModuleImpl<C extends Configuration> extends DropwizardAwareModule<C> {
@Override
protected void configure() {
// obtain sub-configuration object
ModuleConfig config = Preconditions.checkNotNull(configuration(ModuleConfig.class),
"ModuleConfig is not found within application config. Please declare it.");
// use it for binding
bind(SomeService.class).annotatedWith(Names.named(config.getSomething())).to(SomeServiceImpl.class);
}
}
Warning
This is not the recommended way! It was shown just to demonstrate that guice module could be used without bundle. It's better to use a declarative bundle instead:
public class ModuleImpl extends AbstractBundle {
private ModuleConfig config;
public ModuleImpl(ModuleConfig config) {
this.config = config;
}
@Override
protected void configure() {
bind(SomeService.class).annotatedWith(Names.named(config.getSomething())).to(SomeServiceImpl.class);
}
}
public class ModuleBundle extends GuiceyBundle {
@Override
public void run(GuiceyEnvironment environment) throws Exception {
ModuleConfig config = Preconditions.checkNotNull(environment.configuration(ModuleConfig.class),
"ModuleConfig is not found within application config. Please declare it.");
environment.modules(new ModuleImpl(config));
}
}
Bundle tips¶
These tips show various techniques for developing bundles.
Mostly, these tips are based on developing guicey extensions.
See extensions source for examples.
Uniqueness¶
For everything that is registered "by instance", the de-duplication mechanism is applied.
You can use it to provide only one instance of a bundle by extends
UniqueGuiceyBundle
. If more sophisticated logic is required, a manual equals and hash code implementation,
may be used. This could be used to de-duplicate only instances with the same constructor arguments.
This is most applicable to guice modules as guice will not start with duplicate bindings
(MyModule extends UniqueModule
).
Auto-loaded bundle¶
Auto-loading is based on the guicey bundles lookup feature.
Be aware that a user may switch off bundle lookup (with .disableBundleLookup()
) or apply custom lookup).
Auto load override¶
If your bundle provides configuration, but you still want to load it automatically with the default configuration, then you can use bundle uniqueness:
public class AutoLoadableBundle extends UniqueGuiceyBundle { ... }
If this is used, only one bundle instance is allowed. If a user registers another instance of the bundle manually, the bundle found from a lookup will simply be ignored. The lifecycle annotations module uses this technique.
Optional extensions¶
All extensions must be registered during the initialization phase, when configuration is not yet available and so it is not possible to implement optional extension registration.
To work around this, you can conditionally disable extensions:
public class MyFeatureBundle implements GuiceyBundle {
@Override
public void initialize(GuiceyBootstrap bootstrap) {
// always register extension
bootstrap.extensions(OptionalExtension.class);
}
@Override
public void run(GuiceyEnvironment environment) throws Exception {
// disable extension based on configuration value
if (!environment.configuration().getSomeValue()) {
environment.disableExtension(OptionalExtension.class);
}
}
}
Replace features¶
As bundle has almost complete access to configuration, it can use disables to substitute application functions.
For example, it is known that an application uses ServiceX
from some core module provided by the organization. Your module
requires a modified service. Your bundle may disable the core module, and install a customized module as a replacement:
public class MyFeatureBundle implements GuiceyBundle {
@Override
public void initialize(GuiceyBootstrap bootstrap) {
bootstrap
.disableModules(CoreModule.class)
.modules(new CustomizedCoreModule());
}
}
Note
This is not the best pattern to follow. It is simpler to use binding override to override single service. This is an example for demonstration purposes.
Bundles can't disable other bundles (because target bundle could be already processed at this point).
Bundle options¶
Bundles can use the guicey options mechanism to access guicey option values:
public class MyBundle implements GuiceyBundle {
@Override
public void initialize(GuiceyBootstrap bootstrap) {
if (bootstrap.option(GuiceyOptions.UseHkBridge)) {
// show warning that bridge required
}
}
}
There is also support for custom options.
Note
Option values are set only in main GuiceBundle
. They are immutable, so all bundles receive the same option values.
Configuration access¶
A bundle may access direct dropwizard Configuration
, as well as individual values thanks to yaml values
introspection.
Qualified properties¶
Configuration properties, required for injection, could be simply annotated with a qualifier annotations:
public class MyConfig extends Configuration {
@Named("custom")
private String prop1;
@CustomQualifier
private SubObj obj1 = new SubObj();
And used directly in service:
@Singleton
public class MyService {
@Inject @Named("custom") String prop;
@Inject @CustomQualifier SubObj obj;
}
Or accessed in bundle (or guice module):
public class XFeatureBundle implements GuiceyBundle {
@Override
public void run(GuiceyEnvironment environment) throws Exception {
SubObj config = bootstrap.annotatedValue(CustomQualifier.class);
...
}
}
Unique sub config¶
When creating re-usable bundle it is often required to access yaml configuration data. Usually this is solved by some "configuration look-ups" like in dropwizard-views
Guicey allows you to obtain the sub-configuration object directly:
public class XFeatureBundle implements GuiceyBundle {
@Override
public void run(GuiceyEnvironment environment) throws Exception {
XFeatureConfig environment = bootstrap.configuration(XFeatureConfig.class);
...
}
}
Note that this bundle doesn't know exact type of user configuration, it just
assumes that XFeatureConfig
is declared somewhere in configuration at any level
just once. For example:
public class MyConfig extends Configuration {
@JsonProperty
private XFeatureConfig xfeature;
...
}
Important
Your sub configuration object must appear only once within user configuration.
Object uniqueness checked by exact type match, so if configuration also
contains some extending class (XFeatureConfigExt extends XFeatureConfig
)
it will be different unique config.
Access by path¶
When you are not sure that configuration is unique, you can rely on exact path definition of required sub configuration:
public class XFeatureBundle implements GuiceyBundle {
private String path;
public XFeatureBundle(String path) {
this.path = path;
}
@Override
public void run(GuiceyEnvironment environment) throws Exception {
XFeatureConfig conf = environment.configuration(path);
...
}
}
The Path may be declared by the bundle user, who knows required configuration location:
GuiceBundle.builder()
.bundles(new XFeatureBundle("sub.feature"))
...
.build()
Where
public class MyConfig extends Configuration {
@JsonProperty
private SubConfig sub = { // pseudo code to combine class declarations
@JsonProperty
private XFeatureConfig feature;
}
...
}
Multiple configs¶
In case, when multiple config objects could be declared in user configuration, you can access all of them:
public class XFeatureBundle implements GuiceyBundle {
@Override
public void run(GuiceyEnvironment environment) throws Exception {
List<XFeatureConfig> confs = environment.configurations(XFeatureConfig.class);
...
}
}
For configuration
public class MyConfig extends Configuration {
@JsonProperty
private XFeatureConfig xfeature;
@JsonProperty
private XFeatureConfig xfeature2;
...
}
This configurations
method will return both objects: [xfeature, xfeature2]
Important
In contrast to unique configurations, this method returns all subclasses as well.
So if there are XFeatureConfigExt extends XFeatureConfig
declared somewhere it will also be returned.
Custom configuration analysis¶
In all other cases (with more complex requirements) you can use ConfigurationTree
object which
represents introspected configuration paths.
public class XFeatureBundle implements GuiceyBundle {
@Override
public void run(GuiceyEnvironment environment) throws Exception {
// get all properties of custom configuration (ignoring properties from base classes)
List<ConfigPath> paths = environment.configurationTree()
.findAllRootPathsFrom(MyConfig.class);
// search for not null values of marked (annotated) classes
List markedTypes = paths.stream()
.filter(it -> it.getValue() != null
&& it.getType().getValueType().hasAnnotation(MyMarker.class))
.map(it -> it.getValue())
.collect(Collectors.toList());
...
}
}
In this example, the bundle searches for properties declared directly in the MyConfig
configuration
class with non-null values and the custom marker (@MyMarker
) class annotation.
See introspected configuration structure description for details.
Shared state¶
Guicey maintains special shared state object useful for storing application-wide data.
Warning
Yes, any shared state is a "hack". Normally, you should avoid using it. Guicey provides this ability to unify all such current and future hacks: so if you need to communicate between bundles - you don't need to reinvent the wheel and don't have additional problems in tests (due to leaking states).
For example, it is used by spa bundle to share list of registered application names between all spa bundle instances and so be able to prevent duplicate name registration.
Server pages bundle use shared state to maintain global configuration and allow application bundles communication with global views bundle.
Equal communication scenario¶
Use the following in cases when multiple (equal) bundles need to communicate, the first initialized bundle will initialize the shared state and others simply use it:
public class EqualBundle implements GuiceyBundle {
@Override
public void initialize(GuiceyBootstrap bootstrap) {
// either obtain already shared object or share new object
SomeState state = bootstrap.sharedState(EqualBundle, () -> new SomeState());
...
}
}
Parent-child scenario¶
Use the following in cases when there is one global bundle, which must initialize some global state and child bundles, which use or append to this global state:
public class GlobalBundle implements GuiceyBundle {
@Override
public void initialize(GuiceyBootstrap bootstrap) {
// share global state object
bootstrap.shareState(GlobalBundle, new GlobalState());
}
}
public class ChildBundle implements GuiceyBundle {
@Override
public void initialize(GuiceyBootstrap bootstrap) {
// access shared object or fail when not found
GlobalState state = environment.sharedStateOrFail(GlobalBundle,
"Failed to obtain global state - check if global bundle registered");
}
}
Before/after run logic¶
If multiple bundles must be synchronized on run phase, use guicey events.
To run code after all guicey bundles have been initialized, but before run is called:
@Override
public void initialize(final GuiceyBootstrap bootstrap) {
bootstrap.listen(new GuiceyLifecycleAdapter() {
@Override
protected void beforeRun(final BeforeRunEvent event) {
// do something before bundles run
// NOTE that environment and configuration already available!
}
});
}
To run code after all guicey bundles run methods have been called (delayed init):
@Override
public void run(final GuiceyEnvironment environment) {
environment.listen(new GuiceyLifecycleAdapter() {
@Override
protected void bundlesStarted(final BundlesStartedEvent event) {
// still sdropwizard run phase (anything could be configured)
// but all guicey bundles aready executed
}
});
}
Note
This will work only for guicey bundles! Registered dropwizard bundles may
execute before or after these events. Events are broadcast from the main dropwizard
GuiceBundle
run method, so other dropwizard bundles, registered after guice bundle
will run after it.
It is assumed that guicey bundles will be used for most configurations, but especially
in complex cases when bundle synchronization is required.