前置准备
导入依赖, 去Maven仓库搜索 XStrean ,下载核心依赖, 这里以 1.4.19 版本为例。
<!-- https://mvnrepository.com/artifact/com.thoughtworks.xstream/xstream -->
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.19</version>
</dependency>
简单使用(非注解开发)
通过操作 XStream 类完成对 Xml 和 实体 的转换。
定义实体
定义Xml报文实体, 组装Xml报文格式,以如下格式为例
<Root>
<count>1</count>
<serviceInfo uid="B31FB4508BE82F2E6FB761E067E60CF">
<serviceName>服务名称</serviceName>
<updateTime>2024-04-11T14:22:39.559</updateTime>
<Attr id="1">
<attrName>事项材料名称1</attrName>
<attrCode>事项材料编码1</attrCode>
</Attr>
<Attr id="2">
<attrName>事项材料名称2</attrName>
<attrCode>事项材料编码2</attrCode>
</Attr>
</serviceInfo>
</Root>
定义最外层实体类 Root
@Data
@ToString
@EqualsAndHashCode
public class Root {
Integer total = 1;
ServiceInfo serviceInfo = new ServiceInfo();
private String exclouldField = "exclouldField";
}
定义 ServiceInfo 类
@Data
@ToString
@EqualsAndHashCode
public class ServiceInfo {
private String unid = "B31FB4508BE82F2E6FB761E067E60CF";
private String serviceName = "服务名称";
private LocalDateTime updateTime = LocalDateTime.now();
private List<Attr> attrs = new ArrayList<>();
public ServiceInfo() {
attrs.add(new Attr(1, "事项材料名称1", "事项材料编码1"));
attrs.add(new Attr(2, "事项材料名称2", "事项材料编码2"));
}
}
定义 Attr 类
@Data
@ToString
@EqualsAndHashCode
public class Attr {
private Integer id;
private String attrName;
private String attrCode;
private String exclouldField = "exclouldField";
public Attr(Integer id, String attrName, String attrCode) {
this.id = id;
this.attrName = attrName;
this.attrCode = attrCode;
}
}
定义主方法, 生成 Xml 报文
public static void main(String[] args) {
//定义一个 Xml 实体
Root root = new Root();
XStream xstream = new XStream();
//将实体转换为 Xml 报文
String s = xstream.toXML(root);
System.out.println(s);
}
生成的报文如下所示:
<com.example.springbootdemo.xml.xstream.Root>
<total>1</total>
<serviceInfo>
<unid>B31FB4508BE82F2E6FB761E067E60CF</unid>
<serviceName>服务名称</serviceName>
<updateTime>2024-04-11T15:34:31.984</updateTime>
<attrs>
<com.example.springbootdemo.xml.xstream.Attr>
<id>1</id>
<attrName>事项材料名称1</attrName>
<attrCode>事项材料编码1</attrCode>
<exclouldField>exclouldField</exclouldField>
</com.example.springbootdemo.xml.xstream.Attr>
<com.example.springbootdemo.xml.xstream.Attr>
<id>2</id>
<attrName>事项材料名称2</attrName>
<attrCode>事项材料编码2</attrCode>
<exclouldField>exclouldField</exclouldField>
</com.example.springbootdemo.xml.xstream.Attr>
</attrs>
</serviceInfo>
<exclouldField>exclouldField</exclouldField>
</com.example.springbootdemo.xml.xstream.Root>
我们发现,生成的报文并没有按照我们的意愿生成,这里涉及到XStream 的几个知识点。
类生成标签时,如果未指定别名,那么会以类的全路径(即:包名.类名)作为标签。
方案一 :
调用aliasPackage(...)
方法,去除掉包前缀, 此包下面的所有类被序列化时都会被剔除掉包名前缀, 如xstream.aliasPackage("", "com.example.springbootdemo.xml.xstream")
表示将 com.example.springbootdemo.xml.xstream.Root 序列化为 Root
方案二:
调用alias(...)
方法, 给类取别名。如xstream.alias("Root", Root.class)
表示Root 类序列化时,标签名为 Root类属性(字段)生成标签时,如果未指定别名,那么会以属性(字段)名作为标签。
方案一 :
调用aliasField(...)
方法,给字段取别名, 如:xstream.aliasField("count", Root.class, "total")
表示将Root类中的Total字段序列化为count。如果要排除掉实体中的某个字段,可以调用
omitField(...)
方法。如xstream.omitField(Root.class, "exclouldField")
表示Xml序列化时,忽略Root类中的exclouldField字段。如果要增加形如 < Attr id="1"> 的标签属性,可以通过
useAttributeFor(...)
和aliasAttribute(...)
方法实现。
useAttributeFor(...)
xstream.useAttributeFor(Attr.class, "id")
表示将Attr类中的id字段作为标签属性,形如< Atrr id=""/>
aliasAttribute(...)
xstream.aliasAttribute(ServiceInfo.class, "unid", "uid")
表示将ServiceInfo类中的unid字段作为标签属性, 且属性名称自定义为uid,形如< ServiceInfo uid=""/>可以通过
addImplicitCollection(...)
方法去除集合标签, 如xstream.addImplicitCollection(ServiceInfo.class, "attrs")
表示去除集合标签,生成的Xml去除掉外层的attrs标签,直接取内层数据展示在外层。
添加addImplicitCollection(...)
方法前的Xml报文
<Root>
<count>1</count>
<serviceInfo uid="B31FB4508BE82F2E6FB761E067E60CF">
<serviceName>服务名称</serviceName>
<updateTime>2024-04-11T16:46:00.121</updateTime>
<attrs>
<Attr id="1">
<attrName>事项材料名称1</attrName>
<attrCode>事项材料编码1</attrCode>
<exclouldField>exclouldField</exclouldField>
</Attr>
<Attr id="2">
<attrName>事项材料名称2</attrName>
<attrCode>事项材料编码2</attrCode>
<exclouldField>exclouldField</exclouldField>
</Attr>
</attrs>
</serviceInfo>
<exclouldField>exclouldField</exclouldField>
</Root>
添加addImplicitCollection(...)
方法后的Xml报文
<Root>
<count>1</count>
<serviceInfo uid="B31FB4508BE82F2E6FB761E067E60CF">
<serviceName>服务名称</serviceName>
<updateTime>2024-04-11T16:47:42.472</updateTime>
<Attr id="1">
<attrName>事项材料名称1</attrName>
<attrCode>事项材料编码1</attrCode>
<exclouldField>exclouldField</exclouldField>
</Attr>
<Attr id="2">
<attrName>事项材料名称2</attrName>
<attrCode>事项材料编码2</attrCode>
<exclouldField>exclouldField</exclouldField>
</Attr>
</serviceInfo>
<exclouldField>exclouldField</exclouldField>
</Root>
不难发现, 添加后的报文中少了< attrs> 标签, 即集合的那一层标签被剔除掉了。
使用上述知识点重新编写测试用例, 进行一系列属性设置后,我们便能得到预期的输出了, 将输出的Xml报文传入 XStream 的fromXML(...)
方法即可反序列化成之前的实体。
public static void main(String[] args) {
Root root = new Root();
XStream xstream = new XStream();
//去除包名前缀
xstream.aliasPackage("", "com.example.springbootdemo.xml.xstream");
//可使用类名替代
//xstream.alias("Root", Root.class);
//xstream.alias("Attr", Attr.class);
//属性别名,
// 将Root类中的Total字段在Xml中以count显示
xstream.aliasField("count", Root.class, "total");
//排除属性
//生成Xml时,忽略Root类中的exclouldField字段
xstream.omitField(Root.class, "exclouldField");
//生成Xml时,忽略Attr类中的exclouldField字段
xstream.omitField(Attr.class, "exclouldField");
//标签内属性
//将Attr类中的id字段作为标签属性,形如<Atrr id=""/>
xstream.useAttributeFor(Attr.class, "id");
//将ServiceInfo类中的unid字段作为标签属性, 且属性名称自定义,形如<ServiceInfo uid=""/>
xstream.aliasAttribute(ServiceInfo.class, "unid", "uid");
//去除集合标签,生成的Xml去除掉外层的attrs标签,直接取内层数据展示在外层
xstream.addImplicitCollection(ServiceInfo.class, "attrs");
String s = xstream.toXML(root);
System.out.println(s);
xstream.addPermission(AnyTypePermission.ANY);
//xstream.allowTypesByWildcard(new String[]{pkgName});
xstream.processAnnotations(Root.class);
//忽略掉未知的标签,否则如果实体没有将所有标签囊括在内时会触发异常
xstream.ignoreUnknownElements();
Object res = xstream.fromXML(s);
System.out.println(res.toString());
}
注解开发
首先了解一下常用的一些注解
常用注解
@XStreamAlias
用来指定类或者字段的别名,用于序列化时展示, 作用在类上时与aliasField(...)
方法作用相同, 作用在字段上时与aliasField(...)
方法作用相同。
@XStreamAsAttribute
该注解标注的字段将作为类标签的属性存在,不再作为单独的标签。作用与aliasAttribute(...)
和useAttributeFor(...)
方法类似。
@XStreamAsAttribute
该注解标注的字段会被忽略,不参加Xml的序列化和反序列化。作用与omitField(...)
方法相同。
@XStreamImplicit
用于定义集合、数组、Map的序列化方式。
注意:以如下为例, @XStreamImplicit(itemFieldName = "attr")
注解的 itemFieldName
属性会覆盖掉内部 Attr 类上的 @XStreamAlias("_attr")
注解, 且 attrs标签将不会出现,< attrs>< _attr><attrName></attrName></_attr></attrs>
格式会被替换为 < attr><attrName></attrName>< /attr>
格式
@XStreamImplicit(itemFieldName = "attr")
private List<Attr> attrs = new ArrayList<>();
@XStreamAlias("_attr")
public class Attr {
@XStreamAlias("attrName")
private String attrName;
}
@XStreamConverter
用于指定类型转化器。
定义实体
定义 Root 类
@Data
@ToString
@EqualsAndHashCode
@XStreamAlias("root")
public class Root {
@XStreamAlias("total")
Integer total = 1;
@XStreamAlias("serviceinfo")
ServiceInfo serviceInfo = new ServiceInfo();
@XStreamOmitField
private String exclouldField = "";
}
定义 ServiceInfo 类
@Data
@ToString
@EqualsAndHashCode
@XStreamAlias("serviceinfo")
public class ServiceInfo {
@XStreamAsAttribute
private String unid = "B31FB4508BE82F2E6FB761E067E60CF";
@XStreamAlias("serviceName")
private String serviceName = "服务名称";
@XStreamAlias("updateTime")
//@XStreamConverter(LocalDateTimeConverter.class)
private LocalDateTime updateTime = LocalDateTime.now();
//注解属性都为空时,去除掉外层属性, itemFieldName 属性有值时,内层类 Attr 上的@XStreamAlias注解失效,被itemFieldName取代
@XStreamImplicit(itemFieldName = "attr")
private List<Attr> attrs = new ArrayList<>();
public ServiceInfo() {
attrs.add(new Attr(1, "事项材料名称1", "事项材料编码1"));
attrs.add(new Attr(2, "事项材料名称2", "事项材料编码2"));
}
}
定义 Attr 类
@Data
@ToString
@EqualsAndHashCode
@XStreamAlias("_attr")
public class Attr {
@XStreamAsAttribute
private Integer id;
@XStreamAlias("attrName")
private String attrName;
@XStreamAlias("attrCode")
private String attrCode;
@XStreamOmitField
private String exclouldField = "exclouldField";
public Attr(Integer id, String attrName, String attrCode) {
this.id = id;
this.attrName = attrName;
this.attrCode = attrCode;
}
}
编写工具类 XStreamUtil
public class XStreamUtil {
//new NoNameCoder() 解决 _ 被序列化成 __ 的问题
private static final NoNameCoder noNameCoder = new NoNameCoder();
private static XStream xStream = new XStream(new DomDriver("UTF-8", noNameCoder));
private static XStream getxStream(HierarchicalStreamDriver driver){
if(Objects.isNull(driver)){
return xStream;
}
return new XStream(driver);
}
public static <T> T unmarshal(String pkgName, Class<T> cla, String xmlStr, HierarchicalStreamDriver driver) {
xStream = getxStream(driver);
if (StringUtils.isEmpty(pkgName)) {
//高版本为了解决安全漏洞,增加了白名单机制, 如果不设置这个权限可能会报错
xStream.addPermission(AnyTypePermission.ANY);
} else {
//设置允许解析的包,如果不想设置可以用 addPermission(AnyTypePermission.ANY) 代替
xStream.allowTypesByWildcard(new String[]{pkgName});
}
//支持注解,不然使用的XStream注解不会生效且不报错
xStream.autodetectAnnotations(true);
xStream.processAnnotations(cla);
//忽略未知属性, 如果不添加这个,当Xml报文中出现实体中没有的属性时会报错 No such field
xStream.ignoreUnknownElements();
return (T) xStream.fromXML(xmlStr);
}
public static <T> T unmarshal(String pkgName, Class<T> cla, String xmlStr) {
return unmarshal(pkgName, cla, xmlStr, null);
}
public static <T> T unmarshal(Class<T> cla, String xmlStr) {
return unmarshal(null, cla, xmlStr, null);
}
public static String marshal(Object o, DomDriver driver) {
xStream = getxStream(driver);
//支持注解,不然使用的 @XStreamAlias() 注解不会生效而且不会报错
xStream.autodetectAnnotations(true);
//注册自定义时间转换器,使得XStream全局支持LocalDateTime, 或者不在这里注册,使用@XStreamConverter(LocalDateTimeConverter.class)注解在单一字段上
xStream.registerConverter(new LocalDateTimeConverter());
return xStream.toXML(o);
}
public static String marshal(Object o){
return marshal(o, null);
}
}
测试
public static void main(String[] args) {
Root root = new Root();
String s = XStreamUtil.marshal(root);
System.out.println(s);
Root res = XStreamUtil.unmarshal(Root.class, s);
System.out.println(res.toString());
}
可能遇到的问题
1、注解不生效
描述:
注解在类和字段上的注解不生效,生成的Xml报文是以默认方式生成的。
解决方案:
需要将XStream 的注解开关打开,让他支持注解。
//支持注解,不然使用的注解不会生效而且不会报错
xStream.autodetectAnnotations(true);
2、序列化时 _ 被序列化成 __
描述:
@XStreamAlias("_attr") 注解的属性 ”_attr“ 被 序列化为 ”__attr“
解决方法:
创建XStream 对象时,指定NoNameCoder对象。
//new NoNameCoder() 解决 _ 被序列化成 __ 的问题
private static final NoNameCoder noNameCoder = new NoNameCoder();
private static XStream xStream = new XStream(new DomDriver("UTF-8", noNameCoder));
3、反序列化时实体缺失Xml报文属性报错 No such field
解决方法:
忽略未知属性。
//忽略未知属性, 如果不添加这个,当Xml报文中出现实体中没有的属性时会报错 No such field
xStream.ignoreUnknownElements();
4、com.thoughtworks.xstream.security.ForbiddenClassException
描述:高版本为了解决安全漏洞,增加了白名单机制。
解决方法:
方案一: 调用addPermission(...)
方法,设置XStream权限为AnyTypePermission.ANY
。
方案二: 设置允许解析的包, 将需要使用的实体囊括其中。
//高版本为了解决安全漏洞,增加了白名单机制, 如果不设置这个权限可能会报错
xStream.addPermission(AnyTypePermission.ANY);
//设置允许解析的包,如果不想设置可以用 addPermission(AnyTypePermission.ANY) 代替
xStream.allowTypesByWildcard(new String[]{pkgName});
5、指定的时间格式不被支持
描述:
XStream 支持的时间格式是标准时间格式,如果指定时间格式字段要进行反序列化,可能不被支持。
解决方案:
自定义时间转换器,注册到XStream中,或者使用@XStreamConverter注解指定目标字段的转换器。
以LocalDateTime 和 yyyy-MM-dd HH:mm:ss 格式的数据转换为例。
自定义时间转换器 LocalDateTimeConverter
public class LocalDateTimeConverter implements SingleValueConverter {
private static final DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public String toString(Object o) {
try {
if (Objects.nonNull(o) && (o instanceof LocalDateTime)) {
return pattern.format((TemporalAccessor) o);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public Object fromString(String s) {
try {
if (StringUtils.isNotBlank(s)) {
return LocalDateTime.parse(s, pattern);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public boolean canConvert(Class type) {
return type == LocalDateTime.class;
}
}
方案一: 注解到目标字段
@XStreamAlias("updateTime")
//@XStreamConverter(LocalDateTimeConverter.class)
private LocalDateTime updateTime = LocalDateTime.now();
方案二: 将时间转换器注册到XStream对象
//注册自定义时间转换器,使得XStream全局支持LocalDateTime, 或者不在这里注册,使用@XStreamConverter(LocalDateTimeConverter.class)注解在单一字段上
xStream.registerConverter(new LocalDateTimeConverter());
6、空值标签丢失
描述:
值为null的字段在序列化时会被丢弃。
解决方案:
简单类型如String可以给其赋个空字符串,如果是复杂类型的话考虑自定义一个空值转换器注册到XStream对象中。