02.02.2013
Implementierung von MessageBodyReader und MessageBodyWriter - JAX-RS 2.0
In diesem Teil des Tutorials werden komplexere Objekte vom Type Item
über die Client-API abgefragt. Dazu wird die REST-Schnittstelle erweitert und es werden MessageBodyReader
und MessageBodyWriter
erstellt, um die Objekte für die Formate text/plain
, application/json
und application/xml
zu übertragen.
Auf der Abbildung sieht man, wie der Client einen Request an den Service /itemService/item des Servers absetzt. Bevor die Response mit dem Item an den Client gesendet wird, muss der ItemMessageBodyWriter durchlaufen werden, damit das Item als Stream übertragen wird. Auf Seite des Client wird der Item-Stream von einem ItemMessageBodyReader geparsed, so dass der Client mit Objekten vom Typ Item arbeiten kann.
Wie schon angesprochen, sollen komplexere Objekte über die REST-Schnittstelle übertragen werden. Deshalb legen wir eine Klasse Item
an, die eine id
, einen name
und eine description
jeweils vom Typ String
haben.
Die folgende Klasse wird in dem Projekt JAXRSLibrary unter dem Namen Item
abgespeichert. Die Klasse wird in dem Library-Projekt abgelegt, weil sowohl Client als auch Server auf die Klasse zugreifen.
package org.hameister.itemservice;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Item implements Serializable {
private String id;
private String name;
private String description;
public Item() {
}
public Item(String id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
Bei der Klasse Item
handelt es sich um ein einfaches Pojo oder eine Bean, die mit der Annotation @XmlRootElement
versehen ist, damit sie als XML-Element übetragen werden kann.
Nachdem die Klasse Item
angelegt wurde, erweitern wir die REST-Schnittstelle, um Zugriff auf Objekte von Typ Item
zu haben.
Dazu wird einfach eine neue Methode item
eingefügt, die als Rückgabewert den Typ Item
hat. Mit der Annotation @Producer
werden die Rückgabeformate festgelegt. In dem Beispiel werden Objekte zurückgeliefert, die sich nur durch den Zeitstempel in der Description unterscheiden.
package org.hameister.itemservice;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
@Path("/itemservice")
public class ItemService {
...
@GET
@Path("item")
@Produces({"application/xml, text/plain, application/json"})
public Item item() {
return new Item("id1", "Item 1", "Item Description " + System.currentTimeMillis());
}
}
Würden wir die Methode nun mit der Client-API mit folgendem Kommando aufrufen,
String server = "http://localhost:8080/JAXRSServer/webresources";
Item returnValueItem = client.target(server+"/itemservice/item").request("application/json").get(Item.class);
dann würde auf Client-Seite die Meldung Exception in thread "main" javax.ws.rs.InternalServerErrorException
und im Server-Log folgende Fehlermeldung erscheinen:
org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException:
MessageBodyWriter not found for media type=application/json,
type=class org.hameister.itemservice.Item,
genericType=class org.hameister.itemservice.Item
Der Grund dafür ist, dass der MessageBodyWriter
für die Klasse Item
und das Format JSON
fehlt. Für einen Aufruf mit dem Format text/plain
würde das gleiche passieren. Nur für application/xml
wird ein Ergebnis zurückgeliefert. Grund dafür ist die Annotation @XmlRootElement
in der Klasse Item
.
Die Lösung für das Problem mit text/plain
und application/json
ist das Erstellen eines MessageBodyWriter
für Item
s.
Dazu wird eine Klasse ItemMessageBodyWriter
erstellt, die das Interface MessageBodyWriter
implementiert und in dem Projekt JAXRSLibrary
abgelegt wird.
@Provider
@Produces({"text/plain", "application/json"})
public class ItemMessageBodyWriter implements MessageBodyWriter<Item> {
@Override
public boolean isWriteable(Class<?> type, Type type1, Annotation[] antns, MediaType mt) {
return true;
}
@Override
public long getSize(Item t, Class<?> type, Type type1, Annotation[] antns, MediaType mt) {
return -1;
}
@Override
public void writeTo(Item t, Class<?> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, Object> mm, OutputStream out) throws IOException, WebApplicationException {
if (mt.getType().equals("application") && mt.getSubtype().equals("json")) {
StringBuffer buffer = new StringBuffer();
buffer = buffer.append("{");
buffer = buffer.append("\"id\":\"").append(t.getId()).append("\",");
buffer = buffer.append("\"name\":\"").append(t.getName()).append("\",");
buffer = buffer.append("\"description\":\"").append(t.getDescription()).append("\"");
buffer = buffer.append("}");
try (PrintStream printStream = new PrintStream(out)) {
printStream.print(buffer.toString());
}
return;
} else if(mt.getType().equals("text") && mt.getSubtype().equals("plain")) {
try (PrintStream printStream = new PrintStream(out)) {
printStream.print((t.getId()+"/"+t.getName()+"/"+t.getDescription()));
}
return;
}
throw new UnsupportedOperationException("Not supported MediaType: " + mt);
}
}
Wichtig ist die Annotation @Provider
oberhalb der Klasse und die Annotation @Produces
, die die Formate angibt, die der ItemMessageBodyWriter
zurückliefern kann.
In der Methode writeTo
werden die Werte des Parameters Item t
ausgelesen und in den OutputStream
geschrieben. Im Fall von application/json
wird ein einfacher JSON-String zusammengebaut und für das Format text/plain
ein selbst definiertes Text-Format.
In dem Beispiel wird der JSON-String "per Hand" zusammengesetzt. Diese Variante wurde hier nur gewählt, um zu zeigen, dass es funktioniert. Weiter unten wird das Framework Jackson benutzt, um JSON zu verarbeiten.
Für das Format text/plain
wurde ein eigenes Format definiert. Das soll als Beispiel dafür dienen, dass in den OutputSteam prinzipiell alles geschrieben werden kann. Wichtig dabei ist, dass es beim Empänger wieder gelesen werden kann. Aber dazu kommen wir gleich.
Nachdem die Klasse auf dem Server deployed ist und die Abfrage erneut gestartet wird, tritt ein anderes Problem auf. Im Client-Log erscheint folgende Fehlermeldung:
Caused by: org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException:
MessageBodyReader not found for media type=application/json,
type=class org.hameister.itemservice.Item,
genericType=class org.hameister.itemservice.Item.
Diesmal ist es so, dass der Server den Request richtig verarbeitet und die Response verschickt, aber die Client-API die Antwort nicht verarbeiten kann, weil kein MessageBodyReader
für die Klasse Item
vorhanden ist. Deshalb erstellen wir einen ItemMessageBodyReader
im Projekt JAXRSLibrary
der folgendermaßen aussieht:
package org.hameister.itemservice;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Scanner;
import javax.ws.rs.Consumes;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.Provider;
import org.codehaus.jackson.map.ObjectMapper;
@Provider
@Consumes({"text/plain", "application/json"})
public class ItemMessageBodyReader implements MessageBodyReader<Item> {
@Override
public boolean isReadable(Class<?> type, Type type1, Annotation[] antns, MediaType mt) {
return true;
}
@Override
public Item readFrom(Class<Item> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, String> mm, InputStream in) throws IOException, WebApplicationException {
if (mt.getType().equals("application") && mt.getSubtype().equals("json")) {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(in, Item.class);
} else if (mt.getType().equals("text") && mt.getSubtype().equals("plain")) {
Scanner scanner = new Scanner(in).useDelimiter("/");
String id = scanner.next();
String name = scanner.next();
String description = scanner.next();
Item item = new Item(id,name,description);
return item;
}
throw new UnsupportedOperationException("Not supported MediaType: " + mt);
}
}
Ähnlich wie beim ItemMessageBodyWriter
, wird beim ItemMessageBodyReader
das Interface MessageBodyReader
mit zwei Methoden implementiert. Bei der readFrom
-Methode wird wieder zwischen application/json
und text/plain
unterschieden. Zum Verarbeiten von JSON wird dieses mal der ObjectMapper
des Jackson-Frameworks verwendet. Einen Parser "per Hand" oder mit ANTLR zu erstellen, wäre natürlich auch möglich. Beim Interpretieren der Text-Antwort wird ein Scanner
aus dem Java-Util-Package verwendet.
Zum Testen der Methode erweitern wir die Testklasse JAXRESTClientTest
im Projekt JAXRSClient
und legen drei weitere JUnit-Tests an, die die Methode mit ihren drei Rückgabeformaten aufrufen.
Wichtig ist, dass die Klasse ItemMessageBodyReader.class
beim Client mit register
angemeldet wird, so dass die Response vom Server verarbeitet werden kann.
public class JAXRESTClientTest {
Client client;
private static final String SERVER = "http://localhost:8080/JAXRSServer/webresources";
public JAXRESTClientTest() {
}
@Before
public void setUp() {
client = ClientFactory.newClient();
}
@After
public void tearDown() {
client.close();
}
...
@Test
public void itemJSON() {
client.register(ItemMessageBodyReader.class);
Item returnValueItem =
client.target(SERVER + "/itemservice/item").request("application/json").get(Item.class);
Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO,
"Return value 'itemstring':{0} {1}", new Object[]{returnValueItem.getName(), returnValueItem.getDescription()});
Assert.assertEquals("Changed_item_Item 1", returnValueItem.getName());
Assert.assertTrue("Wrong description text in Item: " + returnValueItem.getDescription(),
returnValueItem.getDescription().startsWith("Item Description"));
}
@Test
public void itemXML() {
client.register(ItemMessageBodyReader.class);
Item returnValueItem =
client.target(SERVER + "/itemservice/item").request("application/xml").get(Item.class);
Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO,
"Return value 'itemstring':{0} {1}", new Object[]{returnValueItem.getName(), returnValueItem.getDescription()});
Assert.assertEquals("Changed_item_Item 1", returnValueItem.getName());
Assert.assertTrue("Wrong description text in Item: " + returnValueItem.getDescription(),
returnValueItem.getDescription().startsWith("Item Description"));
}
@Test
public void itemTextPlain() {
client.register(ItemMessageBodyReader.class);
Item returnValueItem =
client.target(SERVER + "/itemservice/item").request("text/plain").get(Item.class);
Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO,
"Return value 'itemstring':{0} {1}", new Object[]{returnValueItem.getName(), returnValueItem.getDescription()});
Assert.assertEquals("Changed_item_Item 1", returnValueItem.getName());
Assert.assertTrue("Wrong description text in Item: " + returnValueItem.getDescription(),
returnValueItem.getDescription().startsWith("Item Description"));
}
}
Im nächsten Teil dieses Tutorials wird der REST-Service so erweitert, dass auch Listen (List
) mit Item
s übertragen werden können.