Spring MVC с JAXB, ответ списка на основе универсального класса

Я работаю с Spring 4 вместе с Spring MVC.

У меня есть следующий класс POJO

@XmlRootElement(name="person")
@XmlType(propOrder = {"id",...})
public class Person implements Serializable {

    …

    @Id
    @XmlElement
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }

    …
}

Если я хочу вернуть список Person через Spring MVC в формате XML, у меня есть следующий обработчик

@RequestMapping(value="/getxmlpersons/generic", 
        method=RequestMethod.GET, 
        produces=MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public JaxbGenericList<Person> getXMLPersonsGeneric(){
    logger.info("getXMLPersonsGeneric - getxmlpersonsgeneric");
    List<Person> persons = new ArrayList<>();
    persons.add(...);
    …more

    JaxbGenericList<Person> jaxbGenericList = new JaxbGenericList<>(persons);

    return jaxbGenericList;
}

Где находится JaxbGenericList (объявление класса на основе Generics)

@XmlRootElement(name="list")
public class JaxbGenericList<T> {

    private List<T> list;

    public JaxbGenericList(){}

    public JaxbGenericList(List<T> list){
        this.list=list;
    }

    @XmlElement(name="item")
    public List<T> getList(){
        return list;
    }
}

Когда я выполняю URL-адрес, я получаю следующую трассировку стека ошибок

org.springframework.http.converter.HttpMessageNotWritableException: Could not marshal [com.manuel.jordan.jaxb.support.JaxbGenericList@31f8809e]: null; nested exception is javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.internal.SAXException2: class com.manuel.jordan.domain.Person nor any of its super class is known to this context.
javax.xml.bind.JAXBException: class com.manuel.jordan.domain.Person nor any of its super class is known to this context.]

Но Если у меня есть это (объявление класса не основано на Generics):

@XmlRootElement(name="list")
public class JaxbPersonList {

    private List<Person> list;

    public JaxbPersonList(){}

    public JaxbPersonList(List<Person> list){
        this.list=list;
    }

    @XmlElement(name="item")
    public List<Person> getList(){
        return list;
    }
}

и другой метод handler, конечно, следующим образом

@RequestMapping(value="/getxmlpersons/specific", 
        method=RequestMethod.GET, 
        produces=MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public JaxbPersonList getXMLPersons(){
    logger.info("getXMLPersonsSpecific - getxmlpersonsspecific");
    List<Person> persons = new ArrayList<>();
    persons.add(...);
    …more 

    JaxbPersonList jaxbPersonList = new JaxbPersonList(persons);

    return jaxbPersonList;
}

Все работает нормально:

Вопрос, какие дополнительные настройки мне нужны, чтобы спокойно работать только с JaxbGenericList

Дополнение 1

Я не настраивал bean-компонент marshaller, поэтому я думаю, что Spring за кулисами предоставил его. Теперь, согласно вашему ответу, я добавил следующее:

@Bean
public Jaxb2Marshaller marshaller() {
    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    marshaller.setClassesToBeBound(JaxbGenericList.class, Person.class);
    Map<String, Object> props = new HashMap<>();
    props.put(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    marshaller.setMarshallerProperties(props);
    return marshaller;
}

Кажется, чего-то не хватает, потому что я получаю «снова»:

org.springframework.http.converter.HttpMessageNotWritableException: Could not marshal [com.manuel.jordan.jaxb.support.JaxbGenericList@6f763e89]: null; nested exception is javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.internal.SAXException2: class com.manuel.jordan.domain.Person nor any of its super class is known to this context.
javax.xml.bind.JAXBException: class com.manuel.jordan.domain.Person nor any of its super class is known to this context.]

Вы пробовали в веб-среде? Кажется, мне нужно сообщить Spring MVC об использовании этого marshaller, я сказал кажется, потому что другой URL-адрес, работающий с не коллекциями POJO/XML, пока работает нормально.

Я работаю с Spring 4.0.5, а веб-среда настроена через Java Config.

Дополнение второе

Предложенное вами последнее издание работает

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(marshallingMessageConverter());
}

@Bean
public MarshallingHttpMessageConverter marshallingMessageConverter() {
    MarshallingHttpMessageConverter converter = new MarshallingHttpMessageConverter();
    converter.setMarshaller(jaxbMarshaller());
    converter.setUnmarshaller(jaxbMarshaller());
    return converter;
}

Но теперь мой XML не является общим, и код JSON не работает.

