基于ReflectASM+注解开发对象转换工具
开发原因
在项目对接数据中,会遇到了对外标准和内部标准对象转换问题,需要将上报的数据对象转换为我们项目中标准数据对象,当两边数据标准一致时,比较常见的方式,就是new 一个标准的对象,set 和get 对接数据;或者orika 复制对象。第一种方法,就会有长篇幅的set 和get 方法代码出现,代码不够简洁;也容易遗漏字段的赋值,orika 代码简洁,一行代码就可以实现对象的转换。而当标准不一致时题,比如在对接文档中,性别字段为 personGender ,而在我们标准中字段却为gender ,对于这种问题,还是只能回到第一种方法,在代码中set 和get ,除了这两种情况外,还有上报数据类型和我们标准的类型不一致、上报数据格式为字符串,标准的类型是Integer ,针对这个问题,采用了一个基于ReflectASM +注解开发了一个代码转换工具,解决对象转换问题,实现注解配置,一行代码转换的功能。
ReflectASM 是一个非常小的 Java 类库,通过ASM框架操作字节码生成来提供高性能的反射处理,自动为 get/set 字段提供访问类,访问类使用字节码操作而不是 Java 的反射技术,因此非常快。
这里为什么使用ReflectASM ,并不是使用 Java 的反射技术,反射的性能一直是存在诟病,相比于直接调用速度上差了很多。ReflectASM 的调用速度在两者时间,比直接调用慢,但是比使用反射快很多,可以参考,ReflectASM 官方提供的基于 Java 7u3 测试结果
这里,我们可以测试一下java 的直接赋值、反射和ReflectASM 的赋值
Class<Person> clazz = Person.class;
Method method = clazz.getMethod("setPersonName", String.class);
method.setAccessible(true);
MethodAccess methodAccess = MethodAccess.get(Person.class);
Person person = new Person();
int times = 1000000;
long startTime0 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
person.setPersonName("qingshui");
}
System.out.println("直接赋值 : " + (System.currentTimeMillis() - startTime0));
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
method.invoke(person, "qingshui");
}
System.out.println("java 反射 : " + (System.currentTimeMillis() - startTime1));
long startTime2 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
methodAccess.invoke(person, "setPersonName", "qingshui");
}
System.out.println("reflectasm 赋值方法 : " + (System.currentTimeMillis() - startTime2));
测试结果
直接赋值 : 16
java 反射 : 24
reflectasm 赋值方法 : 18
可以看到当遍历次数为100w次时,ReflectASM 和直接赋值差距并不是很大
流程图
实现
同属性名字段的转换
我们使用A 代表源对象、B 代表目标对象,在ReflectASM 中,MethodAccess 为类的方法访问对象,可以通过MethodAccess 获取类的各类方法的索引(下标),ReflectASM 主要使用调用方法和获取索引,再赋值两种方式操作,其中使用索引的速度最快
MethodAccess access = MethodAccess.get(SomeClass.class);
access.invoke(someObject, "setName", "abc");
Integer setIndex = = access.getIndex("setName");
access.invoke(someObject, setIndex, "abc");
测试ReflectASM 两种赋值方式
long startTime2 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
methodAccess.invoke(person, "setPersonName", "qingshui");
}
System.out.println("reflectasm 赋值方法 : " + (System.currentTimeMillis() - startTime2));
int index2 = methodAccess.getIndex("setPersonName");
long startTime3 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
methodAccess.invoke(person, index2, "qingshui");
}
System.out.println("reflectasm 索引 : " + (System.currentTimeMillis() - startTime3));
测试结果:
reflectasm 赋值方法 : 24
reflectasm 索引 : 16
回到主题,当A 和B 的字段的属性名、类型都一致时,如何进行复制操作
private static final String GET_METHOD = "get";
private static final String SET_METHOD = "set";
MethodAccess sAccess = MethodAccess.get(A.getClass());
List<Field> fields = getFields(A);
MethodAccess tAccess = MethodAccess.get(B.getClass());
for (Field field : fields) {
String fieldName=field.getName();
Integer getIndex = sAccess.getIndex(GET_METHOD + StringUtils.capitalize(fieldName));
Object value = sAccess.invoke(A, getIndex);
if (isNullValue(value)) {
continue;
}
Integer setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName));
tAccess.invoke(orig, setIndex, value);
}
其中getFields() 和isNullValue() 可以从附件中查找代码块,当两个属性一致时,步骤如下
- 获取
A 的全部字段(包括父类字段) - 遍历
A 字段,获取A 的get 方法的索引 - 结合步骤2获取到的索引,获取到
A 对应字段的值,如何值为空,就没必要进行赋值操作 - 获取
B 中set 方法的索引 - 集合步骤4的索引,赋值
B 对象对应字段的值
不同属性名的字段转换
当A 源对象和B 目标对象的字段不一样时,比如想把A 的personName 字段的值复制到B 的name 字段,这里便需要使用之前提到的注解辅助帮忙
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Trans {
String value();
}
在需要转换的字段上,添加注解
@Trans(value = "name")
private String personName;
代码实现
private static final String GET_METHOD = "get";
private static final String SET_METHOD = "set";
MethodAccess sAccess = MethodAccess.get(A.getClass());
List<Field> fields = getFields(A);
MethodAccess tAccess = MethodAccess.get(B.getClass());
for (Field field : fields) {
String fieldName=field.getName();
Integer getIndex = sAccess.getIndex(GET_METHOD + StringUtils.capitalize(fieldName));
Object value = sAccess.invoke(A, getIndex);
if (isNullValue(value)) {
continue;
}
String transFieldName;
Trans transValue;
if ((transValue = field.getAnnotation(Trans.class)) != null) {
transFieldName = transValue.value();
}else{
transFieldName=fieldName;
}
Integer setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(transFieldName));
tAccess.invoke(B, setIndex, value);
}
在同属性名字段的基础上,添加了一个判断流程
- 字段是否存在
Trans 注解,如果存在注解,则将获取注解配置的value 属性值
不同类型的字段
在上个方法中,通过注解的方式解决了字段的类型一致,仅方法属性名不一致的复制,如果当A 源对象和B 目标对象的字段类型也不一样,该如何处理?这里便需要扩展注解的内容,增加一个目标类型的配置
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Trans {
String value();
TransType type() default TransType.STRING;
}
public enum TransType {
STRING,
INTEGER
}
这里以Stirng 字符格式的值复制给Integer 的字段为例子
在需要转换的字段上,添加注解
@Trans(value = "gender",type = TransType.INTEGER)
private String personGender;
代码实现:
private static final String GET_METHOD = "get";
private static final String SET_METHOD = "set";
MethodAccess sAccess = MethodAccess.get(A.getClass());
List<Field> fields = getFields(A);
MethodAccess tAccess = MethodAccess.get(B.getClass());
for (Field field : fields) {
String fieldName=field.getName();
Integer getIndex = sAccess.getIndex(GET_METHOD + StringUtils.capitalize(fieldName));
Object value = sAccess.invoke(A, getIndex);
if (isNullValue(value)) {
continue;
}
String transFieldName;
Trans transValue;
TransType type;
if ((transValue = field.getAnnotation(Trans.class)) != null) {
transFieldName = transValue.value();
type = transValue.type();
}else{
transFieldName=fieldName;
}
Object sourceValue;
Integer setIndex;
if(type!=null){
switch (type) {
case STRING:
sourceValue = String.valueOf(value);
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(transFieldName), String.class);
break;
case INTEGER:
sourceValue = Integer.parseInt(String.valueOf(value));
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(transFieldName), Integer.class);
break;
}else{
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(transFieldName));
}
tAccess.invoke(B, setIndex, sourceValue);
}
在上个方法的基础上,增加一个TransType 类型的处理,根据TransType 获取到目标对象对应的字段、类型的索引,然后再赋值给该字段值,由于TransType 的默认值为TransType.STRING; ,如果需要将源对象的Integer 复制到目标对象Integer 时,使用到注解时,需要这样配置
@Trans(value = "gender",type = TransType.INTEGER)
private Integer personGender;
扩展
以上,只是一个简单的转换对象工具的实现,在实际项目中,还需要对这个工具类进行扩展
时间格式处理
这个扩展主要是为了支持Stirng 字符格式的值。转换为Date 格式,这里涉及到了时间字符的格式化处理,还是要扩展注解的内容
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Trans {
String value();
TransType type() default TransType.STRING;
String dateFormat() default "yyyy-MM-dd HH:mm:ss";
}
public enum TransType {
STRING,
INTEGER,
DATE
}
字段的注解配置
@Trans(value = "startTime", type = TransType.DATE, dateFormat = "yyyy-MM-dd")
private String startTime;
代码实现
private static final String GET_METHOD = "get";
private static final String SET_METHOD = "set";
MethodAccess sAccess = MethodAccess.get(A.getClass());
List<Field> fields = getFields(A);
MethodAccess tAccess = MethodAccess.get(B.getClass());
for (Field field : fields) {
String fieldName=field.getName();
Integer getIndex = sAccess.getIndex(GET_METHOD + StringUtils.capitalize(fieldName));
Object value = sAccess.invoke(A, getIndex);
if (isNullValue(value)) {
continue;
}
String transFieldName;
Trans transValue;
TransType type;
if ((transValue = field.getAnnotation(Trans.class)) != null) {
transFieldName = transValue.value();
type = transValue.type();
}else{
transFieldName=fieldName;
}
Object sourceValue;
Integer setIndex;
if(type!=null){
switch (type) {
case DATE:
String formats = transValue.dateFormat();
sourceValue = DateUtil.formatStrToDate(String.valueOf(value), format);
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(transFieldName), Date.class);
break;
case STRING:
sourceValue = String.valueOf(value);
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(transFieldName), String.class);
break;
case INTEGER:
sourceValue = Integer.parseInt(String.valueOf(value));
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(transFieldName), Integer.class);
break;
}else{
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(transFieldName));
}
tAccess.invoke(B, setIndex, sourceValue);
}
通过注解的dateFormat 属性,扩展工具对字符格式化为时间的处理
处理非基类的List转换List
在实际中,我们会遇到一个List<C.class> 转换为另一个List<D.class> 的情况,这里还是要继续扩展注解,比如,需要把List<Person> persons 转换 List<User> users
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Trans {
String value();
TransType type() default TransType.STRING;
String dateFormat() default "yyyy-MM-dd HH:mm:ss";
Class<?> toFieldClass() default String.class;
}
public enum TransType {
STRING,
INTEGER,
DATE,
LIST
}
字段的注解配置
@Trans(value = "users", toFieldClass = User.class)
private List<Person> persons;
@Data
public class Person {
@Trans(value = "name")
private String personName;
@Trans(value = "gender",type = TransType.INTEGER)
private String personGender;
@Trans(value = "startTime", type = TransType.DATE, dateFormat = "yyyy-MM-dd")
private String startTime;
}
在代码实现中,需要增加如下代码处理
public static Object transToExpress(Object dest, Object orig) {
````取值、判断空值
Class<?> toFieldClass = transValue.toFieldClass();
if (value instanceof List && !baseClass(toFieldClass)) {
listNotBaseClassTypeToList(orig, (List<?>) value, transValue);
return;
}
`````处理非集合的代码
}
private static void listNotBaseClassTypeToList(Object orig, List<?> value, Trans transValue) {
String fieldName = transValue.value();
Class<?> toFieldClass = transValue.toFieldClass();
List<?> values = transListToListExpress(toFieldClass, value);
invokeTargetValue(orig, fieldName, values, TransType.LIST);
}
private static List<Object> transListToListExpress(Class<?> orig, List<?> value) {
List<Object> values = new ArrayList<>();
ConstructorAccess<?> access = ConstructorAccess.get(orig);
for (Object dest : value) {
Object newOrig = access.newInstance();
values.add(transToExpress(dest, newOrig));
}
return values;
}
private static boolean baseClass(Class<?> className) {
return className.equals(String.class) || ClassUtils.isPrimitiveOrWrapper(className);
}
private static void invokeTargetValue(Object orig, String fieldName, Object value, TransType type) {
try {
MethodAccess tAccess = getMethodAccess(orig);
Object sourceValue;
Integer setIndex;
switch (type) {
case DATE:
sourceValue = value;
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName), Date.class);
break;
case STRING:
sourceValue = String.valueOf(value);
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName), String.class);
break;
case INTEGER:
sourceValue = Integer.parseInt(String.valueOf(value));
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName), Integer.class);
break;
case LIST:
sourceValue = value;
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName), List.class);
break;
default:
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName));
sourceValue = value;
}
tAccess.invoke(orig, setIndex, sourceValue);
} catch (IllegalArgumentException e) {
log.error("TransToExpress.invokeTargetValue fieldName: {} \n Exception :{}", fieldName, e.getMessage());
}
}
处理非基类的集合类型转换,步骤如下
- 判断当前的需要转换的值是否为
List ,并且转换的目标类toFieldClass 是否为基类,如何是基类的转换,直接调用invokeTargetValue 进行转换,不需要特殊的处理 - 使用
ConstructorAccess 构建目标类对象 - 遍历value,调用
transToExpress 方法转换源对象A 转换为目标对象B ,递归调用,transToExpress 方法(工具类的入口)
优化
使用缓存
由于在代码中会频繁使用MethodAccess 、Field 、ConstructorAccess 等对象,所以在实际项目中,会使用map 缓存,增加复用,提高效率
private static final Map<Class<?>, MethodAccess> ACCESS_MAP = new HashMap<>(64);
private static final Map<Class<?>, List<Field>> FIELDS_MAP = new HashMap<>(64);
private static final Map<String, Integer> INDEX_MAP = new HashMap<>(64);
private static final Map<Class<?>, ConstructorAccess<?>> CONSTRUCTOR_ACCESS_MAP = new HashMap<>(64);
public static Object transToExpress(Object dest, Object orig) {
MethodAccess sAccess = getMethodAccess(dest);
List<Field> fields = getFields(dest);
for (Field field : fields) {
String getKey = dest.getClass().getName() + GET_METHOD + field.getName();
Integer getIndex = INDEX_MAP.get(getKey);
if (getIndex == null) {
getIndex = sAccess.getIndex(GET_METHOD + StringUtils.capitalize(field.getName()));
INDEX_MAP.put(getKey, getIndex);
}
}
}
private static void invokeTargetValue(Object orig, String fieldName, Object value, TransType type) {
try {
MethodAccess tAccess = getMethodAccess(orig);
Object sourceValue;
String setKey = orig.getClass().getName() + SET_METHOD + fieldName;
Integer setIndex = INDEX_MAP.get(setKey);
switch (type) {
case DATE:
sourceValue = value;
if (setIndex == null) {
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName), Date.class);
INDEX_MAP.put(setKey, setIndex);
}
break;
case STRING:
sourceValue = String.valueOf(value);
if (setIndex == null) {
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName), String.class);
INDEX_MAP.put(setKey, setIndex);
}
break;
case INTEGER:
sourceValue = Integer.parseInt(String.valueOf(value));
if (setIndex == null) {
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName), Integer.class);
INDEX_MAP.put(setKey, setIndex);
}
break;
case LIST:
sourceValue = value;
if (setIndex == null) {
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName), List.class);
INDEX_MAP.put(setKey, setIndex);
}
break;
default:
if (setIndex == null) {
setIndex = tAccess.getIndex(SET_METHOD + StringUtils.capitalize(fieldName));
INDEX_MAP.put(setKey, setIndex);
}
sourceValue = value;
}
tAccess.invoke(orig, setIndex, sourceValue);
} catch (IllegalArgumentException e) {
log.error("TransToExpress.invokeTargetValue fieldName: {} \n Exception :{}", fieldName, e.getMessage());
}
}
private static MethodAccess getMethodAccess(Object object) {
MethodAccess sAccess = ACCESS_MAP.get(object.getClass());
if (sAccess == null) {
sAccess = MethodAccess.get(object.getClass());
ACCESS_MAP.put(object.getClass(), sAccess);
}
return sAccess;
}
private static List<Field> getFields(Object dest) {
List<Field> fields = FIELDS_MAP.get(dest.getClass());
if (fields == null) {
fields = TransUtil.getFields(dest.getClass());
FIELDS_MAP.put(dest.getClass(), fields);
}
return fields;
}
总结
文章大部分代码以参考为主(项目问题,完整代码无法贴出),在实际项目,我们扩展这个工具类其他功能,比如增加主键自定义格式化、图片上传的自定义处理、赋值额外字段等功能,在开发这个工具类的过程中,逐步完善工具,丰富功能,体验到了开发轮子魅力所在,不过,最大成就感还是来自这行代码
TransToExpress.transToExpress(person, user)
附件
private static List<Field> getFields(Class<?> objClass) {
List<Field> fieldList = new ArrayList<>();
while (null != objClass) {
fieldList.addAll(Arrays.asList(objClass.getDeclaredFields()));
objClass = objClass.getSuperclass();
}
return fieldList;
}
private static boolean isNullValue(Object value) {
if (value instanceof String) {
return StringUtils.isBlank((String) value);
}
return null == value;
}
|