INTRODUCTION
In current post I am going to explain how to implement and register a custom Spring MVC HttpMessageConverter object. Specifically a converter that binds objects to Yaml protocol. As starting point I am going to use the Rest application I implemented in previous post. That application is a simple Restful application where XML and JSON (Spring MVC already support them) are used. Because Spring MVC does not implement YamlMessageConverter, I am going to explain how to transform previous application from supporting XML and JSON to support Yaml.
Yaml is a human-readable data serialization format that takes concepts from programming languages such as C, Perl, and Python, and ideas from XML and the data format of electronic mail (RFC 2822).
SnakeYAML is a YAML parser and emitter for the Java programming language, and will be used to implement our message converter.
DESIGN
Let's start with a UML class diagram of HttpMessageConverter that are going to be implemented.
HttpMessageConverter is a base interface that must be implemented. It is a strategy interface that specifies methods to convert objects from and to HTTP requests and responses. AbstractHttpMessageConverter is the abstract base class for most HttpMessageConverter implementation (both provided by springframework), and is our base class.
First developed class is an abstract class called AbstractYamlHttpMessageConverter. This class is responsible of generic operations that "should" be required by all Yaml parsers/emitters. In my case it deals with charset options, and transforms HttpInputMessage and HttpOutputMessage to java.io.InputStreamWriter and java.io.OutputStreamWriter. In fact it acts as a Template Pattern to read and write operations (readInternal and writeInternal methods).
Next abstract class is AbstractSnakeYamlHttpMessageConverter. This class is a base class for HttpMessageConverters that use SnakeYaml as Yaml binder. This class gets an instance of Yaml class (central class of SnakeYaml project).
And finally JavaBeanSnakeYamlHttpMessageConverter. This class uses SnakeYaml JavaBeans features for converting from object to Yaml and viceversa. SnakeYaml does not support annotations like Jackson (JSON) or Jaxb (XML), but if some day this feature is implemented, we should create a new class extending from AbstractSnakeYamlHttpMessageConverter with required change.
CODE
First of all is adding a new dependency to pom. In this case SnakeYaml.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<dependency> | |
<groupId>org.yaml</groupId> | |
<artifactId>snakeyaml</artifactId> | |
<version>1.9</version> | |
</dependency> |
Then three classes previously exposed should be developed.
First class is a generic Yaml converter where we are setting accepted media type, in this case application/yaml, and creating a Reader and Writer with required Charset. We are leaving to children classes the responsibility of implementing read and write code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public abstract class AbstractYamlHttpMessageConverter<T> extends | |
AbstractHttpMessageConverter<T> { | |
public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); | |
private final List<Charset> availableCharsets; | |
private boolean writeAcceptCharset = true; | |
protected AbstractYamlHttpMessageConverter() { | |
super(new MediaType("application", "yaml")); | |
this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values()); | |
} | |
public void setWriteAcceptCharset(boolean writeAcceptCharset) { | |
this.writeAcceptCharset = writeAcceptCharset; | |
} | |
@Override | |
protected T readInternal(Class<? extends T> clazz, | |
HttpInputMessage inputMessage) throws IOException, | |
HttpMessageNotReadableException { | |
return readFromSource(clazz, inputMessage.getHeaders(), new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders()))); | |
} | |
@Override | |
protected void writeInternal(T t, HttpOutputMessage outputMessage) | |
throws IOException, HttpMessageNotWritableException { | |
if (writeAcceptCharset) { | |
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); | |
} | |
writeToResult(t, outputMessage.getHeaders(), new OutputStreamWriter(outputMessage.getBody(), getCharset(outputMessage.getHeaders()))); | |
} | |
protected List<Charset> getAcceptedCharsets() { | |
return this.availableCharsets; | |
} | |
private Charset getCharset(HttpHeaders headers) { | |
MediaType contentType = headers.getContentType(); | |
return contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET; | |
} | |
protected abstract T readFromSource(Class<? extends T> clazz, HttpHeaders headers, Reader source) | |
throws IOException; | |
protected abstract void writeToResult(T t, HttpHeaders headers, Writer result) | |
throws IOException; | |
} |
Next class is specific of API that will be used to bind classes to messages, in this case SnakeYaml. This class will be responsible of creating an instance of Yaml class (from SnakeYaml). As is warned in http://snakeyamlrepo.appspot.com/releases/1.9/site/apidocs/index.html each thread must have its own instance; for this reason a ThreadLocal is used to carry out this restriction.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public abstract class AbstractSnakeYamlHttpMessageConverter<T> extends | |
AbstractYamlHttpMessageConverter<T> { | |
//SnakeYaml requires one instance for each Thread so ThreadLocal is used. | |
private ThreadLocal<Yaml> yamlInterfaces = new ThreadLocal<Yaml>(); | |
public AbstractSnakeYamlHttpMessageConverter() { | |
super(); | |
} | |
protected final Yaml getSnakeYamlInterface() { | |
Yaml yaml = this.yamlInterfaces.get(); | |
if(yaml == null) { | |
yaml = new Yaml(); | |
yamlInterfaces.set(yaml); | |
} | |
return yaml; | |
} | |
} |
Final class is an implementation of read/write methods using SnakeYaml. As I have explained previously this class has a meaning because allows us changing SnakeYaml binding strategy (for example to annotation approach) and only worries to rewrite read/write operations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class JavaBeanSnakeYamlHttpMessageConverter extends | |
AbstractSnakeYamlHttpMessageConverter<Object> { | |
public JavaBeanSnakeYamlHttpMessageConverter() { | |
super(); | |
} | |
@Override | |
protected Object readFromSource(Class<?> clazz, HttpHeaders headers, | |
Reader source) throws IOException { | |
Yaml beanLoader = getSnakeYamlInterface(); | |
return beanLoader.loadAs(source, clazz); | |
} | |
@Override | |
protected void writeToResult(Object t, HttpHeaders headers, Writer result) | |
throws IOException { | |
Yaml beanDumpper = getSnakeYamlInterface(); | |
String yamlMessage = beanDumpper.dumpAsMap(t); | |
FileCopyUtils.copy(yamlMessage, result); | |
} | |
@Override | |
public boolean canRead(Class<?> clazz, MediaType mediaType) { | |
return canRead(mediaType); | |
} | |
@Override | |
public boolean canWrite(Class<?> clazz, MediaType mediaType) { | |
return canWrite(mediaType); | |
} | |
@Override | |
protected boolean supports(Class<?> clazz) { | |
// should not be called, since we override canRead/Write | |
throw new UnsupportedOperationException(); | |
} | |
} |
Now it is time to register created message converter to AnnotationMethodHandlerAdapter. First thing you should do is not to use <mvc:annotation-driven>. This annotation registers default message converters and you are not able to modify them. So first step is comment or remove annotation-driven. Next step is declare DefaultAnnotationHandlerMapping bean and AnnotationMethodHandlerAdapter which registers http message converters. In our case only Yaml http message converter is added.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- Enables the Spring MVC @Controller programming model --> | |
<!-- <annotation-driven /> --> | |
<beans:bean id="handlerMapping" class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" /> | |
<beans:bean | |
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> | |
<beans:property name="messageConverters"> | |
<util:list id="beanList"> | |
<beans:bean class="org.springframework.rest.JavaBeanSnakeYamlHttpMessageConverter"></beans:bean> | |
</util:list> | |
</beans:property> | |
</beans:bean> |
RUNNING
And now you can try application. Deploy it on a server, and using for example Rest Client, try http://localhost:8080/RestServer/characters/1 and you will receive a response like:
If you want you can use POST instead of GET to insert a new Character to our Map.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Controller | |
public class HomeController { | |
private static final Map<Integer, Character> characters = new HashMap<Integer, Character>(); | |
static { | |
characters.put(1, new Character(1, "Totoro", false)); | |
characters.put(2, new Character(2, "Satsuki Kusakabe", true)); | |
characters.put(3, new Character(3, "Therru", false)); | |
} | |
@RequestMapping(value = "/characters/{characterId}", method = RequestMethod.POST) | |
@ResponseBody | |
public Character addCharacter(@RequestBody Character character, @PathVariable int characterId) { | |
characters.put(characterId, character); | |
return character; | |
} | |
@RequestMapping(value = "/characters/{characterId}", method = RequestMethod.GET) | |
@ResponseBody | |
public Character findCharacter(@PathVariable int characterId) { | |
return characters.get(characterId); | |
} | |
} |
As you can see developing a Spring MVC HTTP Message Converter is so easy, in fact you must implement two basic operations, when a resource can be read or written and resource conversion.
Hope you found this post useful.
0 comentarios:
Publicar un comentario