The unofficial Dropwizard 0.7.1 to 0.8.5 upgrade notes

A tale of endless googling, trial and error and dependency nightmare

According to the website, Dropwizard is a Java framework for developing ops-friendly, high-performance, RESTful web services.

Image for post
Image for post

As per the Dropwizard 0.8.0 release notes, the most significant change is Jersey upgrade from version 1.18.1 to 2.16, and depending on how heavily you have built on top of Jersey, it could be a smooth or relatively challenging upgrade.

Jersey 1.x — https://github.com/jersey/jersey

Jersey 2.x — https://github.com/eclipse-ee4j/jersey

The Dropwizard 0.7.x to 0.8.x migration guide is very brief https://github.com/dropwizard/dropwizard/wiki/Upgrade-guide-0.7.x-to-0.8.x and was of not much help in my case.

Hopefully, the content of this article, in addition to the migration guide above, would help address a majority of upgrade issues.

Pre-merge, post-merge and testing checklist

If you are upgrading a production-level application, it’s important to make sure that

  • There is a sufficient level of unit and integration test coverage
  • Performance/load testing pipeline exist
  • Some sort of deployment pipeline with canarying
  • The ability to release, hotfix, and rollback if and when needed

After the upgrade, here is what I did

  1. Ensure that unit and integration tests pass locally and in CI server ✔️
  2. G️et the required pull request reviews ✔️
  3. Verify that logs are still visible in your log monitoring systems ✔️
  4. Code freeze on the branch/pull request ✔️
  5. Run performance/load test before/after the upgrade ✔️
  6. Run UI automation tests to ensure functionalities are not broken ✔️
  7. Perform some manual testing of features i.e. SSO, etc. ✔️
  8. Coordinate with the development teams the merge and deployment to dev, canary and productionenvironment date ✔️
  9. Release tocanary (if you have canarying) and keep an eye on issues/bugs reported to identify if related to the upgrade ✔️
  10. Release to prod and keep an eye on issues reported and logs ✔️
  11. When a related issue/bug surfaces, gather the appropriate devs to investigate the issue and decide whether to do a hotfix or the fix can go in the next release ✔️
  12. For the next few days to few weeks keep an eye on logs, and product issues reported ✔️

Fix the imports

Jersey 1.x package name was com.sun.jersey.* and in Jersey 2.x it is org.glassfish.jersey.* and some of the classes are in favor of other options available to us.

  • com.sun.jersey.api.NotFoundExceptionjavax.ws.rs.NotFoundException
  • com.sun.jersey.core.util.Base64 java.util.Base64
  • com.sun.jersey.multipart.*org.glassfish.jersey.media.multipart.*
  • com.sun.jersey.api.ConflictException javax.ws.rs.ClientErrorException
  • com.sun.jersey.api.core.ExtendedUriInfoorg.glassfish.jersey.server.ExtendedUriInfo
  • com.sun.jersey.api.uri.UriTemplateorg.glassfish.jersey.uri.UriTemplate
  • com.sun.jersey.spi.container.ContainerRequestFilterjavax.ws.rs.container.ContainerRequestFilter
  • com.sun.jersey.spi.container.ContainerResponseFilterjavax.ws.rs.container.ContainerResponseFilter
  • com.sun.jersey.spi.container.ContainerRequestjavax.ws.rs.container.ContainerRequestContext
  • com.sun.jersey.spi.container.ContainerResponsejavax.ws.rs.container.ContainerResponseContext
  • com.sun.jersey.api.client.Clientjavax.ws.rs.client.*
  • com.sun.jersey.api.core.ResourceConfigorg.glassfish.jersey.server.ResourceConfig
  • com.sun.jersey.api.container.filter.LoggingFilterorg.glassfish.jersey.filter.LoggingFilter

As shown above, the NotFoundExceptionis dropped and could be replaced with javax.ws.rs version, and similarly, in place of ConflictException, the ClientErrorException could be used passing it a Response.Status.CONFLICT type.

As a result of the change from ContainerRequest to ContainerRequestContext the request.getRequestUri().getPath() is not there and initially, I used request.geturiInfo().getPath() however, after some digging, I found out that with the above change the leading / is missing and so the right choice was to use request.getUriInfo().getAbsolutePath().getPath() based on the Javadoc of UriInfo.getAbsolutePath() and requestUri.getPath() of Jersey.

Worth looking into this StackOverflow answer for more Jersey 2.x class names.

