【问题标题】:Spring MVC with JAXB, List response based on a Generic classSpring MVC with JAXB, List response based on a Generic class
【发布时间】:2014-11-14 09:15:16
【问题描述】:

我正在使用 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;
    }

    …
}

如果我想通过 Spring MVC 以 XML 格式返回 Person 列表,我有以下处理程序

@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 在哪里(基于泛型的类声明

@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.]

但是如果我有这个(类声明不基于泛型):

@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

加一

我没有配置marshaller bean,所以我认为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.]

您是否在 Web 环境中尝试过?似乎我需要告诉 Spring MVC 使用 marshaller,我说 似乎 因为其他使用 not POJO/XML 集合的 URL 工作正常。

我正在使用 Spring 4.0.5 并且 Web 环境是通过 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 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.

已修复添加或更新:

来自:

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 默认值,请帮我一下吗?

【问题讨论】:

    标签: spring spring-mvc jaxb spring-4 spring-oxm


    【解决方案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 的底层:

    我们通过JAXBContext 与 JAXB 进行交互。通常只使用像

    这样的顶级元素创建上下文就可以了
    JAXBContext context = JAXBContext.newInstance(GenericWrapper.class);
    

    JAXB 会将与该类关联的所有类拉入上下文。这就是List&lt;Person&gt; 起作用的原因。 Person.class 是显式关联的。

    但在GenericWrapper&lt;T&gt; 的情况下,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 Web 环境中,如果您不指定 http 消息转换器,Spring 将使用 Jaxb2RootElemenHttpMessageConverter 并仅使用返回类型作为上下文的根元素。还没有检查引擎盖下发生了什么(在源代码中),但我认为问题出在我上面描述的某个地方,Person.class 没有被拉到上下文中。

    您可以做的是在 servlet 上下文中指定您自己的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;
        }
    }
    

    我们得到了我们想要的结果

    :

    【讨论】:

    • 您好,非常感谢您的详细解释,有道理,但是:您可以查看Addition One 部分吗?它已被添加。谢谢。
    • @ManuelJordan 请查看我的更新
    • 如果您检查过这个,请告诉我:-D
    • 您好,非常感谢您的支持,现在可以使用了。但请参阅我的Addition Two 部分。它会影响其他部分
    • 你有一个勘误表,更新条款:Jaxb2RootElementHttpMessageConverter@EnableWebMvc,所以要求 6 个字符让我更新/编辑你的帖子,我只有 2 个要修复。我无法编辑你的帖子。
    猜你喜欢
    • 2022-12-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-14
    • 2022-11-20
    • 2022-12-02
    • 2022-12-02
    • 1970-01-01
    相关资源
    最近更新 更多