This first thing that you will need to create is a RestController
to expose access to your Apache Thrift objects. Let’s assume that you have
the following Apache Thrift object defined in your Apache Thrift definition file:
namespace java com.example.v1
enum BookFormat {
ELECTRONIC,
HARDCOVER,
PAPERBACK
}
struct Book {
10:required string author
20:required string title
30:required string isbn10
40:required string isbn13
50:required BookFormat format
60:required i64 publishDate
70:optional string language
80:optional i64 pages
90:optional i64 edition
}
To provide a way to get a Book
by title, you would create the following:
import com.example.v1.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/books")
public class BookController {
@Autowired
private BookRepository bookRepository;
@RequestMapping(method=RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE}, consumes = {MediaType.ALL_VALUE})
public ResponseEntity<Book> getBookByTitle(@RequestParam(value="title", required=true) final String title) {
Book book = convertToThrift(bookRepository.findByTitle(title));
if(book != null) {
return new ResponseEntity<Book>(book, HttpStatus.OK);
} else {
return new ResponseEntity<Book>(HttpStatus.NOT_FOUND);
}
}
private Book converToThrift(BookEntity bookEntity) {
// Let's assume this method handles the creation of a Thrift-based Book from the JPA entity
...
}
}
When an HTTP GET is made to http://<server>:<port>/api/v1/books with the query string ?title=<some title>
that matches a known book, the book will be
returned in JSON format. As mentioned earlier, this will work with Apache Thrift objects out of the box, as Spring Boot includes Jackson 2 support for
controllers annotated with the RestController
annotation, but will included unwanted fields. To address this, the next step is to add a custom
Jackson 2 serializer. In this example, the code has been extracted to an abstract class to make it easier to add additional serializers and/or for
cases where composition is used in the Apache Thrift definition file:
package com.example.json;
import java.io.IOException;
import java.util.Collection;
import org.apache.thrift.TBase;
import org.apache.thrift.TFieldIdEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.google.common.base.CaseFormat;
/**
* This abstract class represents a generic serializer for converting Thrift-based entities to JSON.
*
* @param <E> An implementation of the {@link TFieldIdEnum} interface.
* @param <T> An implementation of the {@link TBase} interface.
*/
public abstract class AbstractThriftSerializer<E extends TFieldIdEnum, T extends TBase<T, E>> extends JsonSerializer<T> {
private static final Logger log = LoggerFactory.getLogger(AbstractThriftSerializer.class);
@Override
public Class<T> handledType() {
return getThriftClass();
}
@Override
public void serialize(final T value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
for(final E field : getFieldValues()) {
if(value.isSet(field)) {
final Object fieldValue = value.getFieldValue(field);
if(fieldValue != null) {
log.debug("Adding field {} to the JSON string...", CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,field.getFieldName()));
jgen.writeFieldName(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,field.getFieldName()));
if(fieldValue instanceof Short) {
jgen.writeNumber((Short)fieldValue);
} else if(fieldValue instanceof Integer) {
jgen.writeNumber((Integer)fieldValue);
} else if(fieldValue instanceof Long) {
jgen.writeNumber((Long)fieldValue);
} else if(fieldValue instanceof Double) {
jgen.writeNumber((Double)fieldValue);
} else if(fieldValue instanceof Float) {
jgen.writeNumber((Float)fieldValue);
} else if(fieldValue instanceof Boolean) {
jgen.writeBoolean((Boolean)fieldValue);
} else if(fieldValue instanceof String) {
jgen.writeString(fieldValue.toString());
} else if(fieldValue instanceof Collection) {
log.debug("Array opened for field {}.", CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,field.getFieldName()));
jgen.writeStartArray();
for(final Object arrayObject : (Collection<?>)fieldValue) {
jgen.writeObject(arrayObject);
}
jgen.writeEndArray();
log.debug("Array closed for field {}.", CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,field.getFieldName()));
} else {
jgen.writeObject(fieldValue);
}
} else {
log.debug("Skipping converting field {} to JSON: value is null!", CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,field.getFieldName()));
}
} else {
log.debug("Skipping converting field {} to JSON: field has not been set!", CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,field.getFieldName()));
}
}
jgen.writeEndObject();
}
/**
* Returns an array of {@code <E>} enumerated values that represent the fields present in the
* Thrift class associated with this serializer.
* @return The array of {@code <E>} enumerated values that represent the fields present in the
* Thrift class.
*/
protected abstract E[] getFieldValues();
/**
* Returns the {@code <T>} implementation class associated with this serializer.
* @return The {@code <T>} implementation class
*/
protected abstract Class<T> getThriftClass();
}
The AbstractThriftSerializer
extends the Jackson 2 JsonSerializer
to provide instructions to Jackson 2 on how to convert
a Apache Thrift based object to JSON. In particular, it uses the TFieldIdEnum
enumeration found in each Apache Thrift generated class
that provides metadata about each field in the class. If a value has been set for the each field, the value is converted
to JSON based on the Java type associated with that field. In addition, some additional logic was added to convert the
camel cased field names to lower case underscore format using Google Guava's CaseFormat
utility. Implementations of this abstract
class simply need to provide access to the TFieldIdEnum
enumeration declared within the class, as well as the specific type
for registration with Jackson 2:
package com.example.json;
import com.example.v2.Book;
import com.example.v2.Book._Fields;
public class BookSerializer extends AbstractThriftSerializer<Book._Fields, Book> {
@Override
protected _Fields[] getFieldValues() {
return Book._Fields.values();
}
@Override
protected Class<Book> getThriftClass() {
return Book.class;
}
}
The final step is to register the custom serializer with Spring Boot so that the REST
controller will use it when converting our Book
object to JSON. Let’s re-visit the BookController
:
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.v1.Book;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
@RestController
@RequestMapping("/api/v1/books")
public class BookController implements InitializingBean {
@Autowired
private BookRepository bookRepository;
@Autowired
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;
@Override
public void afterPropertiesSet() throws Exception {
// Register the custom Thrift <> JSON deserializers/serializers.
final ObjectMapper mapper = mappingJackson2HttpMessageConverter.getObjectMapper();
final SimpleModule bookModule = new SimpleModule("Book", new Version(1,0,0,null,null,null));
bookModule.addSerializer(new BookSerializer());
mapper.registerModule(bookModule);
}
@RequestMapping(method=RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE}, consumes = {MediaType.ALL_VALUE})
public ResponseEntity<Book> getBookByTitle(@RequestParam(value="title", required=true) final String title) {
Book book = convertToThrift(bookRepository.findByTitle(title));
if(book != null) {
return new ResponseEntity<Book>(book, HttpStatus.OK);
} else {
return new ResponseEntity<Book>(HttpStatus.NOT_FOUND);
}
}
private Book converToThrift(BookEntity bookEntity) {
// Let's assume this method handles the creation of a Thrift-based Book from the JPA entity
...
}
}
So, what did we add? First, we modified the BookController
to implement the InitializingBean
interface so that we could handle the
Jackson 2 configuration at bean creation time. Second, we injected the MappingJackson2HttpMessageConverter
, which is provided by
{sprinb_boot} to handle the conversion of entities to JSON when a controller action is marked to produce JSON. Finally, we implemented
the afterPropertiesSet
method of the InitializingBean
interface to register our BookSerializer
with the Jackson 2 ObjectMapper
used by the MappingJackson2HttpMessageConverter
. Now, when we perform an HTTP GET against our endpoint for a book title that matches
an existing book, we will see the following JSON response:
{
"author" : "Rob Friesel",
"title" : "PhamtomJS Cookbook",
"isbn_10" : "178398192X",
"isbn_13" : "978-1783981922",
"format" : "PAPERBACK",
"publish_date" : 1402531200000,
"language" : "English",
"pages" : 276,
"edition" : 1
}
So, what did we accomplish. First, we were able to customize how Jackson 2 converts an object to JSON. Second, we were able to convert our
Apache Thrift objects to JSON in a manner of our choosing. Third, we did all of this without having to create any new DTO’s or extend from our
generated Apache Thrift objects. In the next post, I will show how to handle the custom deserialization of JSON into Apache Thrift based objects.