@Produces collection in JAXRS / RestEasy

2019-01-28 05:53发布

问题:

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.

回答1:

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.

GenericEntity

Normally type erasure removes generic type information such that a Response instance that contains, e.g., an entity of type List<String> appears to contain a raw List<?> at runtime. When the generic type is required to select a suitable MessageBodyWriter, this class may be used to wrap the entity and capture its generic type.



回答2:

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);
    }
  }
}