Saturday, 23 August 2014

Jersey (JAX-RS) with Protocol Buffer for high performance Rest API

In this tutorial you will learn how to develop a Restful service with Jersey/JAX-RS using protocol buffer (commonly known as protobuf)for high performance.One of the great things about the JAX-RS specification is that it is very extensible and adding new providers for different mime-types is very easy. One of the interesting binary protocols out there is Google Protocol Buffers. They are designed for
 high-performance systems and drastically reduce the amount of over-the-wire data and also the amount of CPU spent serializing and deserializing that data.

The first thing you will need to do to get started is to download and build Protocol Buffers.
The next step is to create a Protocol Buffer using their definition language.We will create a customer.proto that will contain the schema for custmer information.

customer.proto

option java_package = "com.service.protobuf.example";
option java_outer_classname = "CustomerProtos";

enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
}
message PhoneNumber {
    required string phoneNumber=1;
    optional PhoneType phoneType=3;

     
message Address {
    required string addressLine1=2;
    optional string zipCode=4;
    optional string country=5;
    optional string state=6;
}

message CreditCard {
    required string cardNumber=1;
    optional string expMonth=2;
    optional string expYear = 3;
    required Address billingAddress = 5;
    required string type=7;
}   

message Customer{
     required int32 id=1;
     required string lastName=2;
     required string firstName=3;
     required string email=4;
     repeated PhoneNumber phoneNumber=6;
     repeated Address addresses=8;
     repeated CreditCard creditCards=9;

}

A fairly simple data description but it does touch on a lot of the features of Protocol Buffers including embedded messages, enums, repeating entries and their type system. You need to generate the Java code fro protobuf language using the protocol buffer compiler(protoc) that you have to download as mentioned above.

Sample Command:
//protoc -I=D:\Study\Codebase\test --java_out=D:\Study\Codebase\test\code D:\Study\Codebase\test\customer.proto

Now lets define a simple service that we want to get to work using the extension SPI of JAX-RS. This service will have two methods, a GET method for returning a new instance of a Customer and a POST method that just reflects what is passed to it back to the caller unmodified.

CustomerService.java

@Path("/customer")
public class CustomerService {

    @POST
    @Consumes("application/x-protobuf")
    @Produces("application/x-protobuf")
public CustomerProtos.Customer process(CustomerProtos.Customer  person) {
        return person;    
        }       
     
 @GET
     @Produces("application/x-protobuf")
     public CustomerProtos.Customer getCust(){
        return  CustomerProtos.Customer.newBuilder()
            .setEmail("a@bdc.com")
            .setFirstName("Test")
            .setLastName("Customer")
            .setId(1)
            .addAddresses(CustomerProtos.Address.newBuilder()
                    .setAddressLine1("123 Main St")
                    .setCountry("US")
                    .setState("CA")
                    .setZipCode("12345"))
            .addPhoneNumber(CustomerProtos.PhoneNumber.newBuilder()
                    .setPhoneNumber("415 510 5100")
                    .setPhoneType(CustomerProtos.PhoneType.HOME))
            .addCreditCards(CustomerProtos.CreditCard.newBuilder()
                    .setCardNumber("54111111111111")
                    .setExpMonth("10")
                    .setExpYear("2010")
                    .setType("VISA")
                    .setBillingAddress(CustomerProtos.Address.newBuilder()
                        .setAddressLine1("456Main St")
                        .setCountry("US")
                        .setState("AZ")
                        .setZipCode("12345"))
            ).build();
     }

}

For each of these methods we’ve restricted them to either consuming or producing content of type application/x-protobuf
When JAX-RS sees a request that matches that type or a caller that accepts that type these will be valid endpoints to satisfy those requests. Out of the box, Jersey includes readers and writers for a variety of types including form data, XML and JSON. 
They also provide a way to register new mime-type readers and writers with a very simple set of annotations on classes that implement either MessageBodyReader  or MessageBodyWriter. The class that implements reading is very straight forward, first it calls you back to see if you can read something, then it calls you to actually read it passing you the stream of data. 

This class either needs to be under a package that is registered to be scanned when the application starts or it could be explicitly registered by extending Application. You’ll note that in order for us to instantiate a new Protocol Buffer builder we need to use reflection on the type that JAX-RS is expecting. The writer is a bit more complicated because in addition to the isWritable and writeTo methods you have to be able to return the size that you are going to write.

ProtoBufMimeProvider.java

@Provider
@Consumes("application/x-protobuf")
@Produces("application/x-protobuf")
 
public class ProtoBufMimeProvider implements MessageBodyWriter<Message>, MessageBodyReader<Message> {
    //MessageBodyWriter Implementation
    @Override
    public long getSize(Message message, Class<?> arg1, Type arg2, Annotation[] arg3,
            MediaType arg4) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            message.writeTo(baos);
        } catch (IOException e) {
            return -1;
        }
        return baos.size();
    }
 
    @Override
    public boolean isWriteable(Class<?> arg0, Type arg1, Annotation[] arg2,
            MediaType arg3) {
        return Message.class.isAssignableFrom(arg0);
    }
 
    @Override
    public void writeTo(Message message, Class<?> arg1, Type arg2, Annotation[] arg3,
            MediaType arg4, MultivaluedMap<String, Object> arg5,
            OutputStream ostream) throws IOException, WebApplicationException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        message.writeTo(baos);
        ostream.write(baos.toByteArray());
    }
     
     
    //MessageBodyReader Implementation
    @Override
    public boolean isReadable(Class<?> arg0, Type arg1, Annotation[] arg2,
            MediaType arg3) {
        return Message.class.isAssignableFrom(arg0);
    }
    @Override
    public Message readFrom(Class<Message> arg0, Type arg1, Annotation[] arg2,
            MediaType arg3, MultivaluedMap<String, String> arg4,
            InputStream istream) throws IOException, WebApplicationException {
        try {
            Method builderMethod = arg0.getMethod("newBuilder");
            GeneratedMessage.Builder<?> builder = (GeneratedMessage.Builder<?>) builderMethod.invoke(arg0);
            return builder.mergeFrom(istream).build();
        } catch (Exception e) {
            throw new WebApplicationException(e);
        }
    }
}


