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-JAXB
ОБНОВЛЕНИЕ (используя 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;
}
}
Мы получаем желаемый результат
: