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

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

@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?

Дополнение Один

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

@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 не работает.

Для моего другого XML-URLhttp://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);

чтобы:

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, не могли бы вы мне помочь?

1 ответ

Решение

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

TestApplication:

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.stackru.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>

Все это, как говорится...

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

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

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



ОБНОВЛЕНИЕ (используя Spring Web)

Похоже, что в веб-среде 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.stackru.jaxb.domain.JaxbGenericList</beans:value>
            <beans:value>com.stackru.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;
    }
}

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

:

Другие вопросы по тегам