Dropwizard Core¶
The dropwizard-core
module provides you with everything you’ll need for most of your
applications.
It includes:
- Jetty, a high-performance HTTP server.
- Jersey, a full-featured RESTful web framework.
- Jackson, the best JSON library for the JVM.
- Metrics, an excellent library for application metrics.
- Logback, the successor to Log4j, Java’s most widely-used logging framework.
- Hibernate Validator, the reference implementation of the Java Bean Validation standard.
Dropwizard consists mostly of glue code to automatically connect and configure these components.
Organizing Your Project¶
If you plan on developing a client library for other developers to access your service, we recommend
you separate your projects into three Maven modules: project-api
, project-client
, and
project-application
.
project-api
should contain your Representations; project-client
should use
those classes and an HTTP client to implement a full-fledged client for your
application, and project-application
should provide the actual application implementation, including
Resources.
To give a concrete example of this project structure, let’s say we wanted to create a Stripe-like
API where clients can issue charges and the server would echo the charge back to the client.
stripe-api
project would hold our Charge
object as both the server and client want to work
with the charge and to promote code reuse, Charge
objects are stored in a shared module.
stripe-app
is the Dropwizard application. stripe-client
abstracts away the raw HTTP
interactions and deserialization logic. Instead of using a HTTP client, users of stripe-client
would just pass in a Charge
object to a function and behind the scenes, stripe-client
will
call the HTTP endpoint. The client library may also take care of connection pooling, and may
provide a more friendly way of interpreting error messages. Basically, distributing a client library
for your app will help other developers integrate more quickly with the service.
If you are not planning on distributing a client library for developers, one
can combine project-api
and project-application
into a single project,
which tends to look like this:
com.example.myapplication
:api
: Representations. Request and response bodies.cli
: Commandsclient
: Client code that accesses external HTTP services.core
: Domain implementation; where objects not used in the API such as POJOs, validations, crypto, etc, reside.db
: Database access classeshealth
: Health Checksresources
: ResourcesMyApplication
: The application classMyApplicationConfiguration
: configuration class
Application¶
The main entry point into a Dropwizard application is, unsurprisingly, the Application
class. Each
Application
has a name, which is mostly used to render the command-line interface. In the
constructor of your Application
you can add Bundles and Commands to
your application.
Configuration¶
Dropwizard provides a number of built-in configuration parameters. They are well documented in the example project’s configuration and configuration reference.
Each Application
subclass has a single type parameter: that of its matching Configuration
subclass. These are usually at the root of your application’s main package. For example, your User
application would have two classes: UserApplicationConfiguration
, extending Configuration
, and
UserApplication
, extending Application<UserApplicationConfiguration>
.
When your application runs Configured Commands like the server
command, Dropwizard
parses the provided YAML configuration file and builds an instance of your application’s configuration
class by mapping YAML field names to object field names.
Note
If your configuration file doesn’t end in .yml
or .yaml
, Dropwizard tries to parse it
as a JSON file.
To keep your configuration file and class manageable, we recommend grouping related
configuration parameters into independent configuration classes. If your application requires a set of
configuration parameters in order to connect to a message queue, for example, we recommend that you
create a new MessageQueueFactory
class:
public final class MessageQueueFactory {
@NotEmpty
private String host;
@Min(1)
@Max(65535)
private int port = 5672;
@JsonProperty
public String getHost() {
return host;
}
@JsonProperty
public void setHost(String host) {
this.host = host;
}
@JsonProperty
public int getPort() {
return port;
}
@JsonProperty
public void setPort(int port) {
this.port = port;
}
public MessageQueueClient build(Environment environment) {
MessageQueueClient client = new MessageQueueClient(getHost(), getPort());
environment.lifecycle().manage(new Managed() {
@Override
public void start() {
}
@Override
public void stop() {
client.close();
}
});
return client;
}
}
In this example our factory will automatically tie our MessageQueueClient
connection to the
lifecycle of our application’s Environment
.
Your main Configuration
subclass can then include this as a member field:
public class ExampleConfiguration extends Configuration {
@Valid
@NotNull
private MessageQueueFactory messageQueue = new MessageQueueFactory();
@JsonProperty("messageQueue")
public MessageQueueFactory getMessageQueueFactory() {
return messageQueue;
}
@JsonProperty("messageQueue")
public void setMessageQueueFactory(MessageQueueFactory factory) {
this.messageQueue = factory;
}
}
And your Application
subclass can then use your factory to directly construct a client for the
message queue:
public void run(ExampleConfiguration configuration,
Environment environment) {
MessageQueueClient messageQueue = configuration.getMessageQueueFactory().build(environment);
}
Then, in your application’s YAML file, you can use a nested messageQueue
field:
messageQueue:
host: mq.example.com
port: 5673
The @NotNull
, @NotEmpty
, @Min
, @Max
, and @Valid
annotations are part of
Dropwizard Validation functionality. If your YAML configuration file’s
messageQueue.host
field was missing (or was a blank string), Dropwizard would refuse to start
and would output an error message describing the issues.
Once your application has parsed the YAML file and constructed its Configuration
instance,
Dropwizard then calls your Application
subclass to initialize your application’s Environment
.
Note
You can override configuration settings by passing special Java system properties when starting
your application. Overrides must start with prefix dw.
, followed by the path to the
configuration value being overridden.
For example, to override the Logging level, you could start your application like this:
java -Ddw.logging.level=DEBUG server my-config.json
This will work even if the configuration setting in question does not exist in your config file, in which case it will get added.
You can override configuration settings in arrays of objects like this:
java -Ddw.server.applicationConnectors[0].port=9090 server my-config.json
You can override configuration settings in maps like this:
java -Ddw.database.properties.hibernate.hbm2ddl.auto=none server my-config.json
If you need to use the ‘.’ character in one of the values, you can escape it by using ‘.’ instead.
You can also override a configuration setting that is an array of strings by using the ‘,’ character
as an array element separator. For example, to override a configuration setting myapp.myserver.hosts
that is an array of strings in the configuration, you could start your service like this:
java -Ddw.myapp.myserver.hosts=server1,server2,server3 server my-config.json
If you need to use the ‘,’ character in one of the values, you can escape it by using ‘\,’ instead.
The array override facility only handles configuration elements that are arrays of simple strings. Also, the setting in question must already exist in your configuration file as an array; this mechanism will not work if the configuration key being overridden does not exist in your configuration file. If it does not exist or is not an array setting, it will get added as a simple string setting, including the ‘,’ characters as part of the string.
Environment variables¶
The dropwizard-configuration
module also provides the capabilities to substitute configuration settings with the
value of environment variables using a SubstitutingSourceProvider
and EnvironmentVariableSubstitutor
.
public void initialize(Bootstrap<ExampleConfiguration> bootstrap) {
// Enable variable substitution with environment variables
EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(false);
SubstitutingSourceProvider provider =
new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), substitutor);
bootstrap.setConfigurationSourceProvider(provider);
}
The configuration settings which should be substituted need to be explicitly written in the configuration file and follow the substitution rules of StringSubstitutor from the Apache Commons Text library.
mySetting: ${DW_MY_SETTING}
defaultSetting: ${DW_DEFAULT_SETTING:-default value}
In general SubstitutingSourceProvider
isn’t restricted to substitute environment variables but can be used to replace
variables in the configuration source with arbitrary values by passing a custom StringSubstitutor
implementation.
SSL¶
SSL support is built into Dropwizard. You will need to provide your own java
keystore, which is outside the scope of this document (keytool
is the
command you need, and Jetty’s documentation can get you started). There is a
test keystore you can use in the Dropwizard example project.
---
server:
applicationConnectors:
- type: https
port: 8443
keyStorePath: example.keystore
keyStorePassword: example
validateCerts: false
By default, only secure TLSv1.2 cipher suites are allowed. Older versions of cURL, Java 6 and 7, and other clients may be unable to communicate with the allowed cipher suites, but this was a conscious decision that sacrifices interoperability for security.
Dropwizard allows a workaround by specifying a customized list of cipher suites. If no lists of supported protocols or cipher suites are specified, then the JVM defaults are used. If no lists of excluded protocols or cipher suites are specified, then the defaults are inherited from Jetty.
The following list of excluded cipher suites will allow for TLSv1 and TLSv1.1 clients to negotiate a connection similar to pre-Dropwizard 1.0.
---
server:
applicationConnectors:
- type: https
port: 8443
excludedCipherSuites:
- SSL_RSA_WITH_DES_CBC_SHA
- SSL_DHE_RSA_WITH_DES_CBC_SHA
- SSL_DHE_DSS_WITH_DES_CBC_SHA
- SSL_RSA_EXPORT_WITH_RC4_40_MD5
- SSL_RSA_EXPORT_WITH_DES40_CBC_SHA
- SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
- SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA
Since the version 9.4.8 (Dropwizard 1.2.3) Jetty supports native SSL via Google’s Conscrypt which uses BoringSSL (Google’s fork of OpenSSL) for handling cryptography. You can enable it in Dropwizard by registering the provider in your app:
<dependency>
<groupId>org.conscrypt</groupId>
<artifactId>conscrypt-openjdk-uber</artifactId>
<version>${conscrypt.version}</version>
</dependency>
static {
Security.insertProviderAt(new OpenSSLProvider(), 1);
}
and setting the JCE provider in the configuration:
---
server:
type: simple
connector:
type: https
jceProvider: Conscrypt
For HTTP/2 servers you need to add an ALPN Conscrypt provider as a dependency.
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-conscrypt-server</artifactId>
</dependency>
Note
If you are using Conscrypt with Java 8, you must exclude TLSv1.3 protocol as it is now enabled per default with Conscrypt 2.0.0 but not supported by Java 8.
Bootstrapping¶
Before a Dropwizard application can provide the command-line interface, parse a configuration file, or
run as a server, it must first go through a bootstrapping phase. This phase corresponds to your
Application
subclass’s initialize
method. You can add Bundles,
Commands, or register Jackson modules to allow you to include custom types as part
of your configuration class.
Environments¶
A Dropwizard Environment
consists of all the Resources, servlets, filters,
Health Checks, Health, Jersey providers, Managed Objects, Tasks, and
Jersey properties which your application provides.
Each Application
subclass implements a run
method. This is where you should be creating new
resource instances, etc., and adding them to the given Environment
class:
@Override
public void run(ExampleConfiguration config,
Environment environment) {
// encapsulate complicated setup logic in factories
final Thingy thingy = config.getThingyFactory().build();
environment.jersey().register(new ThingyResource(thingy));
environment.healthChecks().register("thingy", new ThingyHealthCheck(thingy));
}
It’s important to keep the run
method clean, so if creating an instance of something is
complicated, like the Thingy
class above, extract that logic into a factory.
Health Checks¶
A health check is a runtime test which you can use to verify your application’s behavior in its production environment. For example, you may want to ensure that your database client is connected to the database:
public class DatabaseHealthCheck extends HealthCheck {
private final Database database;
public DatabaseHealthCheck(Database database) {
this.database = database;
}
@Override
protected Result check() throws Exception {
if (database.isConnected()) {
return Result.healthy();
} else {
return Result.unhealthy("Cannot connect to " + database.getUrl());
}
}
}
You can then add this health check to your application’s environment:
environment.healthChecks().register("database", new DatabaseHealthCheck(database));
By sending a GET
request to /healthcheck
on the admin port you can run these tests and view
the results:
$ curl http://dw.example.com:8081/healthcheck
{"deadlocks":{"healthy":true},"database":{"healthy":true}}
If all health checks report success, a 200 OK
is returned. If any fail, a
500 Internal Server Error
is returned with the error messages and exception stack traces (if an
exception was thrown).
Note
This behavior overlaps in many ways with the new Health functionality. If you wish to disable the admin health servlet, a new flag was introduced into the health check configuration Health checks to allow disabling it.
All Dropwizard applications ship with the deadlocks
health check installed by default, which uses
Java 1.6’s built-in thread deadlock detection to determine if any threads are deadlocked.
Health¶
The health checks described in Health Checks can be configured to create a holistic view of your service health, which can then be used to drive decision making by things like Kubernetes readiness & liveness checks, or to dictate whether or not a load balancer should forward traffic to your service.
This can be done by running these dependency health checks periodically in the background on some schedule, and then aggregating the results of all of those checks into a single indicator of overall health. Certain dependencies may be critical to your application functioning, like a database that your service can’t function without, but other dependencies may be more non-critical to your service being able to function (let’s say a cache, that could be considered more of a nice to have than a necessity).
Define the following health check configurations in your config.yml file:
health:
delayedShutdownHandlerEnabled: true
shutdownWaitPeriod: 10s
healthChecks:
- name: user-database
critical: true
- name: user-notifications-queue
critical: false
schedule:
checkInterval: 2500ms
downtimeInterval: 10s
failureAttempts: 2
successAttempts: 1
- name: user-cache
critical: false
Note
This behavior was integrated from the Dropwizard Health module. If you are migrating from that module to the new Dropwizard core framework health code, you will want to refer to Migrating from dropwizard-health the migration guide.
Application Status¶
There are two types of status that are supported currently: Alive and Ready.
- An
alive
status indicates the application is operating normally and does not need to be restarted to recover from a stuck state. Long-running applications can eventually reach a broken state and cannot recover except by being restarted (e.g. deadlocked threads). - A
ready
status indicates the application is ready to serve traffic. Applications can temporarily be unable to serve traffic due to a variety of reasons, for example, an application might need to build/compute large caches during startup or can critically depend on an external service.
An example of how you might query the health check, assuming you’re using the default responder/responseProvider settings in configuration:
https://<hostname>:<port>/health-check?type=<type>&name=<name>
- replace
<type>
withready
oralive
; defaults toready
if thetype
parameter is not provided - replace
<name>
with the name of the health check to query. Multiple names can be provided, or no no names. If all checks are desired,name=all
can be specified to retrieve all checks
HTTP & TCP Checks¶
Should your service have any dependencies that it needs to perform health checks against that expose either an HTTP or TCP health check interface,
you can use the HttpHealthCheck
or TcpHealthCheck
classes to do so easily.
You will need to register your health check(s) in your Application
class run()
method.
HTTP
@Override
public void run(final AppConfiguration configuration, final Environment environment) {
...
environment.healthChecks().register("some-http-dependency", new HttpHealthCheck("http://some-http-dependency.com:8080/health-check"));
}
TCP
@Override
public void run(final AppConfiguration configuration, final Environment environment) {
...
environment.healthChecks().register("some-tcp-dependency", new TcpHealthCheck("some-tcp-dependency.com", 443));
}
Health Data Access¶
In the Application.run() method, you can access views of the health state data in two different ways:
Accessing data directly
@Override
public void run(final AppConfiguration configuration, final Environment environment) {
...
Collection<HealthStateView> views = environment.health().healthStateAggregator().healthStateViews();
}
Listening to data changes
@Override
public void run(final AppConfiguration configuration, final Environment environment) {
...
HealthStateListener myListener = new HealthStateListener() {
@Override
public void onStateChanged(String healthCheckName, boolean healthy) {
System.out.println(healthCheckName + "changed state to " + healthy);
}
@Override
public void onHealthyCheck(String healthCheckName) {
System.out.println(healthCheckName + "is healthy! :)");
}
@Override
public void onUnhealthyCheck(String healthCheckName) {
System.out.println(healthCheckName + "is unhealthy! :(");
}
};
environment.health().addHealthStateListener(myListener);
}
Managed Objects¶
Most applications involve objects which need to be started and stopped: thread pools, database
connections, etc. Dropwizard provides the Managed
interface for this. You can either have the class
in question implement the #start()
and/or #stop()
methods, or write a wrapper class which does
so. Adding a Managed
instance to your application’s Environment
ties that object’s lifecycle to
that of the application’s HTTP server. Before the server starts, the #start()
method is called.
After the server has stopped (and after its graceful shutdown period) the #stop()
method is called.
For example, given a theoretical Riak client which needs to be started and stopped:
public class RiakClientManager implements Managed {
private final RiakClient client;
public RiakClientManager(RiakClient client) {
this.client = client;
}
@Override
public void start() throws Exception {
client.start();
}
@Override
public void stop() throws Exception {
client.stop();
}
}
public class ManagedApp extends Application<Configuration> {
@Override
public void run(Configuration configuration, Environment environment) {
RiakClient client = new RiakClient();
RiakClientManager riakClientManager = new RiakClientManager(client);
environment.lifecycle().manage(riakClientManager);
}
}
If RiakClientManager#start()
throws an exception–e.g., an error connecting to the server–your
application will not start and a full exception will be logged. If RiakClientManager#stop()
throws
an exception, the exception will be logged but your application will still be able to shut down.
It should be noted that Environment
has built-in factory methods for ExecutorService
and
ScheduledExecutorService
instances which are managed. These managed instances use InstrumentedThreadFactory
that monitors the number of threads created, running and terminated
public void run(Configuration configuration, Environment environment) {
ExecutorService executorService = environment.lifecycle()
.executorService(nameFormat)
.maxThreads(maxThreads)
.build();
ScheduledExecutorService scheduledExecutorService = environment.lifecycle()
.scheduledExecutorService(nameFormat)
.build();
}
Bundles¶
A Dropwizard bundle is a reusable group of functionality, used to define blocks of an application’s
behavior by implementing the ConfiguredBundle
interface.
For example, AssetBundle
from the dropwizard-assets
module provides a simple way
to serve static assets from your application’s src/main/resources/assets
directory as files
available from /assets/*
(or any other path) in your application.
Given the bundle MyConfiguredBundle
and the interface MyConfiguredBundleConfig
below,
your application’s Configuration
subclass would need to implement MyConfiguredBundleConfig
.
public class MyConfiguredBundle implements ConfiguredBundle<MyConfiguredBundleConfig> {
@Override
public void run(MyConfiguredBundleConfig applicationConfig, Environment environment) {
applicationConfig.getBundleSpecificConfig();
}
@Override
public void initialize(Bootstrap<?> bootstrap) {
}
}
public interface MyConfiguredBundleConfig {
String getBundleSpecificConfig();
}
Serving Assets¶
Either your application or your static assets can be served from the root path, but not both. The latter is useful when using Dropwizard to back a Javascript application. To enable it, move your application to a sub-URL.
server:
rootPath: /api/
Note
If you use the Simple server configuration, then rootPath
is calculated relatively from
applicationContextPath
. So, your API will be accessible from the path /application/api/
Then use an extended AssetsBundle
constructor to serve resources in the
assets
folder from the root path. index.htm
is served as the default
page.
bootstrap.addBundle(new AssetsBundle("/assets/", "/"));
When an AssetBundle
is added to the application, it is registered as a servlet
using a default name of assets
. If the application needs to have multiple AssetBundle
instances, the extended constructor should be used to specify a unique name for the AssetBundle
.
bootstrap.addBundle(new AssetsBundle("/assets/css", "/css", null, "css"));
bootstrap.addBundle(new AssetsBundle("/assets/js", "/js", null, "js"));
bootstrap.addBundle(new AssetsBundle("/assets/fonts", "/fonts", null, "fonts"));
SSL Reload¶
By registering the SslReloadBundle
your application can have new certificate information
reloaded at runtime, so a restart is not necessary.
public void initialize(Bootstrap<Configuration> bootstrap) {
bootstrap.addBundle(new SslReloadBundle());
}
To trigger a reload send a POST
request to ssl-reload
curl -k -X POST 'https://localhost:<admin-port>/tasks/ssl-reload'
Dropwizard will use the same exact https configuration (keystore location, password, etc) when performing the reload.
Note
If anything is wrong with the new certificate (eg. wrong password in keystore), no new certificates are loaded. So if the application and admin ports use different certificates and one of them is invalid, then none of them are reloaded.
A http 500 error is returned on reload failure, so make sure to trap for this error with whatever tool is used to trigger a certificate reload, and alert the appropriate admin. If the situation is not remedied, next time the app is stopped, it will be unable to start!
Commands¶
Commands are basic actions which Dropwizard runs based on the arguments provided on the command
line. The built-in server
command, for example, spins up an HTTP server and runs your application.
Each Command
subclass has a name and a set of command line options which Dropwizard will use to
parse the given command line arguments.
Below is an example on how to add a command and have Dropwizard recognize it.
public class MyCommand extends Command {
public MyCommand() {
// The name of our command is "hello" and the description printed is
// "Prints a greeting"
super("hello", "Prints a greeting");
}
@Override
public void configure(Subparser subparser) {
// Add a command line option
subparser.addArgument("-u", "--user")
.dest("user")
.type(String.class)
.required(true)
.help("The user of the program");
}
@Override
public void run(Bootstrap<?> bootstrap, Namespace namespace) throws Exception {
System.out.println("Hello " + namespace.getString("user"));
}
}
Dropwizard recognizes our command once we add it in the initialize
stage of our application.
public class CustomCommandApp extends Application<Configuration> {
@Override
public void initialize(Bootstrap<Configuration> bootstrap) {
bootstrap.addCommand(new MyCommand());
}
@Override
public void run(Configuration configuration, Environment environment) {
}
}
To invoke the new functionality, run the following:
java -jar <jarfile> hello dropwizard
Configured Commands¶
Some commands require access to configuration parameters and should extend the ConfiguredCommand
class, using your application’s Configuration
class as its type parameter. By default,
Dropwizard will treat the last argument on the command line as the path to a YAML configuration
file, parse and validate it, and provide your command with an instance of the configuration class.
A ConfiguredCommand
can have additional command line options specified, while keeping the last
argument the path to the YAML configuration.
public class MyConfiguredCommand extends ConfiguredCommand<Configuration> {
public MyConfiguredCommand() {
// The name of our command is "hello" and the description printed is
// "Prints a greeting"
super("hello", "Prints a greeting");
}
@Override
public void configure(Subparser subparser) {
// Add a command line option
subparser.addArgument("-u", "--user")
.dest("user")
.type(String.class)
.required(true)
.help("The user of the program");
}
@Override
protected void run(Bootstrap<Configuration> bootstrap,
Namespace namespace,
Configuration configuration) throws Exception {
System.out.println("Hello " + namespace.getString("user"));
}
}
For more advanced customization of the command line (for example, having the configuration file
location specified by -c
), adapt the ConfiguredCommand class as needed.
Note
If you override the configure
method, you must call super.override(subparser)
(or call addFileArgument
)
in order to preserve the configuration file parameter in the subparser.
Tasks¶
A Task
is a run-time action your application provides access to on the administrative port via HTTP.
All Dropwizard applications start with: the gc
task, which explicitly triggers the JVM’s garbage
collection (This is useful, for example, for running full garbage collections during off-peak times
or while the given application is out of rotation.); and the log-level
task, which configures the level
of any number of loggers at runtime (akin to Logback’s JmxConfigurator
). The execute method of a Task
can be annotated with @Timed
, @Metered
, and @ExceptionMetered
. Dropwizard will automatically
record runtime information about your tasks. Here’s a basic task class:
public class TruncateDatabaseTask extends Task {
private final Database database;
public TruncateDatabaseTask(Database database) {
super("truncate");
this.database = database;
}
@Override
public void execute(Map<String, List<String>> parameters, PrintWriter output) throws Exception {
this.database.truncate();
}
}
You can then add this task to your application’s environment:
public void run(Configuration configuration, Environment environment) {
Database database = new Database();
environment.admin().addTask(new TruncateDatabaseTask(database));
}
Running a task can be done by sending a POST
request to /tasks/{task-name}
on the admin
port. The task will receive any query parameters as arguments. For example:
$ curl -X POST http://dw.example.com:8081/tasks/gc
Running GC...
Done!
You can also extend PostBodyTask
to create a task which uses the body of the post request. Here’s an example:
public class EchoTask extends PostBodyTask {
public EchoTask() {
super("echo");
}
@Override
public void execute(Map<String, List<String>> parameters, String body, PrintWriter output) throws Exception {
output.write(body);
output.flush();
}
}
Logging¶
Dropwizard uses Logback for its logging backend. It provides an slf4j implementation, and even
routes all java.util.logging
, Log4j, and Apache Commons Logging usage through Logback.
slf4j provides the following logging levels:
ERROR
- Error events that might still allow the application to continue running.
WARN
- Potentially harmful situations.
INFO
- Informational messages that highlight the progress of the application at coarse-grained level.
DEBUG
- Fine-grained informational events that are most useful to debug an application.
TRACE
- Finer-grained informational events than the
DEBUG
level.
Note
If you don’t want to use Logback, you can exclude it from Dropwizard and use an alternative logging configuration:
Exclude Logback from the dropwizard-core artifact
<dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>{$dropwizard.version}</version> <exclusions> <exclusion> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </exclusion> <exclusion> <groupId>ch.qos.logback</groupId> <artifactId>logback-access</artifactId> </exclusion> <exclusion> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> </exclusion> </exclusions> </dependency>
Mark the logging configuration as external in your Dropwizard config
server: type: simple applicationContextPath: /application adminContextPath: /admin requestLog: type: external logging: type: external
Disable bootstrapping Logback in your application
public class ExampleApplication extends Application<ExampleConfiguration> { @Override protected void bootstrapLogging() { } }
Log Format¶
Dropwizard’s log format has a few specific goals:
- Be human readable.
- Be machine parsable.
- Be easy for sleepy ops folks to figure out why things are pear-shaped at 3:30AM using standard
UNIXy tools like
tail
andgrep
.
The logging output looks like this:
TRACE [2010-04-06 06:42:35,271] com.example.dw.Thing: Contemplating doing a thing.
DEBUG [2010-04-06 06:42:35,274] com.example.dw.Thing: About to do a thing.
INFO [2010-04-06 06:42:35,274] com.example.dw.Thing: Doing a thing
WARN [2010-04-06 06:42:35,275] com.example.dw.Thing: Doing a thing
ERROR [2010-04-06 06:42:35,275] com.example.dw.Thing: This may get ugly.
! java.lang.RuntimeException: oh noes!
! at com.example.dw.Thing.run(Thing.java:16)
!
A few items of note:
All timestamps are in UTC and ISO 8601 format.
You can grep for messages of a specific level really easily:
tail -f dw.log | grep '^WARN'
You can grep for messages from a specific class or package really easily:
tail -f dw.log | grep 'com.example.dw.Thing'
You can even pull out full exception stack traces, plus the accompanying log message:
tail -f dw.log | grep -B 1 '^\!'
The ! prefix does not apply to syslog appenders, as stack traces are sent separately from the main message. Instead, t is used (this is the default value of the SyslogAppender that comes with Logback). This can be configured with the stackTracePrefix option when defining your appender.
To apply the prefixing with the ! symbol Dropwizard introduces several new Logback conversion words.
These are dwEx
, dwException
, dwThrowable
, dwXEx
, dwXException
, dwXThrowable
, dwREx
and dwRootException
.
These conversion words work like the ones from Logback, except that the first tab of a stack trace is replaced by a !.
The default Dropwizard logging layout uses the Dropwizard specific conversion words.
Configuration¶
You can specify a default logger level, override the levels of other loggers in your YAML configuration file, and even specify appenders for them. The latter form of configuration is preferable, but the former is also acceptable.
# Logging settings.
logging:
# The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL.
level: INFO
# Logger-specific levels.
loggers:
# Overrides the level of com.example.dw.Thing and sets it to DEBUG.
"com.example.dw.Thing": DEBUG
# Enables the SQL query log and redirect it to a separate file
"org.hibernate.SQL":
level: DEBUG
# This line stops org.hibernate.SQL (or anything under it) from using the root logger
additive: false
appenders:
- type: file
currentLogFilename: ./logs/example-sql.log
archivedLogFilenamePattern: ./logs/example-sql-%d.log.gz
archivedFileCount: 5
Asynchronous Logging¶
By default, all logging in Dropwizard is asynchronous, even to typically
synchronous sinks such as files and the console. When a slow logger (like file
logger on an overloaded disk) is coupled with a high load, Dropwizard will
seamlessly drop events of lower importance (TRACE
, DEBUG
, INFO
) in
an attempt to maintain reasonable latency.
Tip
Instead of logging business critical statements under INFO
, insert them
into a database, durable message queue, or another mechanism that gives
confidence that the request has satisfied business requirements before
returning the response to the client.
This logging behavior can be configured:
- Set
discardingThreshold
to 0 so that no events are dropped - At the opposite end, set
neverBlock
totrue
so that evenWARN
andERROR
levels will be discarded from logging under heavy load
Request access logging has the same logging behavior, and since all request
logging is done under INFO
, each log statement has an equal chance of being
dropped if the log queue is nearing full.
Console Logging¶
By default, Dropwizard applications log INFO
and higher to STDOUT
. You can configure this by
editing the logging
section of your YAML configuration file:
logging:
appenders:
- type: console
threshold: WARN
target: stderr
In the above, we’re instead logging only WARN
and ERROR
messages to the STDERR
device.
File Logging¶
Dropwizard can also log to an automatically rotated set of log files. This is the recommended configuration for your production environment:
logging:
appenders:
- type: file
# The file to which current statements will be logged.
currentLogFilename: ./logs/example.log
# When the log file rotates, the archived log will be renamed to this and gzipped. The
# %d is replaced with the previous day (yyyy-MM-dd). Custom rolling windows can be created
# by passing a SimpleDateFormat-compatible format as an argument: "%d{yyyy-MM-dd-hh}".
archivedLogFilenamePattern: ./logs/example-%d.log.gz
# The number of archived files to keep.
archivedFileCount: 5
# The timezone used to format dates. HINT: USE THE DEFAULT, UTC.
timeZone: UTC
Syslog Logging¶
Finally, Dropwizard can also log statements to syslog.
Note
Because Java doesn’t use the native syslog bindings, your syslog server must have an open network socket.
logging:
appenders:
- type: syslog
# The hostname of the syslog server to which statements will be sent.
# N.B.: If this is the local host, the local syslog instance will need to be configured to
# listen on an inet socket, not just a Unix socket.
host: localhost
# The syslog facility to which statements will be sent.
facility: local0
You can combine any number of different appenders
, including multiple instances of the same
appender with different configurations:
logging:
# Permit DEBUG, INFO, WARN and ERROR messages to be logged by appenders.
level: DEBUG
appenders:
# Log warnings and errors to stderr
- type: console
threshold: WARN
target: stderr
# Log info, warnings and errors to our apps' main log.
# Rolled over daily and retained for 5 days.
- type: file
threshold: INFO
currentLogFilename: ./logs/example.log
archivedLogFilenamePattern: ./logs/example-%d.log.gz
archivedFileCount: 5
# Log debug messages, info, warnings and errors to our apps' debug log.
# Rolled over hourly and retained for 6 hours
- type: file
threshold: DEBUG
currentLogFilename: ./logs/debug.log
archivedLogFilenamePattern: ./logs/debug-%d{yyyy-MM-dd-hh}.log.gz
archivedFileCount: 6
JSON Log Format¶
You may prefer to produce logs in a structured format such as JSON, so it can be processed by analytics or BI software. For that, add a module to the project for supporting JSON layouts:
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-json-logging</artifactId>
<version>${dropwizard.version}</version>
</dependency>
Setup the JSON layout in the configuration file.
For general logging:
logging:
appenders:
- type: console
layout:
type: json
The json
layout will produces the following log message:
{"timestamp":1515002688000, "level":"INFO","logger":"org.eclipse.jetty.server.Server","thread":"main","message":"Started @6505ms"}
For request logging:
server:
requestLog:
appenders:
- type: console
layout:
type: access-json
The access-json
layout will produces the following log message:
{"timestamp":1515002688000, "method":"GET","uri":"/hello-world", "status":200, "protocol":"HTTP/1.1","contentLength":37,"remoteAddress":"127.0.0.1","requestTime":5, "userAgent":"Mozilla/5.0"}
Logging Configuration via HTTP¶
Active log levels can be changed during the runtime of a Dropwizard application via HTTP using
the LogConfigurationTask
. For instance, to configure the log level for a
single Logger
. The logger
parameter may be repeated. The optional duration
parameter must be an ISO 8601 duration format.
When the duration elapses the level will revert to the effective level of the parent logger.:
# Configure com.example.helloworld to INFO
curl -X POST -d "logger=com.example.helloworld&level=INFO" http://localhost:8081/tasks/log-level
# Configure com.example.helloworld and com.example.helloearth to INFO
curl -X POST -d "logger=com.example.helloworld&logger=com.example.helloearth&level=INFO" http://localhost:8081/tasks/log-level
# Configure com.example.helloworld to INFO, then revert to default level after 10 minutes
curl -X POST -d "logger=com.example.helloworld&level=INFO&duration=PT10M" http://localhost:8081/tasks/log-level
# Revert com.example.helloworld to the default level
curl -X POST -d "logger=com.example.helloworld" http://localhost:8081/tasks/log-level
Note
Chaining log level changes on the same package may have unexpected consequences due to the naive implementation of a simple FIFO timer.
Logging Filters¶
Just because a statement has a level of INFO
, doesn’t mean it should be logged with other INFO
statements. One can create logging filters that will intercept log statements before they are written and decide if they’re allowed. Log filters can work on both regular statements and request log statements. The following example will be for request logging as there are many reasons why certain requests may be excluded from the log:
- Only log requests that have large bodies
- Only log requests that are slow
- Only log requests that resulted in a non-2xx status code
- Exclude requests that contain sensitive information in the URL
- Exclude healthcheck requests
The example will demonstrate excluding /secret
requests from the log.
@JsonTypeName("secret-filter-factory")
public class SecretFilterFactory implements FilterFactory<IAccessEvent> {
@Override
public Filter<IAccessEvent> build() {
return new Filter<IAccessEvent>() {
@Override
public FilterReply decide(IAccessEvent event) {
if (event.getRequestURI().equals("/secret")) {
return FilterReply.DENY;
} else {
return FilterReply.NEUTRAL;
}
}
};
}
}
Reference SecretFilterFactory
type in our configuration.
server:
requestLog:
appenders:
- type: console
filterFactories:
- type: secret-filter-factory
The last step is to add our class (in this case com.example.SecretFilterFactory
) to META-INF/services/io.dropwizard.logging.common.filter.FilterFactory
in our resources folder.
Filtering Request Logs for a Specific URI¶
Reference UriFilterFactory
type in your configuration.
server:
requestLog:
appenders:
- type: console
filterFactories:
- type: uri
uris:
- "/health-check"
Testing Applications¶
All of Dropwizard’s APIs are designed with testability in mind, so even your applications can have unit tests:
@ExtendWith(MockitoExtension.class)
class MyApplicationTest {
@Mock
private Environment environment;
@Mock
private JerseyEnvironment jersey;
private MyApplication application;
private MyConfiguration config;
@BeforeEach
void setup() throws Exception {
config = new MyConfiguration();
config.setMyParam("yay");
application = new MyApplication();
when(environment.jersey()).thenReturn(jersey);
}
@Test
void buildsMyResource() throws Exception {
application.run(config, environment);
verify(jersey).register(eq(MyResource.class));
}
}
We highly recommend Mockito for all your mocking needs.
Banners¶
We think applications should print out a big ASCII art banner on startup. Yours should, too. It’s fun.
Just add a banner.txt
class to src/main/resources
and it’ll print it out when your application
starts:
INFO [2011-12-09 21:56:37,209] io.dropwizard.core.cli.ServerCommand: Starting hello-world
dP
88
.d8888b. dP. .dP .d8888b. 88d8b.d8b. 88d888b. 88 .d8888b.
88ooood8 `8bd8' 88' `88 88'`88'`88 88' `88 88 88ooood8
88. ... .d88b. 88. .88 88 88 88 88. .88 88 88. ...
`88888P' dP' `dP `88888P8 dP dP dP 88Y888P' dP `88888P'
88
dP
INFO [2011-12-09 21:56:37,214] org.eclipse.jetty.server.Server: jetty-7.6.0
...
We could probably make up an argument about why this is a serious devops best practice with high ROI and an Agile Tool, but honestly we just enjoy this.
We recommend you use TAAG for all your ASCII art banner needs.
Resources¶
Unsurprisingly, most of your day-to-day work with a Dropwizard application will be in the resource classes, which model the resources exposed in your RESTful API. Dropwizard uses Jersey for this, so most of this section is just re-hashing or collecting various bits of Jersey documentation.
Jersey is a framework for mapping various aspects of incoming HTTP requests to POJOs and then mapping various aspects of POJOs to outgoing HTTP responses. Here’s a basic resource class:
@Path("/{user}/notifications")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class NotificationsResource {
private final NotificationStore store;
public NotificationsResource(NotificationStore store) {
this.store = store;
}
@GET
public NotificationList fetch(@PathParam("user") OptionalLong userId,
@QueryParam("count") @DefaultValue("20") OptionalInt count) {
final List<Notification> notifications = store.fetch(userId.get(), count.get());
if (notifications != null) {
return new NotificationList(userId, notifications);
}
throw new WebApplicationException(Status.NOT_FOUND);
}
@POST
public Response add(@PathParam("user") OptionalLong userId,
@NotNull @Valid Notification notification) {
final long id = store.add(userId.get(), notification);
return Response.created(UriBuilder.fromResource(NotificationResource.class)
.build(userId.get(), id))
.build();
}
}
This class provides a resource (a user’s list of notifications) which responds to GET
and
POST
requests to /{user}/notifications
, providing and consuming application/json
representations. There’s quite a lot of functionality on display here, and this section will
explain in detail what’s in play and how to use these features in your application.
Paths¶
Important
Every resource class must have a @Path
annotation.
The @Path
annotation isn’t just a static string, it’s a URI Template. The {user}
part
denotes a named variable, and when the template matches a URI the value of that variable will be
accessible via @PathParam
-annotated method parameters.
For example, an incoming request for /1001/notifications
would match the URI template, and the
value "1001"
would be available as the path parameter named user
.
If your application doesn’t have a resource class whose @Path
URI template matches the URI of an
incoming request, Jersey will automatically return a 404 Not Found
to the client.
Methods¶
Methods on a resource class which accept incoming requests are annotated with the HTTP methods they
handle: @GET
, @POST
, @PUT
, @DELETE
, @HEAD
, @OPTIONS
, @PATCH
.
Support for arbitrary new methods can be added via the @HttpMethod
annotation. They also must
be added to the list of allowed methods. This means, by default,
methods such as CONNECT
and TRACE
are blocked, and will return a 405 Method Not Allowed
response.
If a request comes in which matches a resource class’s path but has a method which the class doesn’t
support, Jersey will automatically return a 405 Method Not Allowed
to the client.
The return value of the method (in this case, a NotificationList
instance) is then mapped to the
negotiated media type. In this case, our resource only supports
JSON, and so the NotificationList
is serialized to JSON using Jackson.
Metrics¶
Every resource method or the class itself can be annotated with @Timed, @Metered, @ResponseMetered and @ExceptionMetered. If the annotation is placed on the class, it will apply to all its resource methods. Dropwizard augments Jersey to automatically record runtime information about your resource methods.
public class ExampleApplication extends ResourceConfig {
.
.
.
register(new InstrumentedResourceMethodApplicationListener (new MetricRegistry()));
config = config.register(ExampleResource.class);
.
.
.
}
@Path("/example")
@Produces(MediaType.TEXT_PLAIN)
public class ExampleResource {
@GET
@Timed
public String show() {
return "yay";
}
@GET
@Metered(name = "fancyName") // If name isn't specified, the meter will given the name of the method it decorates.
@Path("/metered")
public String metered() {
return "woo";
}
@GET
@ExceptionMetered(cause = IOException.class) // Default cause is Exception.class
@Path("/exception-metered")
public String exceptionMetered(@QueryParam("splode") @DefaultValue("false") boolean splode) throws IOException {
if (splode) {
throw new IOException("AUGH");
}
return "fuh";
}
@GET
@ResponseMetered
@Path("/response-metered")
public Response responseMetered(@QueryParam("invalid") @DefaultValue("false") boolean invalid) {
if (invalid) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
return Response.ok().build();
}
}
@Timed
measures the duration of requests to a resource@Metered
measures the rate at which the resource is accessed@ResponseMetered
measures rate for each class of response codes (1xx/2xx/3xx/4xx/5xx)@ExceptionMetered
measures how often exceptions occur processing the resource
Important
@Timed
and @Metered
can only be used on the same resource method at the same time, if
their name is unique, also see the annotation parameter name
.
Otherwise, the generated metrics names will be identical which will cause an IllegalArgumentException
.
Parameters¶
The annotated methods on a resource class can accept parameters which are mapped to from aspects of the incoming request.
For example:
- A
@PathParam("user")
-annotatedString
takes the raw value from theuser
variable in the matched URI template and passes it into the method as aString
. - A
@QueryParam("count")
-annotatedOptionalInt
parameter takes the firstcount
value from the request’s query string and passes it as aString
toOptionalInt
’s constructor.OptionalInt
parses the string as anInteger
, returning a400 Bad Request
if the value is malformed. - A
@FormParam("name")
-annotatedSet<String>
parameter takes all thename
values from a posted form and passes them to the method as a set of strings. - A
*Param
–annotatedNonEmptyStringParam
will interpret empty strings as absent strings, which is useful in cases where the endpoint treats empty strings and absent strings as interchangeable.
What’s noteworthy here is that you can actually encapsulate the vast majority of your validation
logic using specialized parameter objects. See AbstractParam
for details.
Request Entities¶
If you’re handling request entities (e.g., an application/json
object on a PUT
request), you
can model this as a parameter without a *Param
annotation. In the
example code, the add
method provides a good example of
this:
@POST
public Response add(@PathParam("user") OptionalLong userId,
@NotNull @Valid Notification notification) {
final long id = store.add(userId.get(), notification);
return Response.created(UriBuilder.fromResource(NotificationResource.class)
.build(userId.get(), id))
.build();
}
Jersey maps the request entity to any single, unbound parameter. In this case, because the resource
is annotated with @Consumes(MediaType.APPLICATION_JSON)
, it uses the Dropwizard-provided Jackson
support which, in addition to parsing the JSON and mapping it to an instance of Notification
,
also runs that instance through Dropwizard’s Constraining Entities.
If the deserialized Notification
isn’t valid, Dropwizard returns a 422 Unprocessable Entity
response to the client.
Note
If a request entity parameter is just annotated with @Valid
, it is still allowed to be
null
, so to ensure that the object is present and validated @NotNull @Valid
is a
powerful combination.
Media Types¶
Jersey also provides full content negotiation, so if your resource class consumes
application/json
but the client sends a text/plain
entity, Jersey will automatically reply
with a 406 Not Acceptable
. Jersey’s even smart enough to use client-provided q
-values in
their Accept
headers to pick the best response content type based on what both the client and
server will support.
Responses¶
If your clients are expecting custom headers or additional information (or, if you simply desire an
additional degree of control over your responses), you can return explicitly-built Response
objects:
return Response.noContent().language(Locale.GERMAN).build();
In general, though, we recommend you return actual domain objects if at all possible. It makes testing resources much easier.
Error Handling¶
Almost as important as an application’s happy path (receiving expected input and returning expected output) is an application’s behavior when something goes wrong.
If your resource class unintentionally throws an exception, Dropwizard will log that exception under
the ERROR
level (including stack traces) and return a terse, safe application/json
500
Internal Server Error
response. The response will contain an ID that can be grepped out the server
logs for additional information.
If your resource class needs to return an error to the client (e.g., the requested record doesn’t
exist), you have two options: throw a subclass of Exception
or restructure your method to
return a Response
. If at all possible, prefer throwing Exception
instances to returning
Response
objects, as that will make resource endpoints more self describing and easier to test.
The least intrusive way to map error conditions to a response is to throw a WebApplicationException
:
@GET
@Path("/{collection}")
public Saying reduceCols(@PathParam("collection") String collection) {
if (!collectionMap.containsKey(collection)) {
final String msg = String.format("Collection %s does not exist", collection);
throw new WebApplicationException(msg, Status.NOT_FOUND)
}
// ...
}
In this example a GET
request to /foobar
will return
{"code":404,"message":"Collection foobar does not exist"}
One can also take exceptions that your resource may throw and map them to appropriate responses. For instance,
an endpoint may throw IllegalArgumentException
and it may be worthy enough of a response to warrant a
custom metric to track how often the event occurs. Here’s an example of such an ExceptionMapper
public class IllegalArgumentExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
private final Meter exceptions;
public IllegalArgumentExceptionMapper(MetricRegistry metrics) {
exceptions = metrics.meter(name(getClass(), "exceptions"));
}
@Override
public Response toResponse(IllegalArgumentException e) {
exceptions.mark();
return Response.status(Status.BAD_REQUEST)
.header("X-YOU-SILLY", "true")
.type(MediaType.APPLICATION_JSON_TYPE)
.entity(new ErrorMessage(Status.BAD_REQUEST.getStatusCode(),
"You passed an illegal argument!"))
.build();
}
}
and then registering the exception mapper:
@Override
public void run(final MyConfiguration conf, final Environment env) {
env.jersey().register(new IllegalArgumentExceptionMapper(env.metrics()));
env.jersey().register(new Resource());
}
Overriding Default Exception Mappers¶
To override a specific exception mapper, register your own class that implements the same
ExceptionMapper<T>
as one of the default. For instance, we can customize responses caused by
Jackson exceptions:
public class JsonProcessingExceptionMapper implements ExceptionMapper<JsonProcessingException> {
@Override
public Response toResponse(JsonProcessingException exception) {
// create the response
}
}
With this method, one doesn’t need to know what the default exception mappers are, as they are
overridden if the user supplies a conflicting mapper. While not preferential, one can also disable
all default exception mappers, by setting server.registerDefaultExceptionMappers
to false
.
See the class ExceptionMapperBinder
for a list of the default exception mappers.
URIs¶
While Jersey doesn’t quite have first-class support for hyperlink-driven applications, the provided
UriBuilder
functionality does quite well.
Rather than duplicate resource URIs, it’s possible (and recommended!) to initialize a UriBuilder
with the path from the resource class itself:
UriBuilder.fromResource(UserResource.class).build(user.getId());
Testing¶
As with just about everything in Dropwizard, we recommend you design your resources to be testable.
Dependencies which aren’t request-injected should be passed in via the constructor and assigned to
final
fields.
Testing, then, consists of creating an instance of your resource class and passing it a mock. (Again: Mockito.)
public class NotificationsResourceTest {
private final NotificationStore store = mock(NotificationStore.class);
private final NotificationsResource resource = new NotificationsResource(store);
@Test
public void getsReturnNotifications() {
final List<Notification> notifications = mock(List.class);
when(store.fetch(1, 20)).thenReturn(notifications);
final NotificationList list = resource.fetch(new LongParam("1"), new IntParam("20"));
assertThat(list.getUserId(),
is(1L));
assertThat(list.getNotifications(),
is(notifications));
}
}
Caching¶
Adding a Cache-Control
statement to your resource class is simple with Dropwizard:
@GET
@CacheControl(maxAge = 6, maxAgeUnit = TimeUnit.HOURS)
public String getCachableValue() {
return "yay";
}
The @CacheControl
annotation will take all of the parameters of the Cache-Control
header.
Sessions¶
Although Dropwizard’s main purpose is to build stateless RESTful APIs, a stateful web service can be built using HTTP sessions. As most users won’t profit from having session support enabled by default, session support is implemented as opt-in.
The underlying Jetty server will handle sessions only if a SessionHandler
is provided at
application startup. Therefore the following code has to be added to the run
method of the
Application
class:
@Override
public void run(final TestConfiguration configuration, final Environment environment) {
environment.servlets().setSessionHandler(new org.eclipse.jetty.server.session.SessionHandler());
}
This will provide Jetty’s default SessionHandler
to the servlet environment and session support is enabled.
To get an HttpSession
object injected into a Jersey resource method, Dropwizard provides a @Session
annotation:
public Response doSomethingWithSessions(@Session HttpSession httpSession) {
return Response.ok().build();
}
Representations¶
Representation classes are classes which, when handled to various Jersey MessageBodyReader
and
MessageBodyWriter
providers, become the entities in your application’s API. Dropwizard heavily
favors JSON, but it’s possible to map from any POJO to custom formats and back.
Basic JSON¶
Jackson is awesome at converting regular POJOs to JSON and back. This file:
public class Notification {
private String text;
public Notification(String text) {
this.text = text;
}
@JsonProperty
public String getText() {
return text;
}
@JsonProperty
public void setText(String text) {
this.text = text;
}
}
gets converted into this JSON:
{
"text": "hey it's the value of the text field"
}
If, at some point, you need to change the JSON field name or the Java field without affecting the
other, you can add an explicit field name to the @JsonProperty
annotation.
If you prefer immutable objects rather than JavaBeans, that’s also doable:
public class Notification {
private final String text;
@JsonCreator
public Notification(@JsonProperty("text") String text) {
this.text = text;
}
@JsonProperty("text")
public String getText() {
return text;
}
}
Advanced JSON¶
Not all JSON representations map nicely to the objects your application deals with, so it’s sometimes necessary to use custom serializers and deserializers. Just annotate your object like this:
@JsonSerialize(using=FunkySerializer.class)
@JsonDeserialize(using=FunkyDeserializer.class)
public class Funky {
// ...
}
Then make a FunkySerializer
class which implements JsonSerializer<Funky>
and a
FunkyDeserializer
class which implements JsonDeserializer<Funky>
.
Snake Case¶
A common issue with JSON is the disagreement between camelCase
and snake_case
field names.
Java and Javascript folks tend to like camelCase
; Ruby, Python, and Perl folks insist on
snake_case
. To make Dropwizard automatically convert field names to snake_case
(and back),
just annotate the class with @JsonSnakeCase
:
@JsonSnakeCase
public class Person {
private final String firstName;
@JsonCreator
public Person(@JsonProperty String firstName) {
this.firstName = firstName;
}
@JsonProperty
public String getFirstName() {
return firstName;
}
}
This gets converted into this JSON:
{
"first_name": "Coda"
}
Unknown properties¶
If the name of a JSON property cannot be mapped to a Java property (or otherwise handled), that JSON property will simply be ignored.
You can change this behavior by configuring Dropwizard’s object mapper:
public void initialize(Bootstrap<ExampleConfiguration> bootstrap) {
bootstrap.getObjectMapper().enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
Note
The YAML configuration parser will fail on unknown properties regardless of the object mapper configuration.
Streaming Output¶
If your application happens to return lots of information, you may get a big performance and efficiency
bump by using streaming output. By returning an object which implements Jersey’s StreamingOutput
interface, your method can stream the response entity in a chunk-encoded output stream. Otherwise,
you’ll need to fully construct your return value and then hand it off to be sent to the client.
HTML Representations¶
For generating HTML pages, check out Dropwizard’s views support.
Custom Representations¶
Sometimes, though, you’ve got some wacky output format you need to produce or consume and no amount
of arguing will make JSON acceptable. That’s unfortunate but OK. You can add support for arbitrary
input and output formats by creating classes which implement Jersey’s MessageBodyReader<T>
and
MessageBodyWriter<T>
interfaces. (Make sure they’re annotated with @Provider
and
@Produces("text/gibberish")
or @Consumes("text/gibberish")
.) Once you’re done, just add
instances of them (or their classes if they depend on Jersey’s @Context
injection) to your
application’s Environment
on initialization.
Filters¶
There might be cases when you want to filter out requests or modify them before they reach your Resources.
Jersey filters¶
Jersey has a rich api for filters and interceptors that can be used directly in Dropwizard.
You can stop the request from reaching your resources by throwing a WebApplicationException
. Alternatively,
you can use filters to modify inbound requests or outbound responses.
@Provider
public class DateNotSpecifiedFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String dateHeader = requestContext.getHeaderString(HttpHeaders.DATE);
if (dateHeader == null) {
Exception cause = new IllegalArgumentException("Date Header was not specified");
throw new WebApplicationException(cause, Response.Status.BAD_REQUEST);
}
}
}
This example filter checks the request for the “Date” header, and denies the request if was missing. Otherwise, the request is passed through.
Filters can be dynamically bound to resource methods using DynamicFeature:
@Provider
public class DateRequiredFeature implements DynamicFeature {
@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
if (resourceInfo.getResourceMethod().getAnnotation(DateRequired.class) != null) {
context.register(DateNotSpecifiedFilter.class);
}
}
}
The DynamicFeature is invoked by the Jersey runtime when the application is started. In this example, the feature checks
for methods that are annotated with @DateRequired
and registers the DateNotSpecified
filter on those methods only.
You typically register the feature in your Application class, like so:
environment.jersey().register(DateRequiredFeature.class);
Servlet filters¶
Another way to create filters is by creating servlet filters. They offer a way to register filters that apply both to servlet requests as well as resource requests. Jetty comes with a few bundled filters which may already suit your needs. If you want to create your own filter, this example demonstrates a servlet filter analogous to the previous example:
public class DateNotSpecifiedServletFilter implements jakarta.servlet.Filter {
// Other methods in interface omitted for brevity
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
String dateHeader = ((HttpServletRequest) request).getHeader(HttpHeaders.DATE);
if (dateHeader != null) {
chain.doFilter(request, response); // This signals that the request should pass this filter
} else {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpStatus.BAD_REQUEST_400);
httpResponse.getWriter().print("Date Header was not specified");
}
}
}
}
This servlet filter can then be registered in your Application class by wrapping it in FilterHolder
and adding it to the application context together with a
specification for which paths this filter will be active. Here’s an example:
environment.servlets().addFilter("DateNotSpecifiedServletFilter", new DateNotSpecifiedServletFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
How it’s glued together¶
When your application starts up, it will spin up a Jetty HTTP server, see DefaultServerFactory
.
This server will have two handlers, one for your application port and the other for your admin port.
The admin handler creates and registers the AdminServlet
. This has a handle to all of the
application healthchecks and metrics via the ServletContext.
The application port has an HttpServlet as well, this is composed of DropwizardResourceConfig
,
which is an extension of Jersey’s resource configuration that performs scanning to
find root resource and provider classes. Ultimately when you call
env.jersey().register(new SomeResource())
,
you are adding to the DropwizardResourceConfig
. This config is a jersey Application
, so all of
your application resources are served from one Servlet
DropwizardResourceConfig
is where the various ResourceMethodDispatchAdapter are registered to
enable the following functionality:
- Resource method requests with
@Timed
,@Metered
,@ExceptionMetered
are delegated to special dispatchers which decorate the metric telemetry- Resources that return Optional are unboxed. Present returns underlying type, and non-present 404s
- Resource methods that are annotated with
@CacheControl
are delegated to a special dispatcher that decorates on the cache control headers- Enables using Jackson to parse request entities into objects and generate response entities from objects, all while performing validation