Spring MVC with JAXB, List response based on a Generic class

十年热恋 提交于 2019-12-11 04:06:52

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!