在项目中经常会遇到xml与Object的转换,即java对象序列号为xml文档,xml文档可以反序列化为java对象。目前比较好用的有jaxb和XStream。下面进行分别介绍
一、JAXB
Java Architecture for XML Binding (JAXB) 是一个业界的标准,是一项可以根据XML Schema产生Java类的技术。
废话不多说,直接上例子,假设我们需要实现一个简单的学校学生系统,我们的java对象如下:
/** * @author ozl * 基类 */ public abstract class AbstracElement { private String address; private String name; /** * @return the address */ public String getAddress() { return address; } /** * @param address the address to set */ public void setAddress(String address) { this.address = address; } /** * @return the name */ public String getName() { return name; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } } //学生对象 public class Student extends AbstracElement { // 出生日期 private XMLGregorianCalendar birthDay; /** * @return the birthDay */ public XMLGregorianCalendar getBirthDay() { return birthDay; } /** * @param birthDay * the birthDay to set */ public void setBirthDay(XMLGregorianCalendar birthDay) { this.birthDay = birthDay; } } public class School extends AbstracElement { private List<Student> students; private int studentCount; /** * @return the students */ public List<Student> getStudents() { return students; } /** * @return the studentCount */ public int getStudentCount() { studentCount=getStudents().size(); return this.studentCount; } }
jaxb通过注解的关联java对象和序列号的方式,现在我们着重介绍一下常用的xml.binding提供的注解。
@XmlAccessorType 控制默认序列号使用javabean的属性或者字段,值为XmlAccessType类型,当为PROPERTY时表示每一个setter/getter方法对都是序列号、FIELD表示字段、PUBLIC_MEMBER表示FIELD或者getter/setter方法。
@XmlRootElement:映射xml的根节点,name和namespace方法
@XmlElement:表示一个子节点,里面可以有name和namespace方法
@XmlAttribute:表示属性。
现在我们在上面的java代码上就可以使用上面的注解实现序列化了。
我们在基类AbstracElement中把name作为节点的属性,在address作为字元素处理,代码如下:
/** * @return the address */ @XmlElement(name="address",namespace=JaxbTest.default_namespace) public String getAddress() { return address; } /** * @return the name */ @XmlAttribute public String getName() { return name; } 修改School类如下: @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name="school",namespace=JaxbTest.default_namespace) //定义根节点的名字和命名空间 public class School extends AbstracElement { @XmlElement(name="student",namespace=JaxbTest.default_namespace ) private List<Student> students; //其他的方法省略了 } @XmlAccessorType(XmlAccessType.PROPERTY) public class Student extends AbstracElement { // 出生日期 // @XmlElement //如果此时再加上这个注解就会报错,发现两个birthDay属性 private XMLGregorianCalendar birthDay; /** * @return the birthDay */ @XmlElement(namespace = JaxbTest.default_namespace) public XMLGregorianCalendar getBirthDay() { return birthDay; } //省略了其他的方法 } 我们的测试代码如下: public class JaxbTest { public static final String default_namespace = "http://www.ozl.com/test.default"; public static final String extend_namespace = "http://www.ozl.com/test.extend"; /** * @param args * @throws JAXBException * @throws DatatypeConfigurationException */ public static void main(String[] args) throws JAXBException, DatatypeConfigurationException { marshallerXML(); unmarshaller(); } public static void marshallerXML() throws JAXBException, DatatypeConfigurationException { JAXBContext jc = JAXBContext.newInstance(School.class); School school = new School(); school.setName("实验"); school.setAddress("南京雨花台区"); List<Student> students = new ArrayList<>(); school.setStudents(students); Student student1 = new Student(); student1.setName("张三"); student1.setAddress("南京市xx路"); XMLGregorianCalendar c = DatatypeFactory.newInstance() .newXMLGregorianCalendarDate(2014, 11, 11, DatatypeConstants.FIELD_UNDEFINED); student1.setBirthDay(c); students.add(student1); Student student2 = new Student(); student2.setName("李四"); student2.setAddress("济南市xx路"); students.add(student2); Student student3 = new Student(); student3.setName("王五"); student3.setAddress("济南市xx路"); students.add(student3); Marshaller marshaller = jc.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.setProperty(Marshaller.JAXB_ENCODING, "GBK"); marshaller.marshal(school, System.out); } public static void unmarshaller() throws JAXBException { InputStream io = JaxbTest.class.getResourceAsStream("school.xml"); JAXBContext jc = JAXBContext.newInstance(School.class); Unmarshaller unmarshaller = jc.createUnmarshaller(); School school = (School) unmarshaller.unmarshal(io); System.out.println(school.getStudentCount()); } } 输出结果为: <?xml version="1.0" encoding="GBK" standalone="yes"?> <school name="实验" xmlns="http://www.ozl.com/test.default"> <address>南京雨花台区</address> <student name="张三"> <address>南京市xx路</address> <birthDay>2014-11-11</birthDay> </student> <student name="李四"> <address>济南市xx路</address> </student> <student name="王五"> <address>济南市xx路</address> </student> </school>
现在我需要在student的子节点上包装一层节点,需要使用@XmlElementWrapper注解,Student类的students字段上增加如下注解:
@XmlElementWrapper(name="students",namespace=JaxbTest.default_namespace ) //多包一层节点 @XmlElement(name="student",namespace=JaxbTest.default_namespace ) private List<Student> students;
如果此时再运行我们的测试代码,输出如下:
<?xml version="1.0" encoding="GBK" standalone="yes"?>
<school name="实验" xmlns="http://www.ozl.com/test.default">
<address>南京雨花台区</address>
<students>
<student name="张三">
<address>南京市xx路</address>
<birthDay>2014-11-11</birthDay>
</student>
<student name="李四">
<address>济南市xx路</address>
</student>
<student name="王五">
<address>济南市xx路</address>
</student>
<students>
</school>
现在我需要记录学生的兴趣爱好,在Student类上增加List<String> hobbies字段。我们修改测试代码和Student类如下:
// 星期爱好 private List<String> hobbies; @XmlElement(name="hobby" ,namespace=JaxbTest.default_namespace) //自定义节点的名字 public List<String> getHobbies() { return hobbies; } 修改测试代码的marshallerXML()方法如下: JAXBContext jc = JAXBContext.newInstance(School.class); School school = new School(); school.setName("实验"); school.setAddress("南京雨花台区"); List<Student> students = new ArrayList<>(); school.setStudents(students); Student student1 = new Student(); student1.setName("张三"); student1.setAddress("南京市xx路"); List<String> hobbies = new ArrayList<>(); hobbies.add("游泳"); hobbies.add("乒乓球"); student1.setHobbies(hobbies); XMLGregorianCalendar c = DatatypeFactory.newInstance() .newXMLGregorianCalendarDate(2014, 11, 11, DatatypeConstants.FIELD_UNDEFINED); student1.setBirthDay(c); students.add(student1); Student student2 = new Student(); student2.setName("李四"); student2.setAddress("济南市xx路"); hobbies = new ArrayList<>(hobbies); hobbies.add(0, "篮球"); student2.setHobbies(hobbies); students.add(student2); Student student3 = new Student(); student3.setName("王五"); student3.setAddress("济南市xx路"); hobbies = new ArrayList<>(hobbies); hobbies.remove(1); student3.setHobbies(hobbies); students.add(student3); Marshaller marshaller = jc.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.setProperty(Marshaller.JAXB_ENCODING, "GBK"); marshaller.marshal(school, System.out); 测试结果如下: <?xml version="1.0" encoding="GBK" standalone="yes"?> <school name="实验" xmlns="http://www.ozl.com/test.default"> <address>南京雨花台区</address> <students> <student name="张三"> <address>南京市xx路</address> <birthDay>2014-11-11</birthDay> <hobby>游泳</hobby> <hobby>乒乓球</hobby> </student> <student name="李四"> <address>济南市xx路</address> <hobby>篮球</hobby> <hobby>游泳</hobby> <hobby>乒乓球</hobby> </student> <student name="王五"> <address>济南市xx路</address> <hobby>篮球</hobby> <hobby>乒乓球</hobby> </student> </students> </school>
发现hobby列了很多子节点,但是我需要序列号为一个节点,这时候可以使用@XmlJavaTypeAdapter来实现自己序列号或者反序列化的规则,现在修改如下:
/** * @return the hobbies */ @XmlJavaTypeAdapter(ListXmlAdapter.class) //自定义序列号和反序列化方式 @XmlElement(name="hobby" ,namespace=JaxbTest.default_namespace) //自定义节点的名字 public List<String> getHobbies() { return hobbies; } 而ListXmlAdapter类如下: /** * @author ozl * */ public class ListXmlAdapter extends XmlAdapter<String, List<String>> { @Override public List<String> unmarshal(String v) throws Exception { String[] values = v.split("#"); return new ArrayList<String>(Arrays.asList(values)); } @Override public String marshal(List<String> v) throws Exception { StringBuffer buffer=new StringBuffer(); for(int i=0;i<v.size()-1;i++) { buffer.append(v.get(i)); buffer.append("#"); } if(v.size()>0) { buffer.append(v.get(v.size()-1)); } return buffer.toString(); } }
此时再运行我们的测试代码,得到的结果如下:
<?xml version="1.0" encoding="GBK" standalone="yes"?>
<school name="实验" xmlns="http://www.ozl.com/test.default">
<address>南京雨花台区</address>
<students>
<student name="张三">
<address>南京市xx路</address>
<birthDay>2014-11-11</birthDay>
<hobby>游泳#乒乓球</hobby>
</student>
<student name="李四">
<address>济南市xx路</address>
<hobby>篮球#游泳#乒乓球</hobby>
</student>
<student name="王五">
<address>济南市xx路</address>
<hobby>篮球#乒乓球</hobby>
</student>
</students>
</school>
控制序列号的顺序:有的时候我们的xml可能是要符合一定的XSD的,需要指定顺序,@XmlType可以满足我们的要求,
@XmlType(name = "StudentType", namespace = JaxbTest.default_namespace, propOrder = { "hobbies" ,"birthDay"}) // propOrder表示元素系列化的顺序。不能包括父类的字段
有的时候我们需要添加一些扩展的元素和属性。 首先看一下如何添加扩展的属性,我们使用@XmlAnyAttribute注解实现,该注解作用的类型必须为Map<QName, String>类型,我们在School中增加 @XmlAnyAttribute //只要定义扩展属性,在xsd中为anyAttribute类型 private Map<QName, String> addationInfo; 修改测试代码如下:
public static void marshallerXML() throws JAXBException, DatatypeConfigurationException { JAXBContext jc = JAXBContext.newInstance(School.class); School school = new School(); school.setName("实验"); school.setAddress("南京雨花台区"); Map<QName, String> schoolAddation = new HashMap<QName, String>(); QName nameQname = new QName(extend_namespace, "nature", "extend"); schoolAddation.put(nameQname, "公立"); school.setAddationInfo(schoolAddation); Marshaller marshaller = jc.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.setProperty(Marshaller.JAXB_ENCODING, "GBK"); marshaller.marshal(school, System.out); } 测试结果如下: <?xml version="1.0" encoding="GBK" standalone="yes"?> <school name="实验" extend:nature="公立" xmlns="http://www.ozl.com/test.default" xmlns:extend="http://www.ozl.com/test.extend"> <address>南京雨花台区</address> </school>
@XmlAnyElement定义我们可以扩展元素: 我们在School类中增加如下字段: @XmlAnyElement private List<Object> anyExtendElements; 同时需要在school中增加一个校长的子节点,起java类如下:
@XmlRootElement(name="principal",namespace=JaxbTest.extend_namespace) public class Principal extends AbstracElement { private String description; /** * @return the description */ @XmlElement public String getDescription() { return description; } /** * @param description the description to set */ public void setDescription(String description) { this.description = description; } } 此时,我们需要定义个xmlFactory来识别这个类 @XmlRegistry public class ObjectFactory { @XmlElementDecl(name="principal",namespace=JaxbTest.extend_namespace) public JAXBElement<Principal> createPrincipal(Principal principal) { return new JAXBElement<Principal>(new QName("principal"), Principal.class, principal); } } 修改测试代码如下: public static void marshallerXML() throws JAXBException, DatatypeConfigurationException { JAXBContext jc = JAXBContext.newInstance(School.class, ObjectFactory.class); School school = new School(); school.setName("实验"); school.setAddress("南京雨花台区"); Principal p = new Principal(); p.setName("ozl"); school.getAnyExtendElements().add(p); Marshaller marshaller = jc.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.setProperty(Marshaller.JAXB_ENCODING, "GBK"); marshaller.marshal(school, System.out); } 测试结果如下: <?xml version="1.0" encoding="GBK" standalone="yes"?> <ns2:school name="实验" xmlns:ns2="http://www.ozl.com/test.default" xmlns:ns3="http://www.ozl.com/test.extend"> <ns2:address>南京雨花台区</ns2:address> <ns3:principal name="ozl"/> </ns2:school>
这样就是用jaxb实现了xml和java对象之间的转换
二、XStream xStream
可以轻易的将Java对象和xml文档相互转换,而且也支持json的转换。我们仍然用School模型来简要介绍XStream。
public abstract class AbstracElement { private String address; private String name; /** * @return the address */ public String getAddress() { return address; } /** * @param address the address to set */ public void setAddress(String address) { this.address = address; } /** * @return the name */ @XmlAttribute public String getName() { return name; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } } public class Student extends AbstracElement { // 出生日期 private XMLGregorianCalendar birthDay; // 星期爱好 private List<String> hobbies; /** * @return the birthDay */ public XMLGregorianCalendar getBirthDay() { return birthDay; } /** * @param birthDay * the birthDay to set */ public void setBirthDay(XMLGregorianCalendar birthDay) { this.birthDay = birthDay; } /** * @return the hobbies */ public List<String> getHobbies() { return hobbies; } /** * @param hobbies * the hobbies to set */ public void setHobbies(List<String> hobbies) { this.hobbies = hobbies; } } public class School extends AbstracElement { private List<Student> students; private int studentCount; /** * @return the students */ public List<Student> getStudents() { return students; } /** * @param students * the students to set */ public void setStudents(List<Student> students) { this.students = students; } /** * @return the studentCount */ public int getStudentCount() { studentCount = getStudents().size(); return this.studentCount; } } public class XStreamTest { /** * @param args * @throws DatatypeConfigurationException */ public static void main(String[] args) throws DatatypeConfigurationException { XStream xstream = new XStream(); //默认构造函数需要xpp.jar xstream.alias("school", School.class); xstream.alias("student", Student.class); xstream.toXML(createStudent(), System.out); } public static School createStudent() throws DatatypeConfigurationException { School school=new School(); school.setName("实验"); school.setAddress("南京雨花台区"); List<Student> students=new ArrayList<Student>(); Student student1=new Student(); student1.setName("张三"); student1.setAddress("南京市xx路"); List<String> hobbies = new ArrayList<>(); hobbies.add("游泳"); hobbies.add("乒乓球"); student1.setHobbies(hobbies); XMLGregorianCalendar c = DatatypeFactory.newInstance() .newXMLGregorianCalendarDate(2014, 11, 11, DatatypeConstants.FIELD_UNDEFINED); student1.setBirthDay(c); students.add(student1); school.setStudents(students); return school; } }
这样输入的结果如下:
<school>
<address>南京雨花台区</address>
<name>实验</name>
<students>
<student>
<address>南京市xx路</address>
<name>张三</name>
<birthDay class="com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl">
<year>2014</year>
<month>11</month>
<day>11</day>
<timezone>-2147483648</timezone>
<hour>-2147483648</hour>
<minute>-2147483648</minute>
<second>-2147483648</second>
</birthDay>
<hobbies>
<string>游泳</string>
<string>乒乓球</string>
</hobbies> </student>
</students>
<studentCount>0</studentCount>
</school>
此时我们不需要实例化school的studentCount属性,我们可以在测试代码中添加如下代码:
xstream.omitField(School.class, "studentCount");//表示序列号忽略该字段,也可以通过注解@XStreamOmitField
Xstream默认对List类型多了一层包装,我们可以通过修改如下代码来去掉<students>包装:
xstream.addImplicitCollection(School.class, "students");
而我同时需要把name字段作为一个属性输出,我们可以添加测试代码如下: xstream.useAttributeFor(AbstracElement.class, "name");
同样hobbies我们需要实例化一个节点,这个时候我们得提供自定义的序列化方式 xstream.registerLocalConverter(Student.class, "hobbies", new ListXmlConverter()); 其中ListXmlConverter类的代码如下:
public class ListXmlConverter implements SingleValueConverter { /* (non-Javadoc) * @see com.thoughtworks.xstream.converters.ConverterMatcher#canConvert(java.lang.Class) */ @Override public boolean canConvert(Class type) { if(List.class.isAssignableFrom(type)) { return true; } return false; } /* (non-Javadoc) * @see com.thoughtworks.xstream.converters.SingleValueConverter#toString(java.lang.Object) */ @Override public String toString(Object obj) { @SuppressWarnings("unchecked") List<String> hobbies=(List<String>) obj; StringBuffer buffer=new StringBuffer(); for(int i=0;i<hobbies.size()-1;i++) { buffer.append(hobbies.get(i)); buffer.append("#"); } if(hobbies.size()>0) { buffer.append(hobbies.get(hobbies.size()-1)); } return buffer.toString(); } /* (non-Javadoc) * @see com.thoughtworks.xstream.converters.SingleValueConverter#fromString(java.lang.String) */ @Override public Object fromString(String str) { String[] values =str.split("#"); return new ArrayList<String>(Arrays.asList(values)); } }
同样我们可以自定义birthday属性的序列化和反序列化的方式。
xstream.registerLocalConverter(Student.class, "birthDay", new DateValueConverter()); DateValueConverter的代码如下:
public class DateValueConverter implements Converter { @Override public boolean canConvert(Class type) { if (XMLGregorianCalendar.class.isAssignableFrom(type)) { return true; } return false; } @Override public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { XMLGregorianCalendar birthday = (XMLGregorianCalendar) source; writer.setValue(birthday.toString()); } @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { String value = reader.getValue(); SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd"); try { Date date = format.parse(value); XMLGregorianCalendar c = DatatypeFactory.newInstance() .newXMLGregorianCalendarDate(date.getYear(), date.getMonth(), date.getDay(), DatatypeConstants.FIELD_UNDEFINED); return c; } catch (ParseException | DatatypeConfigurationException e) { return null; } } }
我们会发现每次在序列号birthday字段的时候总是会多个一个class属性,看着很是不爽。原来是XStream在序列号的时候会判断字段的类型和实际的类型是否为同一个class。 我们可以通过设置系统别名来去除掉该属性。增加如下代码: xstream.aliasSystemAttribute(null, "class");
XStream对命名空间的支持:
并不是所有的XMLDriver都支持命名空间,StaxDriver支持命名空间,我们修改测试代码如下:
QNameMap qNameMap=new QNameMap(); qNameMap.registerMapping(new QName("http://test", "school", "test"), "school"); StaxDriver staxDriver=new StaxDriver(qNameMap); XStream xstream = new XStream(staxDriver); xstream.alias("school", School.class);
这样序列号出来结果如下:
<?xml version="1.0" ?><test:school xmlns:test="http://test" name="实验"><address>南京雨花台区</address><student name="张三"><address>南京市xx路</address><birthDay>2014-11-11</birthDay><hobbies>游泳#乒乓球</hobbies></student></test:school>实验
三、比较(个人意见)
1、jaxb是基于xsd的,对扩展属性和扩展元素命名空间提供了很好的支持
2、jaxb可以是基于属性(getter/setter)方法,而xstream只能支持字段。如果需要序列化的类没有字段(比如所有的东西都存入map中),jaxb可以很轻松的搞定。
3、jaxb是jdk内置的,无需引入额外的jar包