在项目对接数据中,会遇到了对外标准和内部标准对象转换问题,需要将上报的数据对象转换为我们项目中标准数据对象,当两边数据标准一致时,比较常见的方式,就是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 的赋值
Classclazz = 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的全部字段 Listfields = 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的全部字段 Listfields = 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的全部字段 Listfields = 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的全部字段 Listfields = 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
@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 Listpersons; @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
处理非基类的集合类型转换,步骤如下
- 判断当前的需要转换的值是否为List,并且转换的目标类toFieldClass是否为基类,如何是基类的转换,直接调用invokeTargetValue 进行转换,不需要特殊的处理
- 使用ConstructorAccess构建目标类对象
- 遍历value,调用transToExpress方法转换源对象A转换为目标对象B,递归调用,transToExpress方法(工具类的入口)
由于在代码中会频繁使用MethodAccess、Field、ConstructorAccess等对象,所以在实际项目中,会使用map缓存,增加复用,提高效率
private static final Map, MethodAccess> ACCESS_MAP = new HashMap<>(64);
private static final Map, List> FIELDS_MAP = new HashMap<>(64);
private static final Map INDEX_MAP = new HashMap<>(64);
private static final Map, ConstructorAccess>> CONSTRUCTOR_ACCESS_MAP = new HashMap<>(64);
//使用缓存的例子 case1
public static Object transToExpress(Object dest, Object orig) {
//缓存
MethodAccess sAccess = getMethodAccess(dest);
//拿到类的源对象的全部属性
List 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 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 getFields(Object dest) {
List 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 getFields(Class> objClass) {
List 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;
}



