An Example Dropwizard Application with Guice for Dependency Injection
Dependency injection is a tremendously useful design pattern. From my experience the direct benefits of this pattern are almost entirely realized as a reduction in the cost of building and maintaining good unit test coverage. Being able to swap out mocks and real instances or values very easily makes the creation of test suites a breeze in comparison to some of the alternatives. All the other benefits derived over the long time largely arise from having a far more comprehensive and maintainable set of tests, since the development team were able to do more in the time allotted.
There are countless dependency injection frameworks in the Java ecosystem, but here I'll look at Guice. It is lightweight enough to drop into most application frameworks with little friction, and is fairly easy to use. Dropwizard is an opinionated Java web application framework with a few packages where someone has already done the heavy lifting of integrating Guice, such as Dropwizard-Guice. If you've already looked through the options, then laying out the boilerplate for a new Dropwizard application that uses Guice for dependency injection is a matter of outlining just a couple of classes.
As it happens I had to do exactly this recently while working in Scala, one of the many successful younger languages that have emerged to run on the Java Virtual Machine. Since I don't like to let lessons learned slip away with the corrosion of memory I took the time to create an example application: you'll find scala-dropwizard-guice-example at GitHub. It is packaged as a single JAR file with dependencies and can run as a command or a server to echo back the text content in a resource file within the JAR. This is just enough to exercise dependency injection for the Dropwizard configuration and a single service and its tests.
Project Layout
Since Maven is the build tool, the example application has the following layout:
config/ example.yml src/ main/ resources/ content.txt scala/ com/ exratione/ sdgexample/ config/ SdgExampleConfiguration.scala guice/ ProvidesContentTrimLength.scala ScalaServiceWithGuiceBundle.scala SdgExampleModule.scala resource/ ContentDisplayResource.scala service/ ContentService.scala ResourceContentService.scala SdgExampleEnvironmentCommand.scala SdgExampleScalaService.scala test/ resources/ content.txt scala/ com/ exratione/ sdgexample/ mock/ guice/ BaseMockSdgExampleModule.scala service/ ResourceContentServiceTest.scala SdgExampleTest.scala pom.xml
Get the Dependency Versions Right
It is usually the case that things will work fine if only the latest stable versions are specified when filling out project dependencies in the POM file. If using older versions of various platforms, however, such as Dropwizard 6.2, then it is necessary to be more careful. The Dropwizard version used drives which version of Dropwizard-Guice can be used. That in turn requires a specific older version of Guice. Further compatibility issues with some versions of Guava are likely to show up.
Since Guice is a dependency injection framework many types of version mismatch error will only show up at runtime. On occasion it might be necessary to dig back through the public repositories for a project to inspect the POM files and find out what the dependency version should be, and then check to see whether it is accidentally being overridden.
For Dropwizard 6.2, the POM file ended up looking like this, using the Maven Shade Plugin to assemble the application code and all dependencies into a single JAR file:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd" > <modelVersion>4.0.0</modelVersion> <groupId>com.exratione</groupId> <artifactId>sdgexample</artifactId> <version>0.6.2</version> <packaging>jar</packaging> <name>scala-dropwizard-guice-example</name> <properties> <dropwizard.guice.version>0.6.2</dropwizard.guice.version> <dropwizard.version>0.6.2</dropwizard.version> <guava.version>15.0</guava.version> <jackson.version>2.4.5</jackson.version> <junit.version>4.8.1</junit.version> <mavanagaiata.plugin.version>0.7.0</mavanagaiata.plugin.version> <maven.build.timestamp.format>yyyy-MM-dd'T'HH:mm:ss'Z'</maven.build.timestamp.format> <maven.enforcer.plugin.version>1.4</maven.enforcer.plugin.version> <maven.jar.plugin.version>2.5</maven.jar.plugin.version> <maven.resources.plugin.version>2.5</maven.resources.plugin.version> <maven.shade.plugin.version>2.3</maven.shade.plugin.version> <maven.surefire.plugin.version>2.18.1</maven.surefire.plugin.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <scala.binary.version>2.10</scala.binary.version> <scala.collection.version>1.5</scala.collection.version> <scala.guice.version>3.0.2</scala.guice.version> <scala.maven.plugin.version>3.2.0</scala.maven.plugin.version> <scala.lib.version>2.10.3</scala.lib.version> <scala.dropwizard.version>0.6.2-1</scala.dropwizard.version> <scalamock.version>3.2.1</scalamock.version> <scalatest.version>2.2.3</scalatest.version> </properties> <dependencies> <!-- Scala --> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${scala.lib.version}</version> </dependency> <!-- Convert Java collections to Scala collections. --> <dependency> <groupId>org.scalaj</groupId> <artifactId>scalaj-collection_${scala.binary.version}</artifactId> <version>${scala.collection.version}</version> </dependency> <!-- Guice extensions for Scala --> <dependency> <groupId>net.codingwell</groupId> <artifactId>scala-guice_${scala.binary.version}</artifactId> <version>${scala.guice.version}</version> </dependency> <!-- It is important to explicitly control the version of this dependency, as by default it will pull in a later version that has issues in this particular setup. --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${guava.version}</version> </dependency> <!-- Dropwizard. --> <dependency> <groupId>com.yammer.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>com.massrelevance</groupId> <artifactId>dropwizard-scala_${scala.binary.version}</artifactId> <version>${scala.dropwizard.version}</version> </dependency> <!-- This will pull in an appropriate version of Guice. --> <dependency> <groupId>com.hubspot.dropwizard</groupId> <artifactId>dropwizard-guice</artifactId> <version>${dropwizard.guice.version}</version> </dependency> <!-- Ensure that we're using a specified version of Jackson rather that what will turn up via the dependencies. This is necessary. --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-joda</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.jaxrs</groupId> <artifactId>jackson-jaxrs-json-provider</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-yaml</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-guava</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-scala_${scala.binary.version}</artifactId> <version>${jackson.version}</version> </dependency> <!-- Test dependencies --> <dependency> <groupId>org.scalatest</groupId> <artifactId>scalatest_2.10</artifactId> <version>${scalatest.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.scalamock</groupId> <artifactId>scalamock-scalatest-support_2.10</artifactId> <version>${scalamock.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <defaultGoal>install</defaultGoal> <directory>${basedir}/target</directory> <sourceDirectory>src/main</sourceDirectory> <outputDirectory>${basedir}/target/classes</outputDirectory> <testSourceDirectory>src/test</testSourceDirectory> <testOutputDirectory>${basedir}/target/test-classes</testOutputDirectory> <finalName>${project.artifactId}</finalName> <resources> <resource> <directory>src/main/scala</directory> <excludes> <exclude>**/*.scala</exclude> </excludes> </resource> <resource> <targetPath>resources</targetPath> <directory>src/main/resources</directory> </resource> </resources> <testResources> <testResource> <directory>src/test/scala</directory> <excludes> <exclude>**/*.scala</exclude> </excludes> </testResource> <testResource> <targetPath>resources</targetPath> <directory>src/test/resources</directory> </testResource> </testResources> <!-- Plugins defined to execute in the same phase execute in the order they are entered below. --> <plugins> <!-- ====================================================================== Phase: validate ====================================================================== --> <!-- Enforce a minimum Maven and Java version. --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <version>${maven.enforcer.plugin.version}</version> <executions> <execution> <id>enforce-versions</id> <phase>validate</phase> <goals> <goal>enforce</goal> </goals> <configuration> <rules> <requireMavenVersion> <!-- Maven 3.0.3 or later. That's the earliest version to enforce declaration ordering for plugins in the same phase. --> <version>[3.0.3,)</version> </requireMavenVersion> <requireJavaVersion> <!-- Java 1.7 or later. --> <version>[1.7,)</version> </requireJavaVersion> </rules> </configuration> </execution> </executions> </plugin> <!-- Obtaining git information. Here the commit sha will be available via the mvngit.commit.id property. --> <plugin> <groupId>com.github.koraktor</groupId> <artifactId>mavanagaiata</artifactId> <version>${mavanagaiata.plugin.version}</version> <executions> <execution> <id>git-commit</id> <phase>validate</phase> <goals> <goal>commit</goal> </goals> </execution> </executions> </plugin> <!-- ====================================================================== Phase: process-resources ====================================================================== --> <!-- For Scala/Java cross-compilation. See: http://davidb.github.com/scala-maven-plugin --> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>${scala.maven.plugin.version}</version> <executions> <execution> <id>scala-compile</id> <phase>process-resources</phase> <goals> <goal>compile</goal> </goals> <configuration> <args> <arg>-make:transitive</arg> <arg>-dependencyfile</arg> <arg>${project.build.directory}/.scala_dependencies</arg> </args> </configuration> </execution> <execution> <id>scala-test-compile</id> <phase>process-test-resources</phase> <goals> <goal>testCompile</goal> </goals> <configuration> <args> <arg>-make:transitive</arg> <arg>-dependencyfile</arg> <arg>${project.build.directory}/.scala_dependencies</arg> </args> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>${maven.resources.plugin.version}</version> </plugin> <!-- ====================================================================== Phase: test ====================================================================== --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>${maven.surefire.plugin.version}</version> <configuration> <includes> <include>**/*Test.class</include> </includes> </configuration> </plugin> <!-- ====================================================================== Phase: package ====================================================================== --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>${maven.jar.plugin.version}</version> <configuration> <archive> <manifest> <addDefaultImplementationEntries>true</addDefaultImplementationEntries> </manifest> <!-- Put some useful entries into the manifest file for the JAR. --> <manifestEntries> <buildTime>${maven.build.timestamp}</buildTime> <commitId>${mvngit.commit.id}</commitId> </manifestEntries> </archive> </configuration> <executions> <execution> <phase>package</phase> </execution> </executions> </plugin> <!-- The shade plugin adds all the dependencies to the output JAR. Note that for large projects, packaging into a single JAR file can produce a file that cannot be read in Java 1.7. See: http://stackoverflow.com/questions/13021423/invalid-or-corrupt-jar-file-built-by-maven-shade-plugin This will report the JAR file as corrupt: java -jar <path/to/jar> server <path/to/config> This will still work, however: java -cp <path/to/jar> <main.class.name> server <path/to/config> --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>${maven.shade.plugin.version}</version> <configuration> <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> <finalName>${project.artifactId}</finalName> <!-- By default this ends up in the project directory, not the build directory. --> <dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" /> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.exratione.sdgexample.SdgExampleScalaService</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
Setting Up Dropwizard-Guice
Integrating Dropwizard with Guice is simple enough: just add a suitably configured GuiceBundle instance to the environment, which here is done in SdgExampleApplication.scala. The GuiceBundle code then automatically adds resources in the package to the Dropwizard Environment, and makes the Configuration and Environment instances available via an injector that can be obtained from the bundle instance.
package com.exratione.sdgexample import com.exratione.sdgexample.config._ import com.exratione.sdgexample.guice._ import com.exratione.sdgexample.service._ import com.hubspot.dropwizard.guice.GuiceBundle import com.yammer.dropwizard.bundles.ScalaBundle import com.yammer.dropwizard.config.{Bootstrap, Configuration, Environment} import com.yammer.dropwizard.ScalaService import net.codingwell.scalaguice.InjectorExtensions._ object SdgExampleScalaService extends ScalaServiceWithGuiceBundle { def initialize (bootstrap: Bootstrap[SdgExampleConfiguration]) { // Create the bundle for dropwizard-guice integration. guiceBundle = GuiceBundle.newBuilder[SdgExampleConfiguration] .addModule(new SdgExampleModule) // This ensures that Resource implementations in this package are set up // automatically. .enableAutoConfig(getClass.getPackage.getName) // The configuration class will be available via the injector obtained via // guiceBundle.getInjector. .setConfigClass(classOf[SdgExampleConfiguration]) .build bootstrap.setName("scala-dropwizard-guice-example") bootstrap.addBundle(new ScalaBundle) bootstrap.addBundle(guiceBundle) bootstrap.addCommand(new SdgExampleEnvironmentCommand(this)) } def run (configuration: SdgExampleConfiguration, environment: Environment) { // Nothing needs to be done here since the GuiceBundle wires up the // Resources automatically. // If obtaining instances here to start processes or take other actions // then use the injector. E.g.: // // val injector = guiceBundle.getInjector // val contentService = injector.instance[ContentService] // // Though we are passed the configuration and environment as arguments, // dropwizard-guice lets us get these instances from the injector as well. // This can be useful elsewhere in an application. E.g.: // // val config = injector.instance[Configuration] // val env = injector.instance[Environment] } }
Note that in order to make the GuiceBundle - and thus its injector - available to the EnvironmentCommand, I created the abstract class ScalaServiceWithGuiceBundle to hold the GuiceBundle reference:
package com.exratione.sdgexample.guice import com.exratione.sdgexample.config._ import com.hubspot.dropwizard.guice.GuiceBundle import com.yammer.dropwizard.ScalaService /** * This class is needed so that the commands can still access the GuiceBundle * instance. */ abstract class ScalaServiceWithGuiceBundle extends ScalaService[SdgExampleConfiguration] { var guiceBundle: GuiceBundle[SdgExampleConfiguration] = null }
package com.exratione.sdgexample import com.exratione.sdgexample.config._ import com.exratione.sdgexample.guice._ import com.exratione.sdgexample.service._ import com.hubspot.dropwizard.guice.GuiceBundle import com.yammer.dropwizard.cli.EnvironmentCommand import com.yammer.dropwizard.config.Environment import com.yammer.dropwizard.ScalaService import net.codingwell.scalaguice.InjectorExtensions._ import net.sourceforge.argparse4j.inf.Namespace import org.slf4j.LoggerFactory class SdgExampleEnvironmentCommand ( application: ScalaService[SdgExampleConfiguration] ) extends EnvironmentCommand[SdgExampleConfiguration] ( application, "sdg", "An example to show how to use dropwizard-guice." ) { private val logger = LoggerFactory.getLogger(getClass) private val guiceBundle = application .asInstanceOf[ScalaServiceWithGuiceBundle] .guiceBundle override def run ( environment: Environment, namespace: Namespace, configuration: SdgExampleConfiguration ): Unit = { val injector = guiceBundle.getInjector val contentService = injector.instance[ContentService] // Though we are passed the configuration and environment, dropwizard-guice // lets us get these instances from the injector as well. This can be // useful elsewhere in an application. E.g.: // // val config = injector.instance[Configuration] // val env = injector.instance[Environment] // Print out the content - that is the sole purpose of this example command. logger.info(s"""Content: ${contentService.getContent}""") } }
Set Up Injected Dependencies
Aside from the above, the rest of it is all straightforward use of Guice: annotate constructors with @Inject; set up an AbstractModule implementation to define the provided dependencies; build unit tests that receive their own mix of mock and real dependencies; and so forth. This is much the same in Scala as in Java, and you should take a look at the code in the scala-dropwizard-guice-example at GitHub for more details.
The SdgExampleModule class has its @Provide methods broken out into traits for easier mixing and matching in other AbstractModule implementations:
package com.exratione.sdgexample.guice import com.exratione.sdgexample.service._ import com.google.inject.AbstractModule import com.google.inject.Singleton import net.codingwell.scalaguice.ScalaModule /** * This determines the bindings used by an injector built with this module. * See: https://github.com/google/guice/wiki/GettingStarted * * Since @Provides methods can't be overridden it seems like a good idea to * specify them in traits so that they can be reused by a test module if * necessary. * * @see ProvidesContentTrimLength */ class SdgExampleModule extends AbstractModule with ScalaModule with ProvidesContentTrimLength { /** * Define the bindings for injectors built with this module. */ override def configure { // This format uses the extensions provided by ScalaModule. bind[ContentService].to[ResourceContentService].in[Singleton] } }
package com.exratione.sdgexample.guice import com.exratione.sdgexample.config.SdgExampleConfiguration import com.google.inject.name.Named import com.google.inject.Provides /** * Note that @Provides methods cannot be overridden. Putting @Provides methods * into traits means that they can be reused or replaced in test modules if * necessary. */ trait ProvidesContentTrimLength { /** * Not everyone wants to pass the whole configuration to every place only a * part of it is needed. Using a @Provides method is a way of cutting things * down. */ @Provides @Named("contentTrimLength") def contentTrimLength (config: SdgExampleConfiguration): Int = { config.contentTrimLength } }
The single service class that makes use of dependency injection in its constructor:
package com.exratione.sdgexample.service import com.exratione.sdgexample.config.SdgExampleConfiguration import com.google.inject.Inject import com.google.inject.name.Named import java.io.BufferedInputStream import java.util.Scanner import scala.io.Source /** * Load the content from the resource packaged with the JAR. * * The @Inject annotation tells Guice to provide these values when an instance * of this class is requested via the injector. */ class ResourceContentService @Inject() ( // The SdgConfiguration is automatically provided by the GuiceBundle // integration between Dropwizard and Guice. It isn't necessary to specify // a Configuration mapping in SdgExampleModule. config: SdgExampleConfiguration, // We could have just pulled the contentTrimLength from the config instance, // but this illustrates how the @Named @Provide method in the // ProvidesContentTrimLength trait is used to inject the value here. @Named("contentTrimLength") contentTrimLength: Int ) extends ContentService { private var content: String = null override def getContent (): String = { // ... } }
Test Cases With Their Own Injectors
As I said at the opening of this post, unit tests are the real place where dependency injection shines. Using Guice each test suite will probably want its own injector and so its own AbstractModule implementation to define dependencies. Each set of tests will have its own needs in terms of the mix of mocks and real instances and values provided. In this example, I built a base test AbstractModuleImplementation to accept a mock Configuration instance, and an illustrative test case that uses it:
package com.exratione.sdgexample.mock.guice import com.exratione.sdgexample.config.SdgExampleConfiguration import com.google.inject.AbstractModule import com.google.inject.Provides import com.google.inject.Singleton import net.codingwell.scalaguice.ScalaModule /** * A base class for mock modules to define injectors that load a mix of mock and * real instances. * * This allows a mock configuration to be passed in. */ abstract class BaseMockSdgExampleModule ( config: SdgExampleConfiguration ) extends AbstractModule with ScalaModule { /** * Define the various bindings for injectors built with this module. */ override def configure { // No bindings defined. Each test must define a complete set of bindings by // overriding this method. } @Provides @Singleton def configuration: SdgExampleConfiguration = config }
package com.exratione.sdgexample.service import com.exratione.sdgexample.config.SdgExampleConfiguration import com.exratione.sdgexample.guice._ import com.exratione.sdgexample.mock.guice._ import com.exratione.sdgexample.SdgExampleTest import com.exratione.sdgexample.service._ import com.google.inject.AbstractModule import com.google.inject.Guice import com.google.inject.Provides import com.google.inject.Singleton import net.codingwell.scalaguice.InjectorExtensions._ import net.codingwell.scalaguice.ScalaModule class ResourceContentServiceTest extends SdgExampleTest { // ------------------------------------------------------------------------ // Build an injector. // ------------------------------------------------------------------------ // Every test set for a distinct class is probably going to want its own // distinct injector, mixing and matching mocks with real classes. This can // become tedious, but since @Provide methods cannot be overridden there is // usually not much to be gained by trying to build a hierarchy of mocks based // on the actual application configuration. /** * A mock module for use with the injector for this test suite. */ class MockSdgExampleModule (config: SdgExampleConfiguration) extends BaseMockSdgExampleModule (config) with ProvidesContentTrimLength { /** * Define the various bindings for injectors built with this module. */ override def configure { // If there were more classes in this example, this method would define a // mix of bindings to mock and real instances depending on the details of // what was being tested. bind[ContentService].to[ResourceContentService].in[Singleton] } } val mockSdgExampleConfiguration = new SdgExampleConfiguration mockSdgExampleConfiguration.contentEncoding = "UTF-8" mockSdgExampleConfiguration.contentResource = "/resources/content.txt" mockSdgExampleConfiguration.contentTrimLength = 4 val injector = Guice.createInjector(new MockSdgExampleModule( mockSdgExampleConfiguration )) // ------------------------------------------------------------------------ // On with the testing. // ------------------------------------------------------------------------ "ResourceContentServiceTest" should "load the correct content" in { val contentService = injector.instance[ContentService] // A single character removed from "Trim!" by the trim length. assert(contentService.getContent == "Test") } }