概述:本文介绍了利用ORM(Object/Relational Mapper,对象关系映射)技术和Java中的注解、反射等特性实现配置文件的持久化,实现了实体类和配置文件的映射、配置文件的自动加载和自动保存。
项目地址:http://github.com/mayuanxiaonong/jtkPersistence
这个想法来源于之前项目的一个功能实现,当时要用swing做一个批量生成打印二维码的工具,需要对页边距、二维码数量、宽度、高度、间距等参数进行设置,以适应不同的打印纸,毫无疑问这些配置需要保存在配置文件中,利用Java的Properties类进行读取、保存。当时对这些参数进行了简单的封装:
/**
* 二维码打印纸页面参数
* @author Jason
*/
public class PrintParam {
// 页面参数:左页边距、上页边距
private double leftMargin;
private double topMargin;
// 二维码设置:行数、列数、宽、高、间距
private int qrRows;
private int qrCols;
private double qrWidth;
private double qrHeight;
private double qrPadding;
private PropertiesUtil p = new PropertiesUtil();
/**
* 加载配置
*/
public void load() {
leftMargin = p.getDoubleValue("Left_Margin");
topMargin = p.getDoubleValue("Top_Margin");
qrRows = p.getIntegerValue("QR_Rows");
qrCols = p.getIntegerValue("QR_Cols");
qrWidth = p.getDoubleValue("QR_Width");
qrHeight = p.getDoubleValue("QR_Height");
qrPadding = p.getDoubleValue("QR_Padding");
System.out.println("PrintParam loaded ...");
System.out.println(toString());
}
/**
* 保存配置
*/
public void save() {
System.out.println("PrintParam saving ...");
System.out.println(toString());
p.setValue("Left_Margin", leftMargin);
p.setValue("Top_Margin", topMargin);
p.setValue("QR_Rows", qrRows);
p.setValue("QR_Cols", qrCols);
p.setValue("QR_Width", qrWidth);
p.setValue("QR_Height", qrHeight);
p.setValue("QR_Padding", qrPadding);
p.store();
}
// getters & setters
...
}
public class PrintParam {
// 页面参数:左页边距、上页边距
private double leftMargin;
private double topMargin;
// 二维码设置:行数、列数、宽、高、间距
private int qrRows;
private int qrCols;
private double qrWidth;
private double qrHeight;
private double qrPadding;
private PropertiesUtil p = new PropertiesUtil();
/**
* 加载配置
*/
public void load() {
leftMargin = p.getDoubleValue("Left_Margin");
topMargin = p.getDoubleValue("Top_Margin");
qrRows = p.getIntegerValue("QR_Rows");
qrCols = p.getIntegerValue("QR_Cols");
qrWidth = p.getDoubleValue("QR_Width");
qrHeight = p.getDoubleValue("QR_Height");
qrPadding = p.getDoubleValue("QR_Padding");
System.out.println("PrintParam loaded ...");
System.out.println(toString());
}
/**
* 保存配置
*/
public void save() {
System.out.println("PrintParam saving ...");
System.out.println(toString());
p.setValue("Left_Margin", leftMargin);
p.setValue("Top_Margin", topMargin);
p.setValue("QR_Rows", qrRows);
p.setValue("QR_Cols", qrCols);
p.setValue("QR_Width", qrWidth);
p.setValue("QR_Height", qrHeight);
p.setValue("QR_Padding", qrPadding);
p.store();
}
// getters & setters
...
}
虽然将参数封装到一个类中了,但是在读取、保存这些参数的时候还是需要将参数名字的字符串写在这里,而且读取和保存需要写到两个地方,封装的并不太好,而且如果在应用中使用了大量配置文件的话,读取和保存将是个很大的工作量。
所以当时就想到了开发这样一个工具类,可以将配置文件自动加载到实体类中,并能将实体对象自动保存到配置文件中,就像JPA持久化一样。
JPA的持久化实现了实体类和关系型数据库的映射,将实体类中的属性映射到数据表的字段。而配置文件是用哈希表存放的一堆键值对(Keys & Values),我们可以把配置文件看作是只有一条数据的数据表,其中的每一个键值对(K-V)的K就是数据表的字段名,而V就是数据表的字段值。
1、有了JPA做参照,实现起来就比较简单了,首先要设计一些自定义注解,用来标识实体类和字段:
1. @JtkEntity 标识某个实体类映射到一个配置文件,需要指定配置文件的名字。2. @JtkKey 标识某个成员变量映射到配置文件的Key,需要指定Key的名字;或不使用JtkKey标注,默认使用变量名做Key的名字。另外,还可以指定字段的默认值。3. @JtkNone 标识某个成员变量不映射到配置文件,效果通JPA的@Transient。
2、接下来就是要解析实体类,并自动加载配置文件了,这里使用Java的反射机制读取实体类的注解信息:
1. 读取JtkEntity注解
/**
* Get the JtkEntity annotation of the entity class
*
* @param cls
* Class of the entity class
* @return
* @throws JtkPersistenceException
*/
private static JtkEntity getJtkEntity(Class<?> cls) throws JtkPersistenceException {
JtkEntity entity = cls.getAnnotation(JtkEntity.class);
if (entity == null) {
throw new JtkPersistenceException("Class " + cls.getName() + " is not a legal properties entity class!");
}
return entity;
}
2. 遍历实体类中的成员变量,通过@JtkKey注解或变量名得到配置文件中的字段名,使用配置工具类得到值:
Field[] fields = cls.getDeclaredFields();
for (Field f : fields) {
// Jump static fields
if ((f.getModifiers() & Modifier.STATIC) != 0) {
continue;
}
// Jump @JtkNone fields
if (f.getAnnotation(JtkNone.class) != null) {
continue;
}
if (!isSupported(f.getType())) {
throw new JtkPersistenceException("Unsupported type " + f.getType() + " of field " + f.getName());
}
// Get @JtkKey of field
JtkKey key = f.getAnnotation(JtkKey.class);
String keyName;
String defaultValue;
if (key != null) {
keyName = key.name();
defaultValue = key.defaultValue();
} else {
keyName = f.getName();
defaultValue = "";
}
String keyValue = propertiesUtil.getValue(keyName, defaultValue);
根据变量的类型转换变量值:
// Cast value to type of field
Object value = null;
try {
value = cast(f.getType(), keyValue);
} catch (NumberFormatException e) {
throw new JtkPersistenceException("Cast " + keyValue + " to " + f.getType() + " of field " + f.getName() + " failed!", e);
}
3. 利用反射设置变量的值:
try {
f.setAccessible(true);
f.set(object, value);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new JtkPersistenceException("Set value " + value + " to field " + f.getName() + " failed!", e);
}
3、同样的,使用反射机制可以获取一个对象中成员变量表示的字段名和字段值,并保存到配置文件中。
测试:
1. 配置文件 test.properties:
a=123
b=abc
2. 实体类 TestBean.java:
import com.jtk.persistence.JtkEntity;
import com.jtk.persistence.JtkKey;
import com.jtk.persistence.JtkNone;
@JtkEntity(name = "bin/test.properties")
public class TestBean {
@JtkKey(name = "a")
private int aaa;
private String b;
@JtkNone
private String c;
public int getAaa() {
return aaa;
}
public void setAaa(int aaa) {
this.aaa = aaa;
}
public String getB() {
return b;
}
public void setB(String b) {
this.b = b;
}
@Override
public String toString() {
return "TestBean [aaa=" + aaa + ", b=" + b + ", c=" + c + "]";
}
}
3. 测试类 Test.java:
import com.jtk.persistence.JtkPersistenceException;
import com.jtk.persistence.JtkPersistence;
public class Test {
public static void main(String[] args) throws JtkPersistenceException {
TestBean bean = (TestBean) JtkPersistence.getInstance(TestBean.class);
System.out.println(bean.toString());
bean.setAaa(321);
bean.setB(null);
JtkPersistence.save(bean);
}
}
运行结果:
field : aaa
keyName = a
keyValue = 321
defaultValue =
field : b
keyName = b
keyValue =
defaultValue =
TestBean [aaa=321, b=null, c=null]
field : aaa
keyName = a
keyValue = 321
field : b
keyName = b
keyValue =