3

Please note: Although this question specifically mentions Dropwizard, I believe anyone with Jersey/JAX-RS experience should be able to answer this question, as I would imagine Dropwizard is just following Jersey/JAX-RS conventions under the hood.


I have a Dropwizard service that reds/writes in JSON and works beautifully.

I would like to now switch it to read/write binary data (to minimize network bandidth). I see there is the Dropwizard-Protobuf lib but I have a few concerns about implementing binary serialization in Dropwizard.

First off, here's the important stuff from my current (JSON-centric) code:

// Groovy pseudo-code

// Domain entity/POJO
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
class Fizz {
    @JsonProperty
    String name

    @JsonProperty
    boolean isBuzz    
}

// The Dropwizard app entry point
class FizzService extends Application<FizzConfiguration> {
    @Override
    void run(FizzConfiguration fizzCfg, Environment env) throws Exception {
        // ... lots of stuff

        env.jersey().register(new FizzService())
    }
}

// JAX-RS resource with a sample GET endpoint
@Path(value = "/fizz")
@Produces(MediaType.APPLICATION_JSON)
class FizzResource {
    @GET
    @Path("/{id}")
    Fizz getFizzById(@PathParam("id") int id) {
        // Look up a 'Fizz' in a DB and return it.
        lookupFizzinDB(id)
    }
}

So as you can see, the GET /fizz endpoint expect a JSON request entity that contains an element called id of type int. It returns a Fizz response entity that matches the provided id.

I want to switch this from JSON to binary via Google Protocol Buffers.

According to the Dropwizard-Protobuf docs, this is as simple as just adding this to my FizzService#run(...) method:

environment.jersey().register(new ProtocolBufferMessageBodyProvider())

The problem is that currently my whole app is wired to serialize/deserialize to/from JSON. The @JsonProperty annotations on my Fizz class have meaning to Dropwizard. The @Produces(MediaType.APPLICATION_JSON) annotation on the FizzResource also plays a critical role. I'm worried that making my Dropwizard app read/write protobuf-generated binary is not as easy as the 1-liner posted in the docs.

I'm not married to this library. If anyone has any experience setting up REST endpoints in a Dropwizard app to accept/receive protobuf-generated binary, all I care about is a working, efficient solution. Ideas?

smeeb
  • 27,777
  • 57
  • 250
  • 447

1 Answers1

6

You're right, it's not as easy as the one liner. You need to have protobuf generate code for it to work. Check out the Protocol Buffers Documentation. You first need to have a proto file that you compile with the protobuf compiler, which generates the code for you. This generated code is what you use to build your domain/model objects. The protobuf provider from Dropwizard works off this compiled code. Whether or not you use the Dropwizard provider, you well still need to use the generated code. See the section "How do I start" in the above link.

After you have the generated code, then in your resource method, the generated class/type is what you will need to return for the provider to be able to serialize it. You will also need to have @Produces("application/x-protobuf") or @Produces(ProtocolBufferMediaType.APPLICATION_PROTOBUF) on your resource method or resource class, so Jersey knows how to find the provider for the media type.

You can support both application/json and application/x-protobuf, as you can have more that one media type in the @Produces. Just use the syntax @Produces({ .. , .. }).

That's not all though. Since you will need to return two different types, i.e your simple POJO for JSON, or the generated type for Protobuf, you will either need to check for the header in the resource method

@Produces({"application/json", "application/x-protobuf"})
public Response getFoo(@Context HttpHeaders headers) {
    List<MediaType> accepts = headers.getAcceptableMediaTypes();
    if (accepts.contains(MediaType.APPLICATION_JSON_TYPE) {
        return Response.ok(new Foo());
    } else if (accepts.contains(ProtocolBufferMediaType.APPLICATION_PROTOBUF_TYPE) {
        return Reponse.ok(new ProtoBufFoo());
    } else {
        // default
        return Response.ok(new Foo());
    }
}

Or you can have two different method, one for each type

@Produces("application/json")
public Response getFooJson() {
    return Response.ok(new Foo());
}

@Produces("application/x-protobuf")
public Response getFooProto() {
    return Response.ok(new ProtoBufFoo());
}

Whatever the client sends as its Accept header, that is the type that will be sent out. For example Accept: application/json or Accept: application/x-protobuf

See Also:

Community
  • 1
  • 1
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Awesome, awesome answer @peeskillet (+1) - one quick followup if you don't mind: it triggers my OCD that I have to maintain 2 different sets of POJOs (one for JSON, one for Protobuf). Any way to combine these into one POJO? Might be hacky, but my thinking was have a Gradle/Maven task generate the Protobuf POJO from the `.proto` file, then have a subsequent task add the necessary JSON annotations to the generated source file? Would that work?!? Or is there more to the Protobuf generation than just that? Thanks again! – smeeb Sep 11 '15 at 13:23
  • 1
    I don't know it seems pretty complicated what you are saying. I've never really actually used Protobuf with Jersey, so I've never really explored any options for merging the two. Sorry :-( – Paul Samsotha Sep 11 '15 at 13:38
  • Makes sense, and yes it is admittedly kludgy :-) Sorry, I forgot one important (final - I promise!) followup: if I use your code above as a guide, do I still need to use that 1-liner from the Dropwizard-Protobuf lib (where I register the Protobuf provider)? I assume that's what's looking for requests where the `Accept` type is set to "*application/x-protobuf*"? Thanks again so much! – smeeb Sep 11 '15 at 13:41
  • 1
    Yes. That is what actually registers the `MessageBodyWriter` to handle the serialization. The `@Produces` is only to tell jersey to look for a provider that can handle that type. It doesn't mean that there is already a provider to handle it. If you don't have a provider to handle it, then Jersey will give you an exception with something like "No MessageBodyWriter to handle type application/x-protobuf and type ProtobufFoo" – Paul Samsotha Sep 11 '15 at 13:45