今天学习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
中了,另外id
和nickName
也作为属性。
接下来测试把列表变成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步:
-
分析对象,得到各项属性和他们的关系:谁是标签,谁是标签的属性。
-
根据分析结果,在内存中生成一棵DOM树。
-
把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对象,需要两个对象来生成:
OutputFormat
对象,用于把DOM树格式化成可读内容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,欢迎下载交流。