XML解析工具:XStream
虽然现在开发中基本上都是使用JSON作为数据传输的格式,常用的JSON框架比如FastJson, FastJson2, Jackson, Gson等等,但是有时候我们也会用到xml格式用来传输数据,尤其做政府项目的时候很多数据格式都是xml, 所以今天就推荐一个xml的解析工具:XStream。它的功能也比较丰富,具体可以看下官网,它还可以用来解析JSON,只不过很少使用它。
1. XStream 的基本使用
com.thoughtworks.xstream xstream 1.4.20
尽量使用高版本,因为低版本中有很多漏洞,目前最高版本是1.4.20。
1.1 Bean 转 XML
/** * 默认使用全路径作为标签 ... * @XStreamAlias("Person") 可以指定标签的别名 */ @XStreamAlias("Person") @Data public class Person { /** * 默认使用属性名作为标签 ... */ private String idCard; private String name; /** * 自定义类作为属性,包含了name和price属性 */ @XStreamAlias("CAR_INFO") private Car car; /** * 集合属性 */ @XStreamAlias("foodList") private List foodList; } --------------------------------------------- @AllArgsConstructor @Data public class Car { //默认用属性作为标签名,使用 @XStreamAlias("PRICE") 注解可以指定别名 //@XStreamAlias("PRICE") private String price; private String name; } -------------------------------------------- @AllArgsConstructor @Data public class Food { private String id; private String name; }
public class XmlTest { public static void main(String[] args) { Food f1 = new Food("F0001", "烤冷面"); Food f2 = new Food("F0002", "烧烤"); Car car = new Car("宝马", "50万"); Person person = new Person("1000001", "张三", car, Arrays.asList(f1, f2)); String xml = beanToXml(person); System.out.println(xml); } public static String beanToXml(Object obj) { XStream xstream = new XStream(new DomDriver("UTF-8")); //不输出class信息,不然标签中会包含class属性 xstream.aliasSystemAttribute(null, "class"); //支持注解,不然使用的 @XStreamAlias() 注解不会生效,不生效并不会报错,可以测试看下 xstream.autodetectAnnotations(true); return xstream.toXML(obj); } }
1000001 张三 宝马 50万 F0001 烤冷面 F0002 烧烤
用起来还是很简单的
1.2 XML 转 Bean
1000001 张三 宝马 50万 F0001 烤冷面 F0002 烧烤 23
public class XmlTest { public static void main(String[] args) { String xml= " 1000001 张三 宝马 50万 F0001 烤冷面 F0002 烧烤 23 "; Person p = strToBean(xml, Person.class); System.out.println(p); } @SuppressWarnings("unchecked") public static T strToBean(String xmlStr, Class cls) { T targetObject = null; try { XStream xStream = new XStream(); /** * 高版本中为了解决安全漏洞,增加了白名单的机制,这里 * 需要设置权限,不然会报错 */ xStream.addPermission(AnyTypePermission.ANY); xStream.processAnnotations(cls); xStream.autodetectAnnotations(true); targetObject = (T) xStream.fromXML(xmlStr); } catch (Exception e) { e.printStackTrace(); } return targetObject; } }
运行报错:
意思很明显,就是找不到
Person_Info
这个类,这是因为xml结构中的标签是, 但是javaBean中我通过@XStreamAlias("Person")
注解制定的是,所以他俩要保持一致,要将JavaBean中的相关标签与xml标签保持一致。所以将JavaBean调整为:
/** * 默认使用全路径作为标签 ... * @XStreamAlias("Person_Info") 可以指定标签的别名 */ @XStreamAlias("Person_Info") @Data public class Person { /** * 默认使用属性名作为标签 ... */ private String idCard; private String name; /** * 默认使用全路径作为标签 */ @XStreamAlias("CAR_INFO") private Car car; /** * 默认使用属性名作为标签,集合也一样 */ private List foodList; }
运行测试类,依然报错:
从报错信息上很明显的知道,因为xml中有一个
标签,但是
Person
类中没有这个属性,所以导致了报错,此时有两种办法可以解决。第一种就是给Person
类中加上age
属性,另外一种就是如下:
//忽略掉位置的属性 xStream.ignoreUnknownElements();
2. 存在的问题
2.1 序列化时,"_" 会变成 "__"
@AllArgsConstructor @XStreamAlias("S_Student") @Data public class Student { private String name; //main public static void main(String[] args) { Student student = new Student("李四"); String xml = beanToXml(student); System.out.println(xml); } public static String beanToXml(Object obj) { String xmlString = ""; try { XStream xstream = new XStream(new DomDriver("UTF-8")); //支持注解,不然使用的 @XStreamAlias() 注解不会生效 xstream.autodetectAnnotations(true); // 不输出class信息,不然标签中会包含class属性 xstream.aliasSystemAttribute(null, "class"); xmlString = xstream.toXML(obj); } catch (Exception e) { e.printStackTrace(); } return xmlString; } }
解决办法:
在创建XStream对象时,指定NoNameCoder 对象
XStream xstream = new XStream(new DomDriver("UTF-8", new NoNameCoder()));
2.2 反序列化时XML的标签节点与Bean的属性不匹配时报错
前面也有提到过了,如果xml中存在的标签节点在JavaBean中不存在,在XML转Bean的时候将会报错,所以创建XStream对象时可以指定忽略未知属性
XStream xStream = new XStream(new DomDriver("UTF-8", new NoNameCoder())); //忽略未知的属性 xStream.ignoreUnknownElements();
2.3 null值序列化时不会显示标签
参考文档
2.4 自定义转换器完成时间类型的转换
在创建XStream
对象时,其内部会注册大量的转换器,常见类型的转换器基本上都包含了,比如:
看个例子来验证下:
1001 34.06 23.67 3 2023-07-16 17:01:31.285 UTC false
JavaBean:
@XStreamAlias("My_Order") @Data @AllArgsConstructor public class Order { //String类型 private String orderId; //int/Integer 类型 private int count; //Double 类型 private Double price; //BigDecimal类型 private BigDecimal money; //Byte 类型 private Byte flag; //日期类型 private Date buyDate; //Boolean 类型 private Boolean isSuccess; }
测试类:
public class XmlTest { public static void main(String[] args) { String orderXml = "n" + " 1001n" + " 590n" + " 34.06n" + " 23.67n" + " 3n" + " 2023-07-16 17:01:31.285 UTCn" + " falsen" + ""; Order o = strToBean(orderXml, Order.class); System.out.println("order映射结果 = " + JSON.toJSONString(o, JSONWriter.Feature.PrettyFormat)); } @SuppressWarnings("unchecked") public static T strToBean(String xmlStr, Class cls) { T targetObject = null; try { XStream xStream = new XStream(new DomDriver("UTF-8", new NoNameCoder())); xStream.addPermission(AnyTypePermission.ANY); xStream.processAnnotations(cls); xStream.autodetectAnnotations(true); //忽略掉未知的属性 xStream.ignoreUnknownElements(); targetObject = (T) xStream.fromXML(xmlStr); } catch (Exception e) { e.printStackTrace(); } return targetObject; } }
验证结果:
不难发现,基本上所有的类型都可以匹配上。但是日期Date要稍微注意下,如果我换成常见的格式
2023-07-16 17:01:31
就会报错:
不难发现,他默认只能解析它列出的这9种格式的Date类型,并没有我xml中写的 yyyy-MM-dd HH:mm:ss
类型,如果就想支持解析这种格式的Date,可以继承它的com.thoughtworks.xstream.converters.basic.DateConverter
类 或者自定义类型转换器。我这里就不探究了,因为现在使用LocalDate
或者 LocalDateTime
格式的比较多,所以我通过LocalDateTime
来看下如何自定义转换器。
XStream 也提供了
com.thoughtworks.xstream.converters.time.LocalDateTimeConverter
转换器,但是默认也不会解析yyyy-MM-dd HH:mm:ss
, 所以这里就需要我们自定义转换器了。
1. JavaBean:
@XStreamAlias("My_Order") @Data @AllArgsConstructor public class Order { //String类型 private String orderId; //int/Integer 类型 private int count; //Double 类型 private Double price; //BigDecimal类型 private BigDecimal money; //Byte 类型 private Byte flag; /** * 修改为 LocalDateTime 类型 */ private LocalDateTime buyDate; //Boolean 类型 private Boolean isSuccess; }
2. XML:
1001 34.06 23.67 3 2023-07-16 17:01:31.285 UTC true
3. 自定义LocalDateTime类型转换器:
@Slf4j public class LocalDateTimeConverter implements SingleValueConverter { private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override public String toString(Object obj) { try { if (!Objects.isNull(obj) && (obj instanceof LocalDateTime)) { return dateTimeFormatter.format((TemporalAccessor) obj); } } catch (Exception e) { log.error("LocalDateTime 格式转换错误,args: {}", obj); } return null; } @Override public Object fromString(String str) { try { if (StringUtils.isNotBlank(str)) { return LocalDateTime.parse(str, dateTimeFormatter); } } catch (Exception e) { log.error("字符串格式的日期转换LocalDateTime发生错误, args: {}", str); } return null; } @Override public boolean canConvert(Class type) { return type == LocalDateTime.class; } }
4. 解析工具类:
public class XmlToStrTest { public static void main(String[] args) { String orderXml = "n" + " 1001n" + " 590n" + " 34.06n" + " 23.67n" + " 3n" + " 2023-07-16 17:01:31n" + " truen" + ""; Order o = strToBean(orderXml, Order.class); System.out.println("order映射结果 = " + JSON.toJSONString(o, JSONWriter.Feature.PrettyFormat)); } public static T strToBean(String xmlStr, Class cls) { T targetObject = null; try { XStream xStream = new XStream(new DomDriver("UTF-8", new NoNameCoder())); xStream.addPermission(AnyTypePermission.ANY); xStream.processAnnotations(cls); xStream.autodetectAnnotations(true); /** * 自定义类型转换器,优先级高一点,不然还是会使用框架的转换器 * */ xStream.registerConverter(new LocalDateTimeConverter(), XStream.PRIORITY_VERY_HIGH); targetObject = (T) xStream.fromXML(xmlStr); } catch (Exception e) { e.printStackTrace(); } return targetObject; } }
5. 解析结果:
自定义的LocalDateTime类型转换器已经生效了。
注意:上面这种注册转换器的方式是全局生效的,如果只是不想全局生效,只想作用在某一个属性上,可以使用 【@XStreamConverter】 注解:
@XStreamAlias("My_Order") @Data @AllArgsConstructor public class Order { //String类型 private String orderId; //.....其他属性 /** * @XStreamConverter(value = LocalDateTimeConverter.class) * 只针对当前属性生效 */ @XStreamConverter(value = LocalDateTimeConverter.class) private LocalDateTime buyDate; //.....其他属性 }
3. 常用注解
注解 | 说明 |
---|---|
@XStreamAlias |
这个是最常用的注解,用来指定匹配标签节点的名称,如果不指定默认为全类名或者属性名 |
@XStreamAsAttribute |
标注该注解的属性将成为标签节点的属性,不再是一个单独的节点 |
@XStreamConverter |
指定类型转换器,只对当前属性或当前类生效 |
@XStreamImplicit |
用于定义集合,数组,Map字段的序列化方式 |
@XStreamOmitField |
用于标记某个字段不参与序列化和反序列化 |
通过一个例子来演示看下:
@AllArgsConstructor @Data @XStreamAlias("A_TEACHER") public class Teacher { /** * 将name作为根节点A_TEACHER的属性 */ @XStreamAsAttribute private String name; /** * 序列化和反序列时忽略 */ @XStreamOmitField private int age; private Double salary; private String color; /** * 指定元素的字段名 * 如果不指定的话,默认使用类型作为标签名 ... */ @XStreamImplicit(itemFieldName = "likes") private List likes; /** * 注意看 xml */ @XStreamImplicit(itemFieldName = "FOOD_LIST") private List foods; }
序列化后的xml:
35000.0 黑色 读书 讲课 拍视频 F0001 炸酱面 F0002 卤煮 F0003 烧烤 F0004 烤饼
4.开箱即用的工具类
public class XmlParseUtils { /** * 反序列化 */ @SuppressWarnings("unchecked") public static T strToBean(String xmlStr, Class cls) { T targetObject = null; try { XStream xstream = new XStream(new DomDriver("UTF-8", new NoNameCoder())); xstream.ignoreUnknownElements(); xstream.addPermission(AnyTypePermission.ANY); //和上面一样,也是用来解决高版本中安全问题的 //xstream.allowTypesByWildcard(new String[]{"com.qiuguan.**"}); xstream.processAnnotations(cls); xstream.autodetectAnnotations(true); xstream.registerConverter(new LocalDateTimeConverter(), XStream.PRIORITY_VERY_HIGH); //框架自带的Boolean类型转换器,可以将xml中的 yes 转成true, 将 no 转成false, 默认是将 "true"字符串转成boolean类型的true xstream.registerConverter(new BooleanConverter("yes", "no", false)); targetObject = (T) xstream.fromXML(xmlStr); } catch (Exception e) { e.printStackTrace(); } return targetObject; } /** * 序列化 */ public static String beanToXml(Object obj) { String xmlString = ""; try { XStream xstream = new XStream(new DomDriver("UTF-8", new NoNameCoder())); //支持注解,不然使用的 @XStreamAlias() 注解不会生效 xstream.autodetectAnnotations(true); // 不输出class信息,不然标签中会包含class属性 xstream.aliasSystemAttribute(null, "class"); //自定义类型转换器,优先级要高,不然还是会使用框架的转换器 //这里如果不注册的话,序列化的时候自定义的转换器就不会生效了,只在反序列化的时候生效 xstream.registerConverter(new LocalDateTimeConverter(), XStream.PRIORITY_VERY_HIGH); //框架自带的Boolean类型转换器,序列化时可以将 yes 转成true, 将 no 转成false, 默认是将 "true"字符串转成boolean类型的true xstream.registerConverter(new BooleanConverter("yes", "no", false)); //注册null值序列化时也可以显示标签的转换器(参考文档) //xstream.registerConverter(new NullConverter(xStream.getMapper(), new SunUnsafeReflectionProvider())); xmlString = xstream.toXML(obj); } catch (Exception e) { e.printStackTrace(); } return xmlString; } }
github传送门
gitee传送门
好了,关于Xstream就介绍到这里吧,欢迎大家批评指正。