Base64 Encoder/Decoder

Jersey 1.x came with a com.sun.jersey.core.util.Base64 and Jersey 2 does not have a replacement, so I switched to using the java.util.Base64 (in Jersey 2 the Base64 class is dropped) one of the concerns was to verify that the output of encoding and especially decoding remains the same.

Jersey 1.x Base64 mentions that it supports RFC2045 and the java.util.Base64 says it supports RFC2045 as well as RFC 4648.

To be sure, wrote a unit test to test byte arrays of various lengths i.e. 0,1,2,3,4 bytes, and some special characters and symbols

< > / \ “ ‘ : ; & ? @ % #

You can find the unit test in this gist.

Dependencies

You can either rely on dropwizard-core to bring all the compile dependencies or exclude the compile dependency and bring a different version of it. Let’s say you do not wish to bring the same version of hibernate that dropwizard-core of 0.8.5 brings in which case you exclude the dropwizard-hibernate from dropwizard-core and declare it separately with a different version.

In my case, dependencies such as jersey, hibernate, jackson, jetty, shiro and a few others are declared independently, so I had to manually upgrade them.

I have the following dependencies and you might or might not need them all

  • Update dropwizard-core version to 0.8.5
  • Update jeryse version to 2.16
  • Update jetty version to 9.2.9.v20150224
  • Update Jackson version to 2.5.1+
  • Update org.second.dropwizard:dropwizard-shire to 2.0.0 if you use Apache Shiro

If you have dropwizard-hibernate then for 0.8.5 it would be pulling an older version of org.jadira.usertype which would result in the following error

java.lang.NoSuchMethodError: org.hibernate.engine.jdbc.spi.JdbcServices.getConnectionProvider()Lorg/hibernate/engine/jdbc/connections/spi/ConnectionProvider

and based on this answer in StackOverflow, I excluded hibernate-core and org.jaidra.usertype from dropwizard-hibernate as shown below

<dependency>            
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-hibernate</artifactId>
<version>${dropwizard.version}</version>
<exclusions>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
</exclusion>
</exclusions>
</dependency>

and brought the two excluded compile dependencies separately, since org.jadira.usertype bring hibernate-entitymanager need to exclude that so the version of dropwizard-hibernate is pulled.

<dependency>            
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>4.0.0.GA</version>
<exclusions>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</exclusion>
</exclusions>
</dependency>

If you don’t exclude the hibernate-entitymanager then you are probably going to see the following error, see this answer

Exception in thread "main" java.lang.NoClassDefFoundError: org/hibernate/engine/jndi/JndiNameException and javax.ws.rs.core.Application.getProperties()Ljava/util/Map

Register MultiPartFeature & LoggingFilter

For Multipart, I suggest looking into this answer in Stackoverflow, and more specifically don’t forget to register the MutliPartFeature.class as follow

environment.jersey().register(MultiPartFeature.class);

In order to ensure that the inbound/outbound request bodies are logged, you need the following

environment.jersey().register(new LoggingFilter(Logger.getLogger("InboundRequestResponse"), true));

The boolean passed turns on and off the POST entity body that you might not want change to false for production. See http://danofhisword.com/dev/dropwizard/2015/06/29/dropwizard-logging.html

In my case, a Restful web service so all errors are text or JSON with a status code, and without the following, Jersey would try to find servlet error pages and result in 404 for any error.

environment.jersey().getResourceConfig().property(ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, true);

Update Jersey 1.x WebClient to 2.x

If you have used Jersey 1.x WebClient to test endpoints then those tests need to be updated

// HTTP GET example// Jersey 1.x
Student result = client.resource("/api/student/1").type(MediaType.APPLICATION_JSON_TYPE).get(Student.class);
// Jersey 2.x
Student result = client.target("/api/student/1").request(MediaType.APPLICATION_JSON_TYPE).get(Student.class);

Notice the change from .resource to .target and from .type to .request . While HTTP GET is relatively straight forward, HTTP POST becomes a little more verbose as shown below

// HTTP POST example// Jersey 1.x 
Student student = client.resource("/api/student").type(MediaType.APPLICATION_JSON).post(Student.class, new Student());
// Jersey 2.x
WebTarget webTarget = client.target("/api/student"); Invocation.Builder invocationBuilder = webTarget.request(MediaType.APPLICATION_JSON); invocationBuilder.post(Entity.json("..."));

