XML解析工具:XStream

michael-kroul-bTZU6EwVEQs-unsplash.jpg

虽然现在开发中基本上都是使用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

  • 先定义一个实体Bean
  • /**
    * 默认使用全路径作为标签 ...
    * @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;
    }
  • 测试解析成XML
  • 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

  • 先提供一个xml结构
  • 1000001
    张三
    宝马
    50万
    F0001
    烤冷面
    F0002
    烧烤
    23
  • Bean结构还是和上面一样。
  • 我们写一个解析工具类看下
  • 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;
    }
    }

    运行报错:
    image.png

    意思很明显,就是找不到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;
    }

    运行测试类,依然报错:
    image.png

    从报错信息上很明显的知道,因为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;
    }
    }

    image.png

    解决办法:

    在创建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对象时,其内部会注册大量的转换器,常见类型的转换器基本上都包含了,比如:

    image.png

    看个例子来验证下:

    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;
    }
    }

    验证结果:
    image.png

    不难发现,基本上所有的类型都可以匹配上。但是日期Date要稍微注意下,如果我换成常见的格式 2023-07-16 17:01:31 就会报错:

    image.png

    不难发现,他默认只能解析它列出的这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. 解析结果:
    image.png

    自定义的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就介绍到这里吧,欢迎大家批评指正。