反射+XML4J,打造自动把任意对象输出成XML文件的小工具


今天学习XML,做了个通过反射可以把任意对象写出到XML文件的小工具。抛砖引玉,把思路分析一下,给各位读者参考。

完成效果:

先来看看完成效果。

先创建任意的javaBean,可以嵌套其他的javaBean:

public class Student {
    private int id;
    private String nickName;
    private String name;
    private int age;
    private String gender;
    private Teacher teacher;    //可以嵌套另一个javaBean
    // 构造函数和getter,setter等方法
}
// 其中的id和nickName我希望是作为属性可以贴在其他标签中。

public class Teacher {
    private String name;
    private String hobby;
    private int age;
    // 构造函数和getter,setter等方法
}

这个工具既可以只生成一个对象,也可以生成对象列表,先看生成一个对象的效果:

@Test
public void testWriteOne() throws ... {
    Teacher teacher = new Teacher("刘老师", "羽毛球", 24);
    Student student = new Student(4, "老王", "陈奕迅", 23, "男", teacher);
    XML_ObjectWriter.write(new FileOutputStream("oneObject.xml"), student);
}

运行结果:
单个对象插入效果

可以看到teacher被嵌套到Student中了,另外idnickName也作为属性。

接下来测试把列表变成XML文件:

@Test
public void testWriteList() throws ... {
    Teacher teacher = new Teacher("刘老师", "羽毛球", 24);
    Student student = new Student(4, "老王", "陈奕迅", 23, "男", teacher);
    Student student2 = new Student(4, "老王", "陈奕迅", 23, "男", teacher);
    Student student3 = new Student(4, "老王", "陈奕迅", 23, "男", teacher);
    ArrayList<Student> students = new ArrayList<>();
    Collections.addAll(students, student, student2, student3);
    //创建一个student列表,都是引用老师
    XML_ObjectWriter.write(new FileOutputStream("test2.xml"), students);
}

运行!…一个格式优良的XML文件就出来啦!
在这里插入图片描述

下面开始讲解…

关键方法:

//创建一个节点
Element DocumentHelper.createElement(String elementName);  

//创建一个文档
Document DocumentHelper.createDocument();  

//可以格式化XML文档的对象,可以自动帮我们补全注释格式,文档声明等信息。
OutputFormat OutputFormat.createPrettyPrint()

//写出对象到xml文件的核心对象
XMLWriter xmlWriter=new XMLWriter(Outputwriter, OutputFormat)

//得到一个javaBean类的详细信息
BeanInfo Introspector.getBeanInfo(Class<?> class) ;    

//得到javaBean的属性
PropertyDescriptor[] beanInfo.getPropertyDescriptors()

思路讲解:

工作大致可以分为3步:

  1. 分析对象,得到各项属性和他们的关系:谁是标签,谁是标签的属性。

  2. 根据分析结果,在内存中生成一棵DOM树。

  3. 把DOM树输出到文件。

反射可以得到javaBean的各项属性,把javaBean的属性名称作为标签名,javaBean属性的值作为内容。然而此案例的难点是:

怎样让解释器知道,哪个javaBean的属性是xml中的标签,哪个javaBean的属性是xml中的属性,这个属性又应该贴在哪个标签中呢

这种需要给反射增加信息的工作,谁最擅长呢?!

答案就是注解了!所以我们应该依靠注解,来把信息传给解释器,就能让解释器知道javaBean属性的更多信息。

开始

第一步:定义注解

我们可以这样操作:正常情况下属性都默认是xml中的标签,属性值就是标签的内容。而当一个属性被贴上了某个标签,就说明这个属性是xml中的属性,同时标签的内容还能指定这个xml的属性属于哪个标签。(好绕哦)

//定以用于标注属性
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Attribute {
    String pastedOn() default "this";  
}

定义注解很简单吧,pastedOn表示这个属性应该依附在哪个标签下。当然用value可以省略。

pastedOn默认值是this,表示把属性贴在对象标签本身。下面我们就可以给javaBean 的属性提供更多信息了。

@Attribute
public int getId() {
   return id;
}

@Attribute(pastedOn = "name")
public String getNickName() {
   return nickName;
}

只把我们需要的两个属性贴上注解,其他getter方法不用管,就表示其他属性都认为是标签。

第二步:分析对象

propertyDescriptor就是属性描述器,他储存着属性的name,和getter,setter 方法,是反射操作的好助手,我们可以通过Introspector 得到某各类的所有属性的PropertyDescriptor

如果propertyDescriptor的get方法有我们定义的Attribute标签 就先把他存起来,等把所有标签都生成了,再回来处理他们。

因为属性解释器的遍历顺序是不规则的,有可能属性在标签还没生成的时候,就已经出现,我们要避免这种情况,所以先处理标签的生成,这样处理属性时,就肯定能找到对应的标签(如果标签确实存在的话)

public static Element getElementFrom(Object obj) throws ... {
    Class<?> objClass = obj.getClass();
    String elementName = objClass.getSimpleName();
    //新建一个标签,用类名作为标签名就行
    Element element = DocumentHelper.createElement(elementNam
    //用于储存需要以属性来表示,就把该属性描述器存起来
    List<PropertyDescriptor> pdToTagsList = new ArrayList<>();
    //得到所有属性描述器
    PropertyDescriptor[] pds = Introspector.getBeanInfo(objClass, Object.class)
            .getPropertyDescriptors();
    // 遍历每一个属性描述器
    for (PropertyDescriptor pd : pds) {
        Method readMethod = pd.getReadMethod();
        if (readMethod.isAnnotationPresent(Attribute.class)) {
            //如果被贴上attribute标签,则存起来
            pdToTagsList.add(pd);
            continue;
        }
        //否则就把这个属性加入到标签中,作为一个子元素。
        addElementOn(element, pd, obj);
    }
    //最后把存起来的属性描述器也放进标签中。
    tagOn(element, pdToTagsList, obj);
    return element;
}

最核心的一步已经完成了,接下来就看看怎样把属性加入到标签中吧!

第三步:生成DOM树

要把属性添加到父标签中,肯定要把属性变成一个子element元素的。属性名称作为标签,属性值作为标签的内容。把属性值全部变成String作为子element元素的Text就行。

然而我们希望可以嵌套对象,也就是对象里面包含对象,如果属性的返回类型是个对象,上面的办法就不可行了,我们应该用递归方法,当属性的返回类型是个对象,就把子element 变成这个对象返回的子类型。

private static void addElementOn(Element element, PropertyDescriptor pd, Object obj) throws InvocationTargetException, IllegalAccessException, IntrospectionException {
    Element newElement = DocumentHelper.createElement(pd.getName());
    // 通过read方法可以得到属性的值
    Object value = pd.getReadMethod().invoke(obj);
    if (value instanceof Boolean || value instanceof CharSequence || value instanceof Number){
        // 如果属性的值属于数字,布尔值或字符串,就可以直接setText
        newElement.setText(value.toString());
    }
    else{
        //否则就递归调用方法,把子节点变成value的dom树。
        newElement = getElementFrom(value);
    }
    //最后把子节点加到父element中
    element.add(newElement);
}

当把所有标签都设置好以后,就可以往标签上贴属性了,得益于注解@Attribute 的作用,我们可以很容易知道属性要贴在哪个标签中:

private static void tagOn(Element element, List<PropertyDescriptor> withPDS, Object obj) throws InvocationTargetException, IllegalAccessException {
    for (PropertyDescriptor pd : withPDS) {
        Method readMethod = pd.getReadMethod();
        Attribute attribute_annotation = readMethod.getAnnotation(Attribute.class);
        //通过注解内容,得到要贴在哪个标签上
        String tag = attribute_annotation.pastedOn();
        if (tag.equals("this")) {
            //如果是this,说明所贴位置是父级标签,可以直接贴在element上
            tagAttribute(element, pd, obj);
            continue;
        }
        //如果所贴的位置不是父级标签,则先通过element找到子标签。
        Element targetElement = element.element(tag);
        //只要找到就把属性加到子标签中
        if (targetElement != null) tagAttribute(targetElement, pd, obj);
    }
}


private static void tagAttribute(@NotNull Element element, PropertyDescriptor pd, Object obj) throws InvocationTargetException, IllegalAccessException {
        //调用read方法,获得属性值
        Object value = pd.getReadMethod().invoke(obj);
        //把属性值放进指定的标签中
        element.addAttribute(pd.getName(), value.toString());
    }

至此,一棵DOM树就已经在内存中生成了!

第四步:输出DOM树

最后一步反而没有什么要注意的,只要记住XMLWriter对象,需要两个对象来生成:

  1. OutputFormat对象,用于把DOM树格式化成可读内容
  2. Writer对象,用于指定输出路径。
public static void write(OutputStream out, Object object) throws ... {
    // 先创建一个文档,没有文档也能XMLWriter也能输出,但是就没有文档声明
    Document document = DocumentHelper.createDocument();
    //根据传入的参数生成跟节点。
    Element root;
    if (object instanceof Collection) {
        Collection collection = (Collection) object;
        root = getListFrom(collection);
    } else {
        root = getElementFrom(object);
    }
    document.add(root);
    //outputFormat会帮助我们把节点变成可读的XML文档
    OutputFormat outputFormat = OutputFormat.createPrettyPrint();
    outputFormat.setEncoding("utf-8");
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
    // XMLWriter需要接受两个参数
    XMLWriter xmlWriter = new XMLWriter(writer, outputFormat);
    xmlWriter.write(document);
    xmlWriter.close();
}

总结

本案例到这里就完成了!我们可以新建任意的javaBean来测试这个小工具。都能生成一棵

当然,这个工具还很局限,比如不能自动转换实体字符,不会自动添加字符数据区,也没有检测同一标签下是否有相同的属性名。这些都是我们可以继续慢慢去完善的。

通过本案例,我们可以更理解java反射和注解可以怎样配合,他们加到一起功能可以非常强大的!希望你能喜欢。

本案例已上传到GitHub,欢迎下载交流。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值