醉卧草庐听风雨

君子藏器于身,待时而动

0%

JAXB通用结果输出

前段时间参与支援一个SOAP协议任务,唤醒了许久之前的记忆。上次开发WebService大概已是两年多之前了,这次的使用中却是遇到JAXB的问题,其在CXF框架中作为默认的XML序列化和反序列化组件,在输出对象时极易遇到上下文未知的问题,下面就来一探究竟。

JAXB简介

JAXB全称为Java™ Architecture for XML Binding,为Java开发人员提供了一种在XML和Java代码之间进行映射的有效且标准的方式。使开发人员可以不必详细了解XML的规范和设计,编写较少的代码就可以生成标准的XML从而提升生产力。目前网上大多教程只言片语并不全面,不过好在我找到了官方文档地址,可点此访问进行系统而全面的学习使用。而这里则对遇到的问题进行记录和解析。

问题引出

在设计中,通常会设计一个通用的返回对象,用来标识处理的结果和状态,例如下面这个例子

1
2
3
4
5
6
7
@XmlRootElement
public class XmlResponse {
private Integer code;
private String msg;
private Object data;
//省略get set
}

由data作为Object承载通用的返回数据,但是问题也由此而来,在未设置data时一切正常,data一旦设置其他对象返回值就出问题,如下:

1
2
3
4
5
6
7
8
9
@Test
public void test() throws JAXBException {
XmlResponse p = new XmlResponse();
p.setCode(1);
p.setMsg("success");
p.setData(new User());
JAXBContext context = JAXBContext.newInstance(p.getClass());
context.createMarshaller().marshal(p, System.out);
}

上面这段代码在运行时会抛出如下异常

1
2
3
4
javax.xml.bind.MarshalException
- with linked exception:
[com.sun.istack.SAXException2: javax.xml.bind.JAXBException: class tk.wuzk.xml.User以及其任何超类对此上下文都是未知的。
javax.xml.bind.JAXBException: class tk.wuzk.xml.User以及其任何超类对此上下文都是未知的。]

处理方式

对此问题在网上大多给出的解决方案是使用@XmlSeeAlso(User.class),这样做没什么问题,但是用起来却颇为繁琐,这意味着以后每次新的返回类都要在这个XmlSeeAlso中追加。能不能简单些呢?先看看这个注解的作用,JAXB的源码非常丰富在源码中是如下介绍

Instructs JAXB to also bind other classes when binding this class.
Java makes it impractical/impossible to list all sub-classes of a given class. This often gets in a way of JAXB users, as it JAXB cannot automatically list up the classes that need to be known to JAXBContext.
For example, with the following class definitions:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

The user would be required to create JAXBContext as JAXBContext.newInstance(Dog.class,Cat.class) (Animal will be automatically picked up since Dog and Cat refers to it.)
XmlSeeAlso annotation would allow you to write:
@XmlSeeAlso({Dog.class,Cat.class})
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

This would allow you to do JAXBContext.newInstance(Animal.class). By the help of this annotation, JAXB implementations will be able to correctly bind Dog and Cat

这段源码的意思大概是:Animal有两个子类Dog和Cat需要序列化,通常情况下需要用户JAXBContext.newInstance(Dog.class,Cat.class)手动注册,但是使用XmlSeeAlso之后JAXB就会正确绑定相关类。这意味XmlSeeAlso其实是在创建JAXB上下文时帮用户自动注,按提示手动尝试手动注册问题中的User

1
2
3
JAXBContext context = JAXBContext.newInstance(p.getClass());
改为
JAXBContext context = JAXBContext.newInstance(p.getClass(), User.class);

执行之后发现没有任何问题,说明类正确被加载JAXB的上下文中就不会出现问题,那么如何自动注册相关类呢?在Spring官方文档的JAXB章节中已经为我们提供了一个解决方案。即使用org.springframework.oxm.jaxb.Jaxb2Marshaller进行处理,其位于spring-oxm的依赖中,特别优秀的一点便是支持配置packagesToScan实现类的自动装载JAXB上下文。其核心代码如下

1
2
3
4
5
6
7
8
9
10
11
private JAXBContext createJaxbContextFromPackages(String... packagesToScan) throws JAXBException {
ClassPathJaxb2TypeScanner scanner = new ClassPathJaxb2TypeScanner(this.beanClassLoader, packagesToScan);
Class<?>[] jaxb2Classes = scanner.scanPackages();
this.classesToBeBound = jaxb2Classes;
if (this.jaxbContextProperties != null) {
return JAXBContext.newInstance(jaxb2Classes, this.jaxbContextProperties);
}
else {
return JAXBContext.newInstance(jaxb2Classes);
}
}

ClassPathJaxb2TypeScanner可以扫描被JAXB注解的类,以下JAXB注解的类皆会被扫描,有兴趣的同学可以直接看扫描的实现,此处不再说明

  • XmlRootElement
  • XmlType
  • XmlSeeAlso
  • XmlEnum
  • XmlRegistry

如此以来直接进行如下配置即可直接扫描相关的包,而摆脱每次手动注册上下文的烦恼。

1
2
3
4
5
6
7
<bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="packagesToScan">
<list>
<value>tk.wuzk.xml</value>
</list>
</property>
</bean>

写在最后

原本以为写一个通用的返回对象没什么困难,结果其中还是遇到了坑,不像JSON那么便捷好用,好在xml用的也不多。最后不得不再次佩服Spring庞大的生态几乎所有问题都能找到答案。看来后面要再多看几次Spring的文档了,希望这篇文章能帮助您。