Now deploy the service and navigate to the following( with CONTENT-TYPE  as application/x-protobuf) using Rest client like(Poster,Postman) or use you own jersey client given in the code:

http://localhost:8080/RestFileServer/service/customer



Jersey Client:

 private void protoTest() throws IOException{
  
 URL target = new URL ("http://localhost:8080/RestFileServer/service/customer");
 HttpURLConnection conn = (HttpURLConnection) target.openConnection ();
 conn.setDoOutput (true);
 conn.setDoInput (true);
 conn.setRequestMethod ("GET");
 conn.setRequestProperty ("Content-Type", "application/x-protobuf");
conn.setRequestProperty ("Accept", "application/x-protobuf");
 conn.connect ();
 // Check response code
 int code = conn.getResponseCode ();
// boolean success = (code>=200) && (code <300);
 //InputStream in = success? conn.getInputStream (): conn.getErrorStream ();
 InputStream in=conn.getInputStream ();
 int size = conn.getContentLength ();
 byte [] response = new byte [size];
 
 int curr = 0, read = 0;
 while (curr==read){
 read = in.read (response, curr, size - curr);
 if (read <= 0) break;
 curr+=read;
 }
 
 Customer c=Customer.parseFrom (response);
 
 System.out.println ("id:" + c.getId());
 System.out.println ("name:" + c.getFirstName());
 System.out.println ("email:" + c.getEmail());

 for (PhoneNumber pn:c.getPhoneNumberList()) {
 System.out.println ("number:" + pn.getPhoneNumber() + "type:" + pn.getPhoneType().name());

 }
}

Download Code

4 comments: