Thumbnail: dropwizard

Dropwizard Order of Operations: Endpoint Startup

by on under software-development
7 minute read

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:

  1. 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
  2. Default metrics are created and added to the MetricRegistry
  3. Dropwizard determines which command will be run, such as server or check. We’re focusing on the server command for the rest of this document.
  4. The configuration is deserialized
  5. 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
  6. 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.
  7. 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
  8. All components registered with environment.lifecycle() are added to the Jetty Server instance, and then Jetty begins startup
    • environment.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.
  9. 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.
  10. All HK2 Binder instances (I.e., new AbstractBinder() { ... }) registered with Jersey are added to HK2
  11. All Jersey Features that were registered via environment.jersey().register(Class) are injected and configured
  12. All HK2 Binders that were registered by Features to the context are added to HK2
  13. All Jersey component providers (Anything which extends or implements a type annotated with @Contract) are bound and added to HK2
  14. The INITIALIZATION_START event is fired for all ApplicationEventListeners 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
  15. Jersey begins loading component providers (ExceptionMapper, MessageBodyReader, etc.) and resources to build up the ResourceModel
    • ModelProcessor components are run on the model during this step
  16. The INITIALIZATION_APP_FINISHED event is fired for all application event listeners registered with Jersey
    • Dropwizard prints out all the configured resources
  17. Jersey completes any other remaining startup tasks
  18. The INITIALIZATION_FINISHED event is fired for all application event listeners registered with Jersey
  19. 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
  20. The lifeCycleStarted(LifeCycle) event is fired for all LifeCycle.Listener with an instance of org.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.

java, dropwizard, jersey, hk2
comments powered by Disqus