Dropwizard Order of Operations: Endpoint Startup
When building bundles that interact with each other and depend upon code in the application, it’s important to understand the order in which components are initialized to ensure that everything your bundle needs is ready, and that your bundle is ready before anything that needs it. Dropwizard is extremely flexible in that both it and Jersey provide many places to hook and alter the initialization procedure, but few are well documented.
Container Startup
As your application starts up, the following sequence of events will occur; Major milestones have been bolded:
Application.initialize(Bootstrap)
is called; Any bundles that are added to the bootstrap are immediately initialized. Any customizations to the following must be completed in this method:HealthCheckRegistry
MetricRegistry
ConfigurationFactoryFactory
ObjectMapper
(used to read the configuration)ValidatorFactory
ClassLoader
- Default metrics are created and added to the
MetricRegistry
- Dropwizard determines which command will be run, such as
server
orcheck
. We’re focusing on theserver
command for the rest of this document. - The configuration is deserialized
- The
Bundle.run(Environment)
method is invoked for all non-configured bundles added to the Bootstrap- This is where all Jersey components should be registered. If enough information is known to instantiate HealthChecks, Metrics, MetricSets, Manageds, or LifeCycle.Listeners, they can be registered here, but if they require injection then they will have to wait until step 14.
- The order in which bundles are invoked is undefined, though typically it is the order in which they were registered in Step 1
- The
ConfiguredBundle.run(Configuration, Environment)
method is invoked for all configured bundles added to the Bootstrap- Same as step 5, but the configuration is also available.
- The
Application.run(Configuration, Environment)
method is invoked- This is where all the developer code will be registered, such as resources and specific Jersey components used by this endpoint
- All components registered with
environment.lifecycle()
are added to the JettyServer
instance, and then Jetty begins startupenvironment.lifecycle()
is more or less ignored after this point. You can add things to it, but they will never be started, nor will they be registered for shutdown callbacks.
- The lifeCycleStarting(LifeCycle) event is fired for all LifeCycle.Listener with an instance of org.eclipse.jetty.server.Server as the life cycle
- This is the only place a reference to the Server instance is exposed prior to the container completing startup.
- All HK2 Binder instances (I.e.,
new AbstractBinder() { ... }
) registered with Jersey are added to HK2 - All Jersey Features that were registered via
environment.jersey().register(Class)
are injected and configured - All HK2 Binders that were registered by Features to the context are added to HK2
- All Jersey component providers (Anything which extends or implements a type annotated with
@Contract
) are bound and added to HK2 - The
INITIALIZATION_START
event is fired for allApplicationEventListener
s registered with Jersey- At this point, HK2 should be fully configured and ready to inject anything requested of it. This is the proper place to initialize custom Jersey components
- Jersey begins loading component providers (
ExceptionMapper
,MessageBodyReader
, etc.) and resources to build up theResourceModel
ModelProcessor
components are run on the model during this step
- The
INITIALIZATION_APP_FINISHED
event is fired for all application event listeners registered with Jersey- Dropwizard prints out all the configured resources
- Jersey completes any other remaining startup tasks
- The
INITIALIZATION_FINISHED
event is fired for all application event listeners registered with Jersey - Jetty starts the
ContextHandler
for the Admin and Jersey environments- Dropwizard will print a warning if there are no health checks when the admin context handler starts up
- The
lifeCycleStarted(LifeCycle)
event is fired for allLifeCycle.Listener
with an instance oforg.eclipse.jetty.server.Server
as the life cycle
This breaks down into roughly four phases, as marked by the major milestones:
- Bootstrap Initialization
- Application Configuration
- Application Initialization
- Application Ready
Much of the dropwizard documentation and design pushes developers to try and do application initialization in phase 2 (also some bundles, like the dropwizard-guice bundle, are designed to do app init in phase 2). While you can do this, you will find yourself slowly starting to fight with Jersey, running into invisible walls between clashing injection frameworks, and discovering that many of Jersey’s features don’t work when components are initialized outside of Jersey.
Case Study: Injectable Tasks
Often a Task will want to access to other services, such as your application’s DAO or other persistence layer to allow for manual
manipulation such as clearing of caches. However, if your DAO wants to be injected with its own services, Dropwizard’s documented approach
necessitates manually instantiating a large portion of your application’s components in the run()
method. While certainly doable, this
makes it difficult to integrate the same components with Jersey. Dropwizard supports registration of tasks at any point, so instead of
trying to create all of our tasks in step 7 before HK2 is ready, we simply need to delay until HK2 is ready, and then we can inject and
register our tasks. (Note that the following code samples are for illustrative purposes; If you want to use injectable tasks,
managed objects, heathchecks, and metrics, take a look at the dropwizard-hk2 bundle)
First, inside our application’s run()
method we need to bind the AdminEnvironment
into HK2 so that it is accessible:
environment.jersey().register(new AbstractBinder() {
@Override
public void configure() {
bind(environment.admin()).to(AdminEnvironment.class);
}
});
Now we can inject the AdminEnvironment
anywhere and add tasks to it. What we want to do is now load all of the injectable tasks on
startup. We use an ApplicationEventListener
to watch for the INITIALIZATION_START
event (step 14 in the startup list above!) and request
all of the tasks to be created so we can add them to the task servlet:
public static class TaskActivator implements ApplicationEventListener {
private final AdminEnvironment adminEnv;
private final IterableProvider<Task> tasks;
@Inject
public TaskActivator(@NonNull AdminEnvironment adminEnv, @NonNull IterableProvider<Task> tasks) {
this.adminEnv = adminEnv;
this.tasks = tasks;
}
@Override
public void onEvent(ApplicationEvent applicationEvent) {
// Add all the tasks upon application initialization
if (applicationEvent.getType() == Type.INITIALIZATION_START) {
tasks.forEach(adminEnv::addTask);
}
}
@Override
public RequestEventListener onRequest(RequestEvent requestEvent) {
return null; // no request processing
}
}
And don’t forget to register our event listener with Jersey in the run()
method of our application:
environment.jersey().register(TaskActivator.class);
Alright, so now we’re injecting and loading all tasks that HK2 knows about at application startup. All that’s left is to bind the tasks we want in HK2:
environment.jersey().register(new AbstractBinder() {
@Override
public void configure() {
bind(MyCacheClearTask.class).to(Task.class).in(Singleton.class);
}
});
When your app starts up, MyCacheClearTask
will be injected by HK2 and exposed as a task in the admin servlet for use. We can bind
additional tasks too– anything bound to Task
in HK2 will be injected and loaded. This type of delayed initialization also is a great
means of helping to resolve dependency issues when one bundle depends upon a separate bundle - both register their necessary components and
interfaces with HK2, and wait until application startup before initializing anything. This ensures that regardless of the order in which the
bundles have been installed, everything is available when HK2 starts instantiating components.
Let me know what you think of this article on twitter @baharclerode or leave a comment below!