I will leave the HTTP PUT and HTTP DELETE for you to lookup.

Diff between com.sun.jersey.api.NotFoundException and javax.ws.rs.NotFoundException

In Jersey 1.x in order to get the message from NotFoundException you need to call exception.getResponse().getEntity() whereas in javax.ws.rs.NotFoundException it’s simply exception.getMessage() and if you have written a unit test using Jersey 1 NotFoundException then it’s hard to make use of the Rule to verify the exception message however, now it's much easier.

JacksonMessageBodyParser Fork

If in you have any endpoint and/or API tests that fall into the following categories

  1. A HTTP POST is passed "" or null instead of {}
  2. A HTTP DELETE is provided a body

Then you would get an exception and that’s because of https://github.com/dropwizard/dropwizard/issues/625 and https://github.com/dropwizard/dropwizard/pull/633. A fix was added in this commit https://github.com/dropwizard/dropwizard/pull/633/commits/4ae991ed37bab64215727b68381be81cc36b45b4

Dropwizard 0.8.5 does not log sub-resource paths in the console on application startup

I created an entry in the dropwizard-dev Google group forum and with steps on how to reproduce the issue https://groups.google.com/forum/#!topic/dropwizard-dev/G7PdlUWxmsI and have not received a response with an explanation.

I have documented workarounds in the Github https://github.com/rhamedy/dropwizard-sample-app and it does require forking DropwizardResourceConfig

Jetty IOException: Broken pipe and TimeoutException

After upgrading to Dropwizard 0.8.5 two exceptions started showing up and one of them is java.io.IOException: Broken pipe shown below

java.io.IOException: Broken pipe
at sun.nio.ch.FileDispatcherImpl.writev0(FileDispatcherImpl.java)
at sun.nio.ch.SocketDispatcher.writev(SocketDispatcher.java:51)
at sun.nio.ch.IOUtil.write(IOUtil.java:148)
at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:501)
at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:172)at org.eclipse.jetty.io.WriteFlusher.flush(WriteFlusher.java:408)
at org.eclipse.jetty.io.WriteFlusher.write(WriteFlusher.java:302)
.
.
.
org.eclipse.jetty.io.EofException: null
at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:192)at org.eclipse.jetty.io.WriteFlusher.flush(WriteFlusher.java:408)at org.eclipse.jetty.io.WriteFlusher.write(WriteFlusher.java:302)
...

and the other is TimeoutException: Idle timeout expired: 600000/600000 ms with message

An I/O error has occurred while writing a response message entity to the container output stream.

As for the timeout exception, the explanation given in this https://github.com/eclipse/jetty.project/issues/3907 Github issue might be the case.

In comparison to TimeoutException , the Broken pipe the exception happens a lot more often and a majority in the community indicate that it’s safe to ignore and you can read about it in

and

and I have been able to reproduce

If the server side HTTP application is getting Broken Pipe exceptions it just means the client browser has exited/gone to another page/timed out/gone back in the history/whatever. Just forget about it.

by excessive clicking of the front-end and moving away from the page before it loads.

There is also a way to reproduce Broken pipe exception programmatically, and here is a gist https://gist.github.com/rhamedy/97bd27ac748204cdbb9e439e4d268bdd on how to do it. Comment-out lines 19–23 to observe exception.

Conclusion

This was my first ever attempt at upgrading a production-level application with thousands of users and automated processes consuming the APIs, and I have to admit that the following was a huge confidence boost

  • A good level of the unit and integration testing
  • A competent group of developers and architects who helped with code reviews and investigation of pre and post-upgrade issues
  • The CI/CD pipeline that makes it super easy to release, hotfix and rollback
  • Appropriate log monitoring systems i.e. Datadog, Loggly, and Sentry
  • A desire and support from the team to upgrade these frameworks

I hope that you find this article helpful 👍

Further Reading

I have also written a few other articles on the following topics, please feel free to read them

  • How to Load Test: A developer’s guide to performance testing
  • JUnit 4 & 5 Annotations Every Developer Should Know
  • Why you should think twice about contributing to Open Source (promotes contribution in contrary to the title)
  • Key habits and things I wish I knew earlier as a developer
  • A short summary of Java coding best practices
  • Encryption and decryption of data based on users password using PBKDF2 and AES

A Human first • Senior software developer • Loves to write with over 200K views on Medium • Co-founder in the making • Let’s connect on linkedin.com/in/rhamedy

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store