JAX-RS: как автоматически сериализовать коллекцию при возврате объекта Response? - программирование
Подтвердить что ты не робот

JAX-RS: как автоматически сериализовать коллекцию при возврате объекта Response?

У меня есть класс сотрудников, аннотированный JAXB:

@XmlRootElement(name = "employee")
public class Employee {

    private Integer id;
    private String name;

    ...

    @XmlElement(name = "id")
    public int getId() {
        return this.id;
    }

    ... // setters and getters for name, equals, hashCode, toString
}

И объект ресурса JAX-RS (я использую Джерси 1.12)

@GET
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Path("/")
public List<Employee> findEmployees(
    @QueryParam("name") String name, 
    @QueryParam("page") String pageNumber,
    @QueryParam("pageSize") String pageSize) {
    ...
    List<Employee> employees = employeeService.findEmployees(...);

    return employees;
}

Эта конечная точка работает нормально. Я получаю

<employees>
  <employee>
    <id>2</id>
    <name>Ana</name>
  </employee>
</employees>

Однако, если я изменю метод, чтобы вернуть объект Response, и поместите список сотрудников в тело ответа, например:

@GET
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Path("/")
public Response findEmployees(
    @QueryParam("name") String name, 
    @QueryParam("page") String pageNumber,
    @QueryParam("pageSize") String pageSize) {
    ...
    List<Employee> employees = employeeService.findEmployees(...);

    return Response.ok().entity(employees).build();
}

конечная точка приводит к HTTP 500 из-за следующего исключения:

javax.ws.rs.WebApplicationException: com.sun.jersey.api.MessageException: A message body writer for Java class java.util.ArrayList, and Java type class java.util.ArrayList, and MIME media type application/xml was not found

В первом случае JAX-RS, очевидно, устроил для правильного писателя сообщения, чтобы он ударил, возвращая коллекцию. Кажется несколько несогласованным, что это не происходит, когда коллекция помещается в тело сущности. Какой подход я могу предпринять, чтобы получить автоматическую сериализацию JAXB списка при возврате ответа?

Я знаю, что могу

  • Просто верните список из метода ресурсов
  • Создайте отдельный класс EmployeeList

но задавался вопросом, есть ли хороший способ использовать объект Response и получить список для сериализации без создания моего собственного класса-оболочки.

4b9b3361

Ответ 2

Вы можете использовать GenericEntity для отправки коллекции в Response. Вы должны были включить соответствующую библиотеку маршала/немаршала, такую ​​как moxy или jaxrs-jackson.

Ниже приведен код:

    @GET
    @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    @Path("/")
    public Response findEmployees(
        @QueryParam("name") String name, 
        @QueryParam("page") String pageNumber,
        @QueryParam("pageSize") String pageSize) {
        ...
        List<Employee> employees = employeeService.findEmployees(...);

        GenericEntity<List<Employee>> entity = new GenericEntity<List<Employee>>(Lists.newArrayList(employees))

        return Response.ok().entity(entity).build();
    }

Ответ 3

Я решил эту проблему, расширив класс JacksonJsonProvider по умолчанию, в частности метод writeTo.

Анализируя исходный код этого класса, я нашел блок, где фактический тип создается путем отражения, поэтому я изменил исходный код, как показано ниже:

public void writeTo(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String,Object> httpHeaders, OutputStream entityStream) throws IOException {
    /* 27-Feb-2009, tatu: Where can we find desired encoding? Within
     *   HTTP headers?
     */
    ObjectMapper mapper = locateMapper(type, mediaType);
    JsonEncoding enc = findEncoding(mediaType, httpHeaders);
    JsonGenerator jg = mapper.getJsonFactory().createJsonGenerator(entityStream, enc);
    jg.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);

    // Want indentation?
    if (mapper.getSerializationConfig().isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) {
        jg.useDefaultPrettyPrinter();
    }
    // 04-Mar-2010, tatu: How about type we were given? (if any)
    JavaType rootType = null;

    if (genericType != null && value != null) {
        /* 10-Jan-2011, tatu: as per [JACKSON-456], it not safe to just force root
         *    type since it prevents polymorphic type serialization. Since we really
         *    just need this for generics, let only use generic type if it truly
         *    generic.
         */
        if (genericType.getClass() != Class.class) { // generic types are other impls of 'java.lang.reflect.Type'
            /* This is still not exactly right; should root type be further
             * specialized with 'value.getClass()'? Let see how well this works before
             * trying to come up with more complete solution.
             */

            //**where the magic happens**
            //if the type to instantiate implements collection interface (List, Set and so on...)
            //Java applies Type erasure from Generic: e.g. List<BaseRealEstate> is seen as List<?> and so List<Object>, so Jackson cannot determine @JsonTypeInfo correctly
            //so, in this case we must determine at runtime the right object type to set
            if(Collection.class.isAssignableFrom(type))
            {
                Collection<?> converted = (Collection<?>) type.cast(value);
                Class<?> elementClass = Object.class;
                if(converted.size() > 0)
                    elementClass = converted.iterator().next().getClass();
                //Tell the mapper to create a collection of type passed as parameter (List, Set and so on..), containing objects determined at runtime with the previous instruction
                rootType = mapper.getTypeFactory().constructCollectionType((Class<? extends Collection<?>>)type, elementClass);
            }
            else
                rootType = mapper.getTypeFactory().constructType(genericType);
            /* 26-Feb-2011, tatu: To help with [JACKSON-518], we better recognize cases where
             *    type degenerates back into "Object.class" (as is the case with plain TypeVariable,
             *    for example), and not use that.
             */
            if (rootType.getRawClass() == Object.class) {
                rootType = null;
            }
        }
    }
    // [JACKSON-578]: Allow use of @JsonView in resource methods.
    Class<?> viewToUse = null;
    if (annotations != null && annotations.length > 0) {
        viewToUse = _findView(mapper, annotations);
    }
    if (viewToUse != null) {
        // TODO: change to use 'writerWithType' for 2.0 (1.9 could use, but let defer)
        ObjectWriter viewWriter = mapper.viewWriter(viewToUse);
        // [JACKSON-245] Allow automatic JSONP wrapping
        if (_jsonpFunctionName != null) {
            viewWriter.writeValue(jg, new JSONPObject(this._jsonpFunctionName, value, rootType));
        } else if (rootType != null) {
            // TODO: change to use 'writerWithType' for 2.0 (1.9 could use, but let defer)
            mapper.typedWriter(rootType).withView(viewToUse).writeValue(jg, value);
        } else {
            viewWriter.writeValue(jg, value);
        }
    } else {
        // [JACKSON-245] Allow automatic JSONP wrapping
        if (_jsonpFunctionName != null) {
            mapper.writeValue(jg, new JSONPObject(this._jsonpFunctionName, value, rootType));
        } else if (rootType != null) {
            // TODO: change to use 'writerWithType' for 2.0 (1.9 could use, but let defer)
            mapper.typedWriter(rootType).writeValue(jg, value);
        } else {
            mapper.writeValue(jg, value);
        }
    }
}