tags: [XML]
categories: [工具]


JAXB简介

JAXB(Java Architecture for XML Binding简称JAXB)允许Java开发人员将Java类映射为XML表示方式。JAXB提供两种主要特性:将一个Java对象序列化为XML,以及反向操作,将XML解析成Java对象。换句话说,JAXB允许以XML格式存储和读取数据,而不需要程序的类结构实现特定的读取XML和保存XML的代码
简而言之,就是JAVA自带的XML和对象的转换工具

注解一览

@XmlAccessorType
@XmlElement
@XmlRootElement
@XmlAttribute
@XmlTransient
@XmlAccessorOrder
@XmlType
@XmlElementWrapper

@XmlAccessorType

@XmlAccessorType用于指定由java对象生成xml文件时对java对象属性的访问方式常与@XmlRootElement、@XmlType一起使用
它的属性值是XmlAccessType的4个枚举值,分别为:XmlAccessType.FIELDjava对象中的所有成员变量;XmlAccessType.PROPERTYjava对象中所有通过getter/setter方式访问的成员变量;XmlAccessType.PUBLIC_MEMBERjava对象中所有的public访问权限的成员变量和通过getter/setter方式访问的成员变量;XmlAccessType.NONEjava对象的所有属性都不映射为xml的元素
一般使用XmlAccessType.FIELD,这样配合Lombok和@XmlElement就可以不写GET/SET方法来重名名XML节点了

@XmlElement

该注解用在java类的属性上,用于将属性映射为xml的子节点。可通过在后面配置name属性值来改变java属性在xml文件中的名称

@XmlRootElement

类级别的注解,对应的是xml文件中的根节点。常与 @XmlType 和 @XmlAccessorType一起使用

@XmlAttribute

属性注解,有些XML会出现如下的转换要求,需要将指定属性放到标签里面来,可以看到下面的attr属性的设置就需要@XmlAttribute来设置**

  1. <SYS_HEAD>
  2. <USER_ID attr="s,50">109758</USER_ID>
  3. </SYS_HEAD>

@XmlTransient

属性注解,用于标示在由java对象映射xml时,忽略此属性。即,在生成的xml文件中不出现此元素

@XmlAccessorOrder

用于对java对象生成的xml元素进行排序。它有两个属性值:AccessorOrder.ALPHABETICAL:对生成的xml元素按字母顺序排序;XmlAccessOrder.UNDEFINED:不排序
@XmlType
该注解用在class类上,常与@XmlRootElement,@XmlAccessorType一起使用。它有三个属性:name、propOrder、namespace,经常使用的只有前两个属性
@XmlElementWrapper
注解在属性上,但是该属性必须是数组类型的,该注解可以将数组元素重新添加嵌套一个节点,举例,这样的出来就是如第二个展示的

  1. @XmlElementWrapper(name = "INVOICE_INFO_ARRAY")
  2. @XmlElement(name = "struct")
  3. private List<AssociateInvoiceVo> associateInvoiceVoList;
  1. <INVOICE_INFO_ARRAY>
  2. <struct>
  3. ---
  4. </struct>
  5. <struct>
  6. ---
  7. </struct>
  8. </INVOICE_INFO_ARRAY>

注意

  1. @XmlAccessorType的默认访问级别是XmlAccessType.PUBLIC_MEMBER。因此,如果java对象中的private成员变量设置了public权限的getter/setter方法,就不要在private变量上使用@XmlElement和@XmlAttribute注解,否则在由java对象生成xml时会报同一个属性在java类里存在两次的错误
  2. 在使用@XmlType的propOrder属性时,必须列出JavaBean对象中的所有属性(也要在所有属性上加上xml注解),否则会报错


使用

1. 定义对象

解析XML需要提前根据模板XML建立相对应的对象,并添加上相应的注解,转为XML也是一样,需要给对象加上相关的注解,举个栗子:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <service version="2.0">
  3. <USER_INFO>
  4. <USER_ID>109758</USER_ID>
  5. <USER_NAME>孙博文</USER_NAME>
  6. <PASSWORD>123456</PASSWORD>
  7. <BIRTHDAY>19990928</BIRTHDAY>
  8. </USER_INFO>
  9. <QUERY>
  10. <PAGE_NUMBER>1</PAGE_NUMBER>
  11. <PAGE_SIZE>10</PAGE_SIZE>
  12. </QUERY>
  13. </service>

以上的XML,用面向对象的方法解析应该有三个对象,即最外层的service,以及USER_INFO对象和QUERY对象
具体见下

  1. @XmlRootElement(name = "service")
  2. @XmlAccessorType(XmlAccessType.FIELD)
  3. @Getter
  4. @Setter
  5. @ToString
  6. public class Service {
  7. @XmlElement(name = "USER_INFO")
  8. private UserInfo userInfo;
  9. @XmlElement(name ="QUERY")
  10. private Query query;
  11. }
  12. @XmlAccessorType(XmlAccessType.FIELD)
  13. @Setter
  14. @Getter
  15. @ToString
  16. public class UserInfo {
  17. @XmlElement(name = "USER_ID")
  18. private String userId;
  19. @XmlElement(name = "USER_NAME")
  20. private String userName;
  21. @XmlElement(name = "PASSWORD")
  22. private String password;
  23. @XmlElement(name = "BIRTHDAY")
  24. private Date birthday;
  25. }
  26. @XmlRootElement(name = "QUERY")
  27. @XmlAccessorType(XmlAccessType.FIELD)
  28. @Setter
  29. @Getter
  30. @ToString
  31. public class Query {
  32. @XmlElement(name = "PAGE_NUMBER")
  33. private Integer pageNumber;
  34. @XmlElement(name = "PAGE_SIZE")
  35. private Integer pageSize;
  36. }

2. 工具类解析

OK对象都建好了,注解也加上了,下面可以直接测试,关于测试我书写了一个工具类来分别解析和转换XML

  1. @Getter
  2. public class XmlUtil {
  3. private static final Logger log = LoggerFactory.getLogger(XmlUtil.class);
  4. private static ConcurrentHashMap<String, JAXBContext> jaxbContextMap = new ConcurrentHashMap<>(8);
  5. @SuppressWarnings("unchecked")
  6. public static <T> T parse(String xml, Class<T> type) throws JAXBException {
  7. Object t = null;
  8. try {
  9. JAXBContext context = jaxbContextMap.get(type.getName());
  10. if (context == null) {
  11. context = JAXBContext.newInstance(type);
  12. jaxbContextMap.put(type.getName(), context);
  13. }
  14. Unmarshaller unmarshaller = context.createUnmarshaller();
  15. t = unmarshaller.unmarshal(new StringReader(xml));
  16. }catch (Exception e){
  17. log.error("解析xml异常,异常信息:{}", e.getMessage(), e);
  18. return null;
  19. }
  20. return (T) t;
  21. }
  22. public static String toXml(Object object) {
  23. JAXBContext context = jaxbContextMap.get(object.getClass().getName());
  24. try {
  25. if (context == null) {
  26. context = JAXBContext.newInstance(object.getClass());
  27. jaxbContextMap.put(object.getClass().getName(), context);
  28. }
  29. Marshaller marshaller = context.createMarshaller();
  30. marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
  31. marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
  32. StringWriter writer = new StringWriter();
  33. writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
  34. marshaller.marshal(object, writer);
  35. return writer.toString();
  36. }catch (Exception e){
  37. log.error("转xml异常,异常信息:{}", e.getMessage(), e);
  38. return null;
  39. }
  40. }
  41. }

3. 解析结果

image.png
可以看到XML中数据已经被正确的解析为了对象中的属性值
转换也是一样,这里不赘述了

其他问题

1. XML头信息的设置

关于头信息,默认出来的会是这样子<?xml version="1.0" encoding="UTF-8" standalone="yes"?>包含了standalone="yes"这部分,一般没人会要这个,所以需要在工具类中设置marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE),使用最原始的信息,然后手动通过流的方式去写入自定义信息,writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");

2. 将对象转换为XML时,日期格式无法自定义

很多时候对于XML出参时有限制的,尤其是时间,对于Date对象,默认的出参是这样的

  1. UserInfo userInfo = new UserInfo();
  2. userInfo
  3. .setBirthday(new Date());
  4. String s = XmlUtil.toXml(userInfo);
  5. System.out.println(s);
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <USER_INFO>
  3. <BIRTHDAY>2020-08-08T14:44:22.396+08:00</BIRTHDAY>
  4. </USER_INFO>

这里提供了两种方式

  1. 原生JAXB的Adapter处理数据转换
    集成XmlAdapter这个抽象类,覆写它的marshal方法,如果你研究过,JAXB的API的话,会知道marshal方法是转换为XML,而unmarshal方法是将XML解析为对象的,之前的unmarshal方法我们只需要调用它默认的解析器,将marshal做自定义的转换,就可以了

下面是代码

  1. public class DateAdaptor extends XmlAdapter<String, Date> {
  2. @Override
  3. public Date unmarshal(String v) throws Exception {
  4. return DatatypeConverter.parseDate(v).getTime();
  5. }
  6. @Override
  7. public String marshal(Date v) throws Exception {
  8. String pattern = "yyyyMMdd";
  9. return new SimpleDateFormat(pattern).format(v);
  10. }
  11. }

给对应字段应用上适配器

  1. @ToString
  2. public class UserInfo {
  3. @XmlElement(name = "USER_ID")
  4. private String userId;
  5. @XmlElement(name = "USER_NAME")
  6. private String userName;
  7. @XmlElement(name = "PASSWORD")
  8. private String password;
  9. @XmlJavaTypeAdapter(DateAdaptor.class)
  10. @XmlElement(name = "BIRTHDAY")
  11. private Date birthday;
  12. }

输出结果,可以看到已然生效了

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <USER_INFO>
  3. <BIRTHDAY>20200808</BIRTHDAY>
  4. </USER_INFO>
  1. 使用MapStruct自动映射处理

现在的很多项目都采用MapStruct来作为处理对象映射转换的最终方案,对于接口返回的对象可定就是Vo了嘛,所以既然反正都要转,不如直接解析为Vo对象,让MapStruct来帮助我们解决这个问题
这里要提到一个问题,就是MapStruct的隐式转换问题,文档在这:传送门 具体就是如果源Bean中基本数据类型和日期等与目标的数据类型不一致时,MapStruct将属性自动转换为适配类型,基本数据转String都没啥问题,但是日期如果不设置默认的,就会格式化比较奇怪,所以我们直接使用定义数据类型为String,然后添加上dateFormat=”你的pattern”就可以了
因为这个基本就是MapStruct的使用就不写例子了,如果项目上使用MapStruct和Vo对象,建议直接使用第二种,会方便很多!

3. XML标签内属性添加问题

看下下面这个XML

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <service version="2.0">
  3. <USER_INFO>
  4. <USER_ID attr="s,6">109758</USER_ID>
  5. <USER_NAME attr="s,3">孙博文</USER_NAME>
  6. <PASSWORD attr="s,6">123456</PASSWORD>
  7. <BIRTHDAY attr="s,8">19990928</BIRTHDAY>
  8. </USER_INFO>
  9. <QUERY>
  10. <PAGE_NUMBER attr="s,1">1</PAGE_NUMBER>
  11. <PAGE_SIZE attr="s,2">10</PAGE_SIZE>
  12. </QUERY>
  13. </service>

在标签内部有了attr属性,要求属性name为attr,value为s + 实际字符串长度
这个问题其实困扰我挺长时间,虽然开始想到了解决方法,但是觉得有点笨,想想有没有更好的处理方式,期间也看到有人在网上题除较为简单的方案,比如一个Eclipse专家组的成员使用@XmlPath来解决这个问题,原文在这 传送门 但是我本地测试就没成功过,所以最终还是用了之前的方法 使用对象包装的方式将attr字段属性和实际Value都包含,在使用@XmlAttribute注解和@XmlValue来区分,达到上述效果

  1. 定义一个ElString对象

    1. @Setter
    2. @Getter
    3. @XmlAccessorType(XmlAccessType.FIELD)
    4. public class ElString {
    5. @XmlAttribute
    6. private String attr;
    7. @XmlValue
    8. private String value;
    9. public ElString(String value) {
    10. if (StringUtils.isEmpty(value)){
    11. this.value = "";
    12. this.attr = "s,0";
    13. }else {
    14. this.value = value;
    15. this.attr = "s," + value.length();
    16. }
    17. }
    18. }
  2. 将之前Vo里面所有属性的类型都改为这个

  3. 新建类,书写string类型转Elstring的方法,使用MapStruct的use属性指定该类,覆盖掉默认的转换方法,这里需要提的就是隐式转换会发生在进入我们书写类的方法之前,比如源Bean字段类型是Integer那么会被先转换为String,然后进入我们的方法,转换为ElString

    1. @Component
    2. public class ElStringMapper {
    3. public ElString toElString(String string){
    4. return new ElString(string);
    5. }
    6. }

    OK,这边就差不多结束了