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.FIELD
:java对象中的所有成员变量;XmlAccessType.PROPERTY
:java对象中所有通过getter/setter方式访问的成员变量;XmlAccessType.PUBLIC_MEMBER
:java对象中所有的public访问权限的成员变量和通过getter/setter方式访问的成员变量;XmlAccessType.NONE
:java对象的所有属性都不映射为xml的元素
一般使用XmlAccessType.FIELD,这样配合Lombok和@XmlElement就可以不写GET/SET方法来重名名XML节点了
@XmlElement
该注解用在java类的属性上,用于将属性映射为xml的子节点。可通过在后面配置name属性值来改变java属性在xml文件中的名称
@XmlRootElement
类级别的注解,对应的是xml文件中的根节点。常与 @XmlType 和 @XmlAccessorType一起使用
@XmlAttribute
属性注解,有些XML会出现如下的转换要求,需要将指定属性放到标签里面来,可以看到下面的attr属性的设置就需要@XmlAttribute来设置**
<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
注解在属性上,但是该属性必须是数组类型的,该注解可以将数组元素重新添加嵌套一个节点,举例,这样的出来就是如第二个展示的
@XmlElementWrapper(name = "INVOICE_INFO_ARRAY")
@XmlElement(name = "struct")
private List<AssociateInvoiceVo> associateInvoiceVoList;
<INVOICE_INFO_ARRAY>
<struct>
---
</struct>
<struct>
---
</struct>
</INVOICE_INFO_ARRAY>
注意
@XmlAccessorType
的默认访问级别是XmlAccessType.PUBLIC_MEMBER。因此,如果java对象中的private成员变量设置了public权限的getter/setter方法,就不要在private变量上使用@XmlElement和@XmlAttribute注解,否则在由java对象生成xml时会报同一个属性在java类里存在两次的错误- 在使用
@XmlType
的propOrder属性时,必须列出JavaBean对象中的所有属性(也要在所有属性上加上xml注解),否则会报错
使用
1. 定义对象
解析XML需要提前根据模板XML建立相对应的对象,并添加上相应的注解,转为XML也是一样,需要给对象加上相关的注解,举个栗子:
<?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对象
具体见下
@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
@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. 解析结果
可以看到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对象,默认的出参是这样的
UserInfo userInfo = new UserInfo();
userInfo
.setBirthday(new Date());
String s = XmlUtil.toXml(userInfo);
System.out.println(s);
<?xml version="1.0" encoding="UTF-8"?>
<USER_INFO>
<BIRTHDAY>2020-08-08T14:44:22.396+08:00</BIRTHDAY>
</USER_INFO>
这里提供了两种方式
- 原生JAXB的Adapter处理数据转换
集成XmlAdapter
这个抽象类,覆写它的marshal方法,如果你研究过,JAXB的API的话,会知道marshal方法是转换为XML,而unmarshal方法是将XML解析为对象的,之前的unmarshal方法我们只需要调用它默认的解析器,将marshal做自定义的转换,就可以了
下面是代码
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);
}
}
给对应字段应用上适配器
@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;
}
输出结果,可以看到已然生效了
<?xml version="1.0" encoding="UTF-8"?>
<USER_INFO>
<BIRTHDAY>20200808</BIRTHDAY>
</USER_INFO>
- 使用MapStruct自动映射处理
现在的很多项目都采用MapStruct来作为处理对象映射转换的最终方案,对于接口返回的对象可定就是Vo了嘛,所以既然反正都要转,不如直接解析为Vo对象,让MapStruct来帮助我们解决这个问题
这里要提到一个问题,就是MapStruct的隐式转换问题,文档在这:传送门 具体就是如果源Bean中基本数据类型和日期等与目标的数据类型不一致时,MapStruct将属性自动转换为适配类型,基本数据转String都没啥问题,但是日期如果不设置默认的,就会格式化比较奇怪,所以我们直接使用定义数据类型为String,然后添加上dateFormat=”你的pattern”就可以了
因为这个基本就是MapStruct的使用就不写例子了,如果项目上使用MapStruct和Vo对象,建议直接使用第二种,会方便很多!
3. XML标签内属性添加问题
看下下面这个XML
<?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来区分,达到上述效果
定义一个ElString对象
@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();
}
}
}
将之前Vo里面所有属性的类型都改为这个
新建类,书写string类型转Elstring的方法,使用MapStruct的use属性指定该类,覆盖掉默认的转换方法,这里需要提的就是隐式转换会发生在进入我们书写类的方法之前,比如源Bean字段类型是Integer那么会被先转换为String,然后进入我们的方法,转换为ElString
@Component
public class ElStringMapper {
public ElString toElString(String string){
return new ElString(string);
}
}
OK,这边就差不多结束了