Для моего другого URL-адреса XML http://localhost:8080/spring-utility/person/getxmlpersons/specific

HTTP Status 406 -

type Status report

message

description The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.

Исправлено добавление или обновление:

от:

marshaller.setClassesToBeBound(JaxbGenericList.class, Person.class);

to:

marshaller.setClassesToBeBound(JaxbGenericList.class, Person.class, JaxbPersonList.class);

Но для JSON снова другой URL http://localhost:8080/spring-utility/person/getjsonperson

HTTP Status 406 -

type Status report

message

description The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.

Кажется, мне нужно добавить конвертер для JSON (ранее я не определял), я использовал значения по умолчанию Spring, не могли бы вы мне помочь?


person Manuel Jordan    schedule 19.09.2014    source источник


Ответы (1)


Один из способов, которым это может произойти в Spring, — это если у вас неправильно настроен маршаллер. Возьмем, к примеру, этот автономный:

Тестовое приложение:

public class TestApplication {
    public static void main(String[] args) throws Exception {
        AbstractApplicationContext context
                = new AnnotationConfigApplicationContext(AppConfig.class);

        Marshaller marshaller = context.getBean(Marshaller.class);
        GenericWrapper<Person> personListWrapper = new GenericWrapper<>();
        Person person = new Person();
        person.setId(1);
        personListWrapper.getItems().add(person);
        marshaller.marshal(personListWrapper, new StreamResult(System.out) );
        context.close();
    }
} 

AppConfig (обратите внимание на setClassesToBeBound)

@Configuration
public class AppConfig {
    @Bean
    public Jaxb2Marshaller marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setClassesToBeBound(GenericWrapper.class);
        Map<String, Object> props = new HashMap<>();
        props.put(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.setMarshallerProperties(props);
        return marshaller;
    }
}

Домен

@XmlRootElement
public class Person {

    protected Integer id;

    @XmlElement
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
}
@XmlRootElement
public class GenericWrapper<T> {

    protected List<T> items;

    public GenericWrapper() {}
    public GenericWrapper(List<T> items) {
        this.items = items;
    }

    @XmlElement(name = "item")
    public List<T> getItems() {
        if (items == null) {
            items = new ArrayList<>();
        }
        return items;
    }
}

В результате запуска вышеуказанного приложения его не удастся маршалировать по той же причине исключения.

Caused by: javax.xml.bind.JAXBException: class com.stackoverflow.Person nor any of its super class is known to this context.

Причина, если вы посмотрите на AppConfig, заключается в методе setClassesToBeBound. Он включает только класс GenericWrapper. Так почему это проблема?

Давайте посмотрим на JAXB под капотом:

Мы взаимодействуем с JAXB через файл JAXBContext. Обычно можно просто создать контекст с элементом верхнего уровня, например

JAXBContext context = JAXBContext.newInstance(GenericWrapper.class);

JAXB вытянет в контекст все классы, связанные с классом. Вот почему List<Person> работает. Person.class явно связан.

Но в случае с GenericWrapper<T> класс Person нигде не связан с GenericWrapper. Но если мы явно поместим класс Person в контекст, он будет найден, и мы сможем использовать дженерики.

JAXBContext context 
                = JAXBContext.newInstance(GenericWrapper.class, Person.class);

При этом под капотом Jaxb2Marshaller используется тот же контекст. Возвращаясь к AppConfig, вы можете увидеть marshaller.setClassesToBeBound(GenericWrapper.class);, который в конечном итоге совпадает с первым созданием JAXBContext выше. Если мы добавим Person.class, то получим ту же конфигурацию, что и второе творение JAXBContext.

marshaller.setClassesToBeBound(GenericWrapper.class, Person.class);

Теперь снова запустите тестовое приложение, и оно получит желаемый результат.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<genericWrapper>
    <item>
        <id>1</id>
    </item>
</genericWrapper>

Все сказанное...

Я не знаю, как вы настроили свою конфигурацию marshaller, и есть ли у фреймворка настройки по умолчанию. Но вам нужно заполучить маршаллер и настроить его для поиска классов одним из трех способов:

  • packagesToScan.
  • contextPath
  • или classesToBeBound (как я сделал выше)

Это можно сделать с помощью конфигурации Java или конфигурации xml.



ОБНОВЛЕНИЕ (с использованием веб-сайта Spring)

Таким образом, кажется, что в веб-среде Spring, если вы не укажете конвертер сообщений http, Spring будет использовать Jaxb2RootElemenHttpMessageConverter и просто используйте возвращаемый тип в качестве корневого элемента для контекста. Не проверял, что происходит под капотом (в исходниках), но предполагаю, что проблема кроется где-то в том, что я описал выше, где Person.class не втягивается в контекст.

Что вы можете сделать, так это указать свой собственный MarshallingHttpMessageConverter в контексте сервлета. Чтобы заставить его работать (с конфигурацией xml), это то, что я добавил

<annotation-driven>
    <message-converters>
        <beans:bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
            <beans:property name="marshaller" ref="jaxbMarshaller"/>
            <beans:property name="unmarshaller" ref="jaxbMarshaller"/>
        </beans:bean>
    </message-converters>
</annotation-driven>

<beans:bean id="jaxbMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
    <beans:property name="classesToBeBound">
        <beans:list>
            <beans:value>com.stackoverflow.jaxb.domain.JaxbGenericList</beans:value>
            <beans:value>com.stackoverflow.jaxb.domain.Person</beans:value>
        </beans:list>
    </beans:property>
</beans:bean>

Вы можете добиться того же с конфигурацией Java, как показано в @EnableWebMcv API, в котором вы расширяете WebMvcConfigurerAdapter и переопределяете configureMessageConverters, а также добавляете туда преобразователь сообщений. Что-то вроде:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.add(marshallingMessageConverter());
    }

    @Bean
    public MarshallingHttpMessageConverter marshallingMessageConverter() {
        MarshallingHttpMessageConverter converter = new MarshallingHttpMessageConverter();
        converter.setMarshaller(jaxbMarshaller());
        converter.setUnmarshaller(jaxbMarshaller());
        return converter;
    }

    @Bean 
    public Jaxb2Marshaller jaxbMarshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setClassesToBeBound(JaxbGenericList.class, Person.class);
        Map<String, Object> props = new HashMap<>();
        props.put(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.setMarshallerProperties(props);
        return marshaller;
    }
}

Используя аналогичный метод контроллера:

@Controller
public class TestController {

    @RequestMapping(value = "/people", 
                    method = RequestMethod.GET, 
                    produces = MediaType.APPLICATION_XML_VALUE)
    @ResponseBody
    public JaxbGenericList<Person> getXMLPersonsGeneric() {
        JaxbGenericList<Person> personsList = new JaxbGenericList<Person>();
        Person person1 = new Person();
        person1.setId(1);
        personsList.getItems().add(person1);
        return personsList;
    }
}

Мы получаем желаемый результат

введите здесь описание изображения:

person Paul Samsotha    schedule 20.09.2014
comment
Здравствуйте, большое спасибо за ваше подробное объяснение, имеет смысл, но: не могли бы вы проверить раздел Addition One? Он был добавлен. Благодарю вас. - person Manuel Jordan; 20.09.2014
comment
@ManuelJordan Пожалуйста, смотрите мое ОБНОВЛЕНИЕ - person Paul Samsotha; 21.09.2014
comment
Дайте мне знать, если вы проверили это :-D - person Paul Samsotha; 22.09.2014
comment
Привет и большое спасибо за вашу поддержку, теперь это работает. Но посмотрите мой раздел Addition Two. Это влияет на другие разделы - person Manuel Jordan; 22.09.2014
comment
У вас есть опечатки, обновите условия: Jaxb2RootElementHttpMessageConverter и @EnableWebMvc, SO запрашивает 6 символов, чтобы я мог обновить/отредактировать ваш пост, мне нужно исправить только 2. Я не могу отредактировать ваш пост. - person Manuel Jordan; 22.09.2014
comment
Как вы узнали, что Jaxb2RootElementHttpMessageConverter используется по умолчанию? Благодарю вас. - person Manuel Jordan; 22.09.2014
comment
Я вижу в API важную иерархию через интерфейс HttpMessageConverter<T>. Он имеет классы Jaxb2RootElementHttpMessageConverter Jaxb2CollectionHttpMessageConverter и MappingJackson2HttpMessageConverter, MappingJackson2XmlHttpMessageConverter. Обратите внимание на Джексона для JSON и XML и JAXB для Root и Collection. Теперь ваше решение использует Jaxb2Marshaller, оно относится к другой иерархии в API. Немного смущает эта ситуация. Если вы можете добавить explanation в свой ответ об этом наблюдении, пожалуйста, сделайте это. Заранее спасибо. - person Manuel Jordan; 22.09.2014