I found some strange behaviour that I cannot understand.
I have tested 4 similar examples:
1
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response produce() {
List<Book> books = Arrays
.asList(new Book[] {
new Book("aaa", "AAA", "12345"),
new Book("bbb", "BBB", "09876")
});
return Response.ok(books).build();
}
2
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Book> produce() {
List<Book> books = Arrays
.asList(new Book[] {
new Book("aaa", "AAA", "12345"),
new Book("bbb", "BBB", "09876")
});
return books;
}
3
@GET
@Produces(MediaType.APPLICATION_XML)
public List<Book> produce() {
List<Book> books = Arrays
.asList(new Book[] {
new Book("aaa", "AAA", "12345"),
new Book("bbb", "BBB", "09876")
});
return books;
}
4
@GET
@Produces(MediaType.APPLICATION_XML)
public Response produce() {
List<Book> books = Arrays
.asList(new Book[] {
new Book("aaa", "AAA", "12345"),
new Book("bbb", "BBB", "09876")
});
return Response.ok(books).build();
}
Everything works in #1, #2, #3 but 4th example throws:
Could not find MessageBodyWriter for response object of type: java.util.Arrays$ArrayList of media type: application/xml.
I run it on Wildfly 9 and I wonder if it is related to RestEasy or JaxRS in general? I know that I can fix it by wrapping collection in GenericEntity, but I don't understand this inconsistent behaviour.
The problem is the missing of type information. This is required for JAXB, which handles the XML serialization.
1 and 2 works because Jackson is being used for JSON, and it generally doesn't need to know type information as it just introspects properties.
3 works because type information is known through the method return type.
4 doesn't work because there is no type information. It's is erased by type erasure. That's where GenericEntity
comes to the rescue. It stores the type information.
Normally type erasure removes generic type information such that a
Response
instance that contains, e.g., an entity of typeList<String>
appears to contain a rawList<?>
at runtime. When the generic type is required to select a suitableMessageBodyWriter
, this class may be used to wrap the entity and capture its generic type.
swch, i create an example of MessageBodyWriter for Collections (Set, List etc) Also, some can analyze Annotation for xml root name, gzip, and cache... have fun!
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.namespace.QName;
@Provider
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class CollectionProvider
implements MessageBodyWriter<Collection>
{
static final byte[] COMMA = ",".getBytes();
static final byte[] ARRAY_START = "[".getBytes();
static final byte[] ARRAY_END = "]".getBytes();
static final byte[] NULL = "null".getBytes();
static final QName OBJECT = new QName(null, "object");
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType)
{
if (!Collection.class.isAssignableFrom(type))
return false;
if (genericType == null || !(genericType instanceof ParameterizedType))
return false;
Type[] args = ((ParameterizedType) genericType).getActualTypeArguments();
for (Type arg: args)
{
if (arg instanceof TypeVariable) // can't mashal Collection<T>
return false;
if (!(arg instanceof Class))
return false;
}
String type = mediaType.getType().toLowerCase();
String subtype = mediaType.getSubtype().toLowerCase();
return type.equals("application") &&
(subtype.startsWith("json") || subtype.startsWith("xml"));
}
@Override
public long getSize(Collection list, Class<?> c, Type type, Annotation[] annotation, MediaType mediaType)
{
return -1;
}
@Override
public void writeTo(Collection list, Class<?> c, Type type, Annotation[] annotation, MediaType mediaType,
MultivaluedMap<String, Object> multivaluedMap, OutputStream outputStream)
throws IOException, WebApplicationException
{
try
{
boolean json = mediaType.getSubtype().toLowerCase().startsWith("json");
if (list.isEmpty())
{
if(json)
{
outputStream.write(ARRAY_START);
outputStream.write(ARRAY_END);
}
else
outputStream.write("<list/>".getBytes());
}
else
{
Set<Class> classes = new HashSet<Class>();
for (Type clazz: ((ParameterizedType) type).getActualTypeArguments())
classes.add((Class) clazz);
JAXBContext jc = JAXBContext.newInstance(classes.toArray(new Class[classes.size()]));
Marshaller m = jc.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, false);
if(json)
{
m.setProperty("eclipselink.media-type", MediaType.APPLICATION_JSON);
m.setProperty("eclipselink.json.include-root", false);
}
if(json)
outputStream.write(ARRAY_START);
else
outputStream.write("<list>".getBytes());
for (Iterator it = list.iterator(); it.hasNext();)
{
Object object = it.next();
if(json)
{
if (object == null) // Allow nullabale value collections
outputStream.write(NULL);
else
m.marshal(new JAXBElement(OBJECT, object.getClass(), object), outputStream);
if (it.hasNext())
outputStream.write(COMMA);
}
else if (object != null) // null in xml? xm...
m.marshal(object, outputStream); // <-- requered XmlRoot annotation
}
if(json)
outputStream.write(ARRAY_END);
else
outputStream.write("</list>".getBytes());
}
}
catch (JAXBException e)
{
throw new IOException(e);
}
}
}
来源:https://stackoverflow.com/questions/35039294/produces-collection-in-jaxrs-resteasy