JAXB处理XML总结

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
2
3
<SYS_HEAD>
<USER_ID attr="s,50">109758</USER_ID>
</SYS_HEAD>

@XmlTransient

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

@XmlAccessorOrder

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

1
2
3
@XmlElementWrapper(name = "INVOICE_INFO_ARRAY")
@XmlElement(name = "struct")
private List<AssociateInvoiceVo> associateInvoiceVoList;
1
2
3
4
5
6
7
8
<INVOICE_INFO_ARRAY>
<struct>
---
</struct>
<struct>
---
</struct>
</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
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<service version="2.0">
<USER_INFO>
<USER_ID>109758</USER_ID>
<USER_NAME>孙博文</USER_NAME>
<PASSWORD>123456</PASSWORD>
<BIRTHDAY>19990928</BIRTHDAY>
</USER_INFO>
<QUERY>
<PAGE_NUMBER>1</PAGE_NUMBER>
<PAGE_SIZE>10</PAGE_SIZE>
</QUERY>
</service>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@XmlRootElement(name = "service")
@XmlAccessorType(XmlAccessType.FIELD)
@Getter
@Setter
@ToString
public class Service {

@XmlElement(name = "USER_INFO")
private UserInfo userInfo;

@XmlElement(name ="QUERY")
private Query query;
}


@XmlAccessorType(XmlAccessType.FIELD)
@Setter
@Getter
@ToString
public class UserInfo {

@XmlElement(name = "USER_ID")
private String userId;

@XmlElement(name = "USER_NAME")
private String userName;

@XmlElement(name = "PASSWORD")
private String password;

@XmlElement(name = "BIRTHDAY")
private Date birthday;
}

@XmlRootElement(name = "QUERY")
@XmlAccessorType(XmlAccessType.FIELD)
@Setter
@Getter
@ToString
public class Query {

@XmlElement(name = "PAGE_NUMBER")
private Integer pageNumber;

@XmlElement(name = "PAGE_SIZE")
private Integer pageSize;
}

2. 工具类解析

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Getter
public class XmlUtil {

private static final Logger log = LoggerFactory.getLogger(XmlUtil.class);

private static ConcurrentHashMap<String, JAXBContext> jaxbContextMap = new ConcurrentHashMap<>(8);

@SuppressWarnings("unchecked")
public static <T> T parse(String xml, Class<T> type) throws JAXBException {
Object t = null;
try {
JAXBContext context = jaxbContextMap.get(type.getName());
if (context == null) {
context = JAXBContext.newInstance(type);
jaxbContextMap.put(type.getName(), context);
}
Unmarshaller unmarshaller = context.createUnmarshaller();
t = unmarshaller.unmarshal(new StringReader(xml));
}catch (Exception e){
log.error("解析xml异常,异常信息:{}", e.getMessage(), e);
return null;
}
return (T) t;
}

public static String toXml(Object object) {
JAXBContext context = jaxbContextMap.get(object.getClass().getName());
try {
if (context == null) {
context = JAXBContext.newInstance(object.getClass());
jaxbContextMap.put(object.getClass().getName(), context);
}
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
StringWriter writer = new StringWriter();
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
marshaller.marshal(object, writer);
return writer.toString();
}catch (Exception e){
log.error("转xml异常,异常信息:{}", e.getMessage(), e);
return null;
}
}
}

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

这里提供了两种方式

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

下面是代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DateAdaptor extends XmlAdapter<String, Date> {

@Override
public Date unmarshal(String v) throws Exception {
return DatatypeConverter.parseDate(v).getTime();
}

@Override
public String marshal(Date v) throws Exception {
String pattern = "yyyyMMdd";
return new SimpleDateFormat(pattern).format(v);
}
}

给对应字段应用上适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ToString
public class UserInfo {

@XmlElement(name = "USER_ID")
private String userId;

@XmlElement(name = "USER_NAME")
private String userName;

@XmlElement(name = "PASSWORD")
private String password;

@XmlJavaTypeAdapter(DateAdaptor.class)
@XmlElement(name = "BIRTHDAY")
private Date birthday;
}

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

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

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

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

看下下面这个 XML

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

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

  1. 定义一个 ElString 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Setter
@Getter
@XmlAccessorType(XmlAccessType.FIELD)
public class ElString {

@XmlAttribute
private String attr;

@XmlValue
private String value;

public ElString(String value) {
if (StringUtils.isEmpty(value)){
this.value = "";
this.attr = "s,0";
}else {
this.value = value;
this.attr = "s," + value.length();
}
}
}
  1. 将之前 Vo 里面所有属性的类型都改为这个
  2. 新建类,书写 string 类型转 Elstring 的方法,使用 MapStruct 的 use 属性指定该类,覆盖掉默认的转换方法,这里需要提的就是隐式转换会发生在进入我们书写类的方法之前,比如源 Bean 字段类型是 Integer 那么会被先转换为 String,然后进入我们的方法,转换为 ElString
1
2
3
4
5
6
7
@Component
public class ElStringMapper {

public ElString toElString(String string){
return new ElString(string);
}
}

OK,这边就差不多结束了

作者

孙博文

发布于

2020-08-08

更新于

2021-07-18

许可协议

评论