XML解析工具:XStream

虽然现在开发中基本上都是使用JSON作为数据传输的格式,常用的JSON框架比如FastJson, FastJson2, Jackson, Gson等等,但是有时候我们也会用到xml格式用来传输数据,尤其做政府项目的时候很多数据格式都是xml, 所以今天就推荐一个xml的解析工具:XStream。它的功能也比较丰富,具体可以看下官网,它还可以用来解析JSON,只不过很少使用它。

1. XStream 的基本使用

xml 复制代码
<dependency>  
    <groupId>com.thoughtworks.xstream</groupId>  
    <artifactId>xstream</artifactId>  
    <version>1.4.20</version>  
</dependency>

尽量使用高版本,因为低版本中有很多漏洞,目前最高版本是1.4.20。

1.1 Bean 转 XML

  1. 先定义一个实体Bean
java 复制代码
/**  
 * 默认使用全路径作为标签 <com.qiuguan.xml.test.Perosn>...  </com.qiuguan.xml.test.Perosn>  
 * @XStreamAlias("Person") 可以指定标签的别名  
 */  
@XStreamAlias("Person")
@Data
public class Person {  
  
    /**  
     * 默认使用属性名作为标签 <idCard>...</idCard>  
     */  
    private String idCard;  

    private String name;  

    /**  
     * 自定义类作为属性,包含了name和price属性
     */  
    @XStreamAlias("CAR_INFO")  
    private Car car;  


    /**  
     * 集合属性
     */  
    @XStreamAlias("foodList")
    private List<Food> 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;
}
  1. 测试解析成XML
java 复制代码
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);
   }
}
  1. 输出结果
xml 复制代码
<Person>
    <!-- 默认使用属性名作为标签 -->
    <idCard>1000001</idCard>
    <name>张三</name>
    <!-- @XStreamAlias("CAR_INFO") -->
    <CAR_INFO>
        <name>宝马</name>
        <price>50万</price>
    </CAR_INFO>
    <!-- 默认使用属性名作为标签 -->
    <foodList>
        <food>
            <id>F0001</id>
            <name>烤冷面</name>
        </food>
        <food>
            <id>F0002</id>
            <name>烧烤</name>
        </food>
    </foodList>
</Person>

用起来还是很简单的

1.2 XML 转 Bean

  1. 先提供一个xml结构
xml 复制代码
<Person_Info>
    <idCard>1000001</idCard>
    <name>张三</name>
    <CAR>
        <name>宝马</name>
        <price>50万</price>
    </CAR>
    <foodList>
        <food>
            <id>F0001</id>
            <name>烤冷面</name>
        </food>
        <food>
            <id>F0002</id>
            <name>烧烤</name>
        </food>
    </foodList>
    <age>23</age>
</Person_Info>
  1. Bean结构还是和上面一样。
  2. 我们写一个解析工具类看下
java 复制代码
public class XmlTest {

   public static void main(String[] args) {
       String xml= 
         "<Person_Info>
            <idCard>1000001</idCard>
            <NAME>张三</NAME>
            <CAR>
                <name>宝马</name>
                <price>50万</price>
            </CAR>
            <FoodList_Infos>
                <food>
                    <id>F0001</id>
                    <name>烤冷面</name>
                </food>
                <food>
                    <id>F0002</id>
                    <name>烧烤</name>
                </food>
            </FoodList_Infos>
            <age>23</age>
          </Person_Info>";
          
       Person p = strToBean(xml, Person.class);
       System.out.println(p);
   }
   
    @SuppressWarnings("unchecked")  
    public static <T> T strToBean(String xmlStr, Class<T> 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结构中的标签是<Person_Info>, 但是javaBean中我通过@XStreamAlias("Person")注解制定的是 <Person>,所以他俩要保持一致,要将JavaBean中的相关标签与xml标签保持一致。所以将JavaBean调整为:

java 复制代码
/**  
 * 默认使用全路径作为标签 <com.qiuguan.xml.test.Perosn>...</com.qiuguan.xml.test.Perosn>  
 * @XStreamAlias("Person_Info") 可以指定标签的别名  
 */  
@XStreamAlias("Person_Info")  
@Data  
public class Person {  
  
    /**  
     * 默认使用属性名作为标签 <idCard>...</idCard>  
     */  
    private String idCard;  
 
    private String name;  

    /**  
     * 默认使用全路径作为标签  
     */  
    @XStreamAlias("CAR_INFO")  
    private Car car;  

    /**  
     * 默认使用属性名作为标签,集合也一样  
     */  
    private List<Food> foodList;  
}

运行测试类,依然报错:

从报错信息上很明显的知道,因为xml中有一个<age></age>标签,但是Person类中没有这个属性,所以导致了报错,此时有两种办法可以解决。第一种就是给Person类中加上age属性,另外一种就是如下:

java 复制代码
//忽略掉位置的属性  
xStream.ignoreUnknownElements();

2. 存在的问题

2.1 序列化时,"_" 会变成 "__"

java 复制代码
@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 对象

java 复制代码
XStream xstream = new XStream(new DomDriver("UTF-8", new NoNameCoder()));

2.2 反序列化时XML的标签节点与Bean的属性不匹配时报错

前面也有提到过了,如果xml中存在的标签节点在JavaBean中不存在,在XML转Bean的时候将会报错,所以创建XStream对象时可以指定忽略未知属性

java 复制代码
XStream xStream = new XStream(new DomDriver("UTF-8", new NoNameCoder()));  
//忽略未知的属性    
xStream.ignoreUnknownElements();    

2.3 null值序列化时不会显示标签

参考文档

2.4 自定义转换器完成时间类型的转换

在创建XStream对象时,其内部会注册大量的转换器,常见类型的转换器基本上都包含了,比如:

看个例子来验证下:

xml 复制代码
   <My_Order>
      <orderId>1001</orderId>
      <price>34.06</price>
      <money>23.67</money>
      <flag>3</flag>
      <buyDate>2023-07-16 17:01:31.285 UTC</buyDate>
      <isSuccess>false</isSuccess>
   </My_Order>

JavaBean:

java 复制代码
@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;  
}

测试类:

java 复制代码
public class XmlTest {  
  
    public static void main(String[] args) {  

       String orderXml = 
           "<My_Order>\n" +  
                " <orderId>1001</orderId>\n" +  
                " <count>590</count>\n" +  
                " <price>34.06</price>\n" +  
                " <money>23.67</money>\n" +  
                " <flag>3</flag>\n" +  
                " <buyDate>2023-07-16 17:01:31.285 UTC</buyDate>\n" +  
                " <isSuccess>false</isSuccess>\n" +  
            "</My_Order>";

        Order o = strToBean(orderXml, Order.class);  
        System.out.println("order映射结果 = " + JSON.toJSONString(o, JSONWriter.Feature.PrettyFormat));
   }  
  
    @SuppressWarnings("unchecked")  
    public static <T> T strToBean(String xmlStr, Class<T> 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:

java 复制代码
@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:

xml 复制代码
   <My_Order>
      <orderId>1001</orderId>
      <price>34.06</price>
      <money>23.67</money>
      <flag>3</flag>
      <buyDate>2023-07-16 17:01:31.285 UTC</buyDate>
      <isSuccess>true</isSuccess>
   </My_Order>

3. 自定义LocalDateTime类型转换器:

java 复制代码
@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. 解析工具类:

java 复制代码
public class XmlToStrTest {

    public static void main(String[] args) {

        String orderXml = "<My_Order>\n" +
                "  <orderId>1001</orderId>\n" +
                "  <count>590</count>\n" +
                "  <price>34.06</price>\n" +
                "  <money>23.67</money>\n" +
                "  <flag>3</flag>\n" +
                "  <buyDate>2023-07-16 17:01:31</buyDate>\n" +
                "  <isSuccess>true</isSuccess>\n" +
                "</My_Order>";


        Order o = strToBean(orderXml, Order.class);
        System.out.println("order映射结果 = " + JSON.toJSONString(o, JSONWriter.Feature.PrettyFormat));
    }

    public static <T> T strToBean(String xmlStr, Class<T> 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】 注解

java 复制代码
@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 用于标记某个字段不参与序列化和反序列化

通过一个例子来演示看下:

java 复制代码
@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;

    /**
     * 指定元素的字段名
     * 如果不指定的话,默认使用类型作为标签名 <String>...</String>
     */
    @XStreamImplicit(itemFieldName = "likes")
    private List<String> likes;

    /**
     * 注意看 xml
     */
    @XStreamImplicit(itemFieldName = "FOOD_LIST")
    private List<Food> foods;

}

序列化后的xml:

xml 复制代码
<!--name作为节点的属性,不再单独作为一个节点-->
<A_TEACHER name="马云">
    <!--age属性不参与序列化和反序列化-->
    <salary>35000.0</salary>
    <color>黑色</color>
    <!--指定集合元素的名称为 likeItem -->
    <likeItem>读书</likeItem>
    <likeItem>讲课</likeItem>
    <likeItem>拍视频</likeItem>
	  <!-- 通过 @XStreamImplicit注解标注后,集合中每一个对象都是被 <FOOD_LIST> 标签包裹 -->
    <FOOD_LIST>
        <id>F0001</id>
        <name>炸酱面</name>
    </FOOD_LIST>
    <FOOD_LIST>
        <id>F0002</id>
        <name>卤煮</name>
    </FOOD_LIST>
	  <!-- 通过 @XStreamAlias注解标注后,所有对象属于同一个集合,每个对象的标签名也可以自定义 -->
    <FOOD_LIST2>
        <FOOD>
            <id>F0003</id>
            <name>烧烤</name>
        </FOOD>
        <FOOD>
            <id>F0004</id>
            <name>烤饼</name>
        </FOOD>
    </FOOD_LIST2>
</A_TEACHER>

4.开箱即用的工具类

java 复制代码
public class XmlParseUtils {

    /**
     * 反序列化
     */
    @SuppressWarnings("unchecked")
    public static <T> T strToBean(String xmlStr, Class<T> 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就介绍到这里吧,欢迎大家批评指正。

相关推荐
why技术20 分钟前
可以说是一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
后端·面试
m0_7482546624 分钟前
定时任务特辑 Quartz、xxl-job、elastic-job、Cron四个定时任务框架对比,和Spring Boot集成实战
java·spring boot·后端
海边漫步者39 分钟前
Idea2024中搭建JavaFX开发环境并创建运行项目
java·intellij-idea·javafx
diemeng11191 小时前
2024系统编程语言风云变幻:Rust持续领跑,Zig与Ada异军突起
开发语言·前端·后端·rust
Warren981 小时前
Springboot中分析SQL性能的两种方式
java·spring boot·后端·sql·mysql·intellij-idea
Distance失落心2 小时前
idea任意版本的安装
java·ide·java-ee·eclipse·intellij-idea
Aphelios3802 小时前
Linux 下 VIM 编辑器学习记录:从基础到进阶(下)
java·linux·学习·编辑器·vim
独孤求败Ace2 小时前
第46天:Web开发-JavaEE应用&原生和FastJson反序列化&URLDNS链&JDBC链&Gadget手搓
java·spring·java-ee
Ting-yu2 小时前
项目实战--网页五子棋(匹配模块)(4)
java
优人ovo2 小时前
3分钟idea接入deepseek
java·ide·intellij-idea