问题
I am working with Spring 4, together with Spring MVC.
I have the following POJO class
@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;
}
…
}
If I want return a list of Person through Spring MVC in XML format, I have the following handler
@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;
}
Where JaxbGenericList is (class declaration based on 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;
}
}
When I execute the URL, I get the following error stack trace
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.]
But If I have this (class declaration not based on 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;
}
}
and other handler method of course, as follows
@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;
}
All work fine:
Question, what extra configuration I need to work in peace with only JaxbGenericList
Addition One
I did not configure a marshaller
bean, so I think Spring behind the scenes has provided one. Now according with your reply I have added the following:
@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;
}
Something seems missing because I receive 'again':
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.]
Have you tried in a Web Environment? Seems I need tell Spring MVC use that marshaller
, I said seems because other URL working with not collections of POJO/XML works fine yet.
I am working with Spring 4.0.5 and Web environment is configured through Java Config
Addition Two
Your latest edition suggestion works
@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;
}
But now my XML not generic and JSON code fails.
For my other XML URL
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.
It has been fixed adding or updating:
from:
marshaller.setClassesToBeBound(JaxbGenericList.class, Person.class);
to:
marshaller.setClassesToBeBound(JaxbGenericList.class, Person.class, JaxbPersonList.class);
But for JSON, other URL http://localhost:8080/spring-utility/person/getjsonperson
again
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.
Seems I need add a converter for JSON (I did not define before), I have used the Spring defaults, could you give me a hand please?
回答1:
One way this can happen in Spring is if you don't have the marshaller configured properly. Take for example this standalone:
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 (notice the 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;
}
}
Domain
@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;
}
}
Result running the above application, it'll fail to marshal, with the same cause of the exception
Caused by: javax.xml.bind.JAXBException: class com.stackoverflow.Person nor any of its super class is known to this context.
The cause, if you look at the AppConfig
, is the method setClassesToBeBound
. It only includes the GenericWrapper
class. So why is this a problem?
Let's look at JAXB under the hood:
We interact with JAXB through the JAXBContext
. Normally it's alright to just create the context with the top level element like
JAXBContext context = JAXBContext.newInstance(GenericWrapper.class);
JAXB will pull into the context all classes associated with the class. That's why List<Person>
works. Person.class
is explicitly associated.
But in the case of GenericWrapper<T>
, the Person
class is nowhere related to the GenericWrapper
. But if we explicitly put Person
class into the context, it'll be found, and we can use the generics.
JAXBContext context
= JAXBContext.newInstance(GenericWrapper.class, Person.class);
That being said, under the hood, Jaxb2Marshaller
uses this same context. Going back to the AppConfig
, you can see marshaller.setClassesToBeBound(GenericWrapper.class);
, which is ultimately the same as the first JAXBContext
creation above. If we add Person.class
, then we would have the same configuration as the second JAXBContext
creation.
marshaller.setClassesToBeBound(GenericWrapper.class, Person.class);
Now run the test application again, and it'll get the desired output.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<genericWrapper>
<item>
<id>1</id>
</item>
</genericWrapper>
All that being said...
I don't know how you've set up your configuration of the marshaller, or if the framework has a default set up. But you need to get a hold of the marshaller, and configure it to find the classes in one of three ways:
packagesToScan
.contextPath
- or
classesToBeBound
(like I've done above)
This can be done through Java configuration or xml configuration
- See more at the Spring-JAXB reference documentation
UPDATE (using Spring web)
So it seems, in a Spring web environment, if you don't specify the http message converter, Spring will use Jaxb2RootElemenHttpMessageConverter and just use the return type as the root element for the context. Haven't checked what's going on under the hood (in the source), but I would assume that the problem lies somewhere in what I described above, where the Person.class
is not being pulled into the context.
What you can do is specify your own MarshallingHttpMessageConverter, in the servlet context. To get it to work (with xml config), this is what I added
<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>
You could achieve the same with Java confguration, as seen in the @EnableWebMcv API, where you extend WebMvcConfigurerAdapter
and override configureMessageConverters
, and add the message converter there. Something like:
@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;
}
}
Using similar controller method:
@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;
}
}
We get the result we want
:来源:https://stackoverflow.com/questions/25939629/spring-mvc-with-jaxb-list-response-based-on-a-generic-class