基于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;
// case0: 直接赋值
long startTime0 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
person.setPersonName("qingshui");
}
System.out.println("直接赋值 : " + (System.currentTimeMillis() - startTime0));
// case1: java反射
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
method.invoke(person, "qingshui");
}
System.out.println("java 反射 : " + (System.currentTimeMillis() - startTime1));
// case2: reflectasm
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
两种赋值方式
// case2: reflectasm
long startTime2 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
methodAccess.invoke(person, "setPersonName", "qingshui");
}
System.out.println("reflectasm 赋值方法 : " + (System.currentTimeMillis() - startTime2));
// case3: reflectasm 索引
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";
//Object A 方法访问对象
MethodAccess sAccess = MethodAccess.get(A.getClass());
//获取到A的全部字段
List<Field> fields = getFields(A);
//Object B 方法访问对象
MethodAccess tAccess = MethodAccess.get(B.getClass());
for (Field field : fields) {
String fieldName=field.getName();
//并且属性名称为 name 为getName
//StringUtils.capitalize 字符首字母大写
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";
//Object A 方法访问对象
MethodAccess sAccess = MethodAccess.get(A.getClass());
//获取到A的全部字段
List<Field> fields = getFields(A);
//Object B 方法访问对象
MethodAccess tAccess = MethodAccess.get(B.getClass());
for (Field field : fields) {
String fieldName=field.getName();
//并且属性名称为 name 为getName
//StringUtils.capitalize 字符首字母大写
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";
//Object A 方法访问对象
MethodAccess sAccess = MethodAccess.get(A.getClass());
//获取到A的全部字段
List<Field> fields = getFields(A);
//Object B 方法访问对象
MethodAccess tAccess = MethodAccess.get(B.getClass());
for (Field field : fields) {
String fieldName=field.getName();
//并且属性名称为 name 为getName
//StringUtils.capitalize 字符首字母大写
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;
//获取到set方法的下标
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";
//Object A 方法访问对象
MethodAccess sAccess = MethodAccess.get(A.getClass());
//获取到A的全部字段
List<Field> fields = getFields(A);
//Object B 方法访问对象
MethodAccess tAccess = MethodAccess.get(B.getClass());
for (Field field : fields) {
String fieldName=field.getName();
//并且属性名称为 name 为getName
//StringUtils.capitalize 字符首字母大写
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;
//获取到set方法的下标
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;
}
在代码实现中,需要增加如下代码处理
/**
* TransToExpress::TransToExpress
* <p>转换数据内容到目标数据
* <p>HISTORY: 2021/9/13 qingshui : Created.
*
* @param dest 源数据
* @param orig 目标数据
* @return Object 目标对象
*/
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 method =============*/
/**
* TransToExpress::listNotBaseClassTypeToList
* <p>TO:非基础类型的集合转换成目标的集合
* <p>HISTORY: 2021/11/12 qingshui : Created.
*
* @param orig 目标值
* @param value 转换的集合
* @param transValue 注解
*/
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);
}
/**
* TransToExpress:: transListToListExpress
* <p>list集合转list
* <p>通过ConstructorAccess构造函数,构造对象,便利调用transToExpress方法
* <p>HISTORY: 2021/9/16 qingshui : Created.
*
* @param orig 目标的对象的class对象
* @param value 转换之前的值
* @return List<Object> 返回转换后的对象集合
*/
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;
}
/**
* TransToExpress:: baseClass
* <p>判断传入的是不是基础类型和系统类
* <p>HISTORY: 2021/9/17 qingshui : Created.
*
* @param className 类名
* @return boolean true 是的 false 不是
*/
private static boolean baseClass(Class<?> className) {
// org.springframework.util.ClassUtils 方法
return className.equals(String.class) || ClassUtils.isPrimitiveOrWrapper(className);
}
/**
* TransToExpress:: invokeTargetValue
* <p>使用asm对目标的对象进行赋值操作、
* <p>HISTORY: 2021/9/15 qingshui : Created.
*
* @param orig 目标的对象值
* @param fieldName 赋值的字段名称
* @param value 目标值
* @param type 转换的类型,这里其实可以不传入该值进行推断,也能获取到,考虑传入类型,可以准确定位到方法的下标
*/
private static void invokeTargetValue(Object orig, String fieldName, Object value, TransType type) {
try {
MethodAccess tAccess = getMethodAccess(orig);
//获取到set方法的下标
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:
//增加一个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);
//使用缓存的例子 case1
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);
}
}
}
//使用缓存的例子 case2
private static void invokeTargetValue(Object orig, String fieldName, Object value, TransType type) {
try {
MethodAccess tAccess = getMethodAccess(orig);
//获取到set方法的下标
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 method =============*/
/**
* TransToExpress:: getMethodAccess
* <p>TO:获取到对象的方法的句柄
* <p>HISTORY: 2021/11/12 qingshui : Created.
*
* @param object 对象
* @return MethodAccess 返回方法的句柄
*/
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;
}
/**
* TransToExpress:: getDestFields
* <p>TO:获取到源对象的字段值
* <p>HISTORY: 2021/11/12 qingshui : Created.
*
* @param dest 源对象
* @return List<Field> 字段属性.
*/
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 method =============*/
/**
* <p>TO:获取一个类的全部字段信息
* <p>HISTORY: 2022/2/28 qingshui : Created.
* @param objClass 目标类
* @return List<Field> 字段的信息
*/
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;
}
/**
* <p>TO:判断当前字符是否是空值,这里直接转拆箱转换为String,或者判断其他对象是不是null值
* <p>HISTORY: 2021/11/12 qingshui : Created.
*
* @param value 需要判断的值
* @return Boolean true
*/
private static boolean isNullValue(Object value) {
if (value instanceof String) {
return StringUtils.isBlank((String) value);
}
return null == value;
}