背景
项目中经常有VO.PO.DTO等之间转换,由于apache工具类中BeanUtils.copyProperties及Json序列化反序列化方式转换性能比较低(阿里巴巴规范检查有提示不建议采用)。
而org.springframework.beans中的BeanUtils.copyProperties性能好,但不支持自定义类型转换,功能较弱。
于是本人综合各转换特点扩展出来一个工具类,让类型转换性能得到提升,还支持自定义各字段类型的处理方式,并且支持每个转换后回调方法.
- 支持List列表转换
- 支持回调
- 支持Supplier传值
- 该工具类比apach beanutils快,比springframework的BeanUtil.copyProperties强大
- 支持自定义类型转换 默认加入常用date与localDate及LocalDatetime转换
- 默认支持数据库Clob类型与字符串转换
- 支持Map与类对象转换
- 支持下划线转驼峰
- 支持简单嵌套转换
工具类代码
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.FatalBeanException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Supplier;
/**
* @author renxiaodong
*/
public class BeanHelper {
static {
rules = new HashMap<>();
registerConverter(new DefaultDateConverter(), Date.class);
registerConverter(new DefaultLocalDateConverter(), LocalDate.class);
registerConverter(new DefaultLocalDateTimeConverter(), LocalDateTime.class);
registerConverter(new DefaultClobConverter(), Clob.class);
}
static Map<Class, Converter> rules;
public static void registerConverter(Converter converter, Class cls) {
rules.put(cls, converter);
}
public static void deregisterConverter(Converter converter, Class cls) {
rules.remove(cls, converter);
}
public static Converter getConverter(Class cls) {
Converter converter = rules.get(cls);
if (converter == null) {
for (Class keyClz : rules.keySet()) {
if (keyClz.isAssignableFrom(cls)) {
converter = rules.get(keyClz);
rules.put(cls, converter);
break;
}
}
}
return converter;
}
public static class DefaultDateConverter implements Converter {
SimpleDateFormat dateTimeFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public <T> T convert(Class<T> targetClass, Object sourceValue) {
if (sourceValue == null) {
return null;
}
if (targetClass == Date.class) {
return (T) sourceValue;
} else if (targetClass == LocalDate.class) {
Date d = (Date) sourceValue;
return (T) LocalDate.of(d.getYear(), d.getMonth(), d.getDate());
} else if (targetClass == LocalDateTime.class) {
Date d = (Date) sourceValue;
return (T) LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault());
} else if (targetClass == String.class) {
return (T) dateTimeFormatter.format((Date) sourceValue);
}
return null;
}
}
public static class DefaultLocalDateConverter implements Converter {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public <T> T convert(Class<T> targetClass, Object sourceValue) {
if (sourceValue == null) {
return null;
}
LocalDate localDate = (LocalDate) sourceValue;
if (targetClass == Date.class) {
return (T) new Date(localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth());
} else if (targetClass == LocalDate.class) {
return (T) sourceValue;
} else if (targetClass == LocalDateTime.class) {
return (T) LocalDateTime.of(localDate.getYear(), localDate.getMonth(), localDate.getDayOfMonth(), 0, 0);
} else if (targetClass == String.class) {
return (T) localDate.format(dateTimeFormatter);
}
return null;
}
}
public static class DefaultLocalDateTimeConverter implements Converter {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public <T> T convert(Class<T> targetClass, Object sourceValue) {
if (sourceValue == null) {
return null;
}
LocalDateTime localDate = (LocalDateTime) sourceValue;
if (targetClass == Date.class) {
return (T) Date.from(localDate.atZone(ZoneId.systemDefault()).toInstant());
} else if (targetClass == LocalDate.class) {
return (T) LocalDate.of(localDate.getYear(), localDate.getMonth(), localDate.getDayOfMonth());
} else if (targetClass == LocalDateTime.class) {
return (T) sourceValue;
} else if (targetClass == String.class) {
return (T) localDate.format(dateTimeFormatter);
}
return null;
}
}
public static class DefaultClobConverter implements Converter {
@Override
public <T> T convert(Class<T> targetClass, Object sourceValue) {
if (sourceValue == null || !(sourceValue instanceof Clob)) {
return null;
}
try {
Clob clob = (Clob) sourceValue;
if (targetClass == String.class) {
Reader reader = clob.getCharacterStream();
StringBuilder buf = new StringBuilder();
try {
char[] chars = new char[2048];
for (; ; ) {
int len = reader.read(chars, 0, chars.length);
if (len < 0) {
break;
}
buf.append(chars, 0, len);
}
} catch (Exception ex) {
throw new JSONException("read string from reader error", ex);
}
String text = buf.toString();
reader.close();
return (T) text;
}
return null;
} catch (Exception e) {
throw new RuntimeException("转换 clob 失败", e);
}
}
}
/**
* BeanHelper.copyProperties(xxEntity,XXVO.class)
* 建议采用BeanHelper.copyProperties(xxEntity,XXVO::new) 这种方式性能更好
*
* @param source
* @param clazz
* @param <T>
* @param <S>
* @return
*/
public static <S, T> T copyProperties(S source, Class<T> clazz, String... ignoreProperties) {
T o = null;
if (source instanceof List) {
return (T) copyListProperties((List) source, clazz, ignoreProperties);
} else if (source instanceof Map) {
return (T) copyProperties((Map) source, clazz, true, ignoreProperties);
}
try {
o = clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("无法实例化" + clazz + "类型");
}
return copyProperties(source, o, ignoreProperties);
}
/**
* 推荐使用该方法
* BeanHelper.copyProperties(xxEntity,XXVO::new)
*
* @param source
* @param target
* @param ignoreProperties
* @param <T>
* @return
*/
public static <T, S> T copyProperties(S source, Supplier<T> target, String... ignoreProperties) {
if (source instanceof List) {
return (T) copyListProperties((List) source, target, ignoreProperties);
} else if (source instanceof Map) {
return (T) copyProperties((Map) source, target, ignoreProperties);
} else {
return copyProperties(source, target.get(), ignoreProperties);
}
}
public static <S, T> T copyProperties(S source, T target, String... ignoreProperties) {
if (source == null) {
return null;
}
Class<?> actualEditable = target.getClass();
PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
// if (Collection.class.isAssignableFrom(targetPd.getPropertyType()) && Collection.class.isAssignableFrom(sourcePd.getPropertyType())) {
// Collection children = (Collection) sourcePd.getReadMethod().invoke(source);
// for (Object val : children) {
// copyProperties( source, )
// }
// continue;
// System.out.println("不支持属性为List的类型转换");
// }
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
(ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) || rules.containsKey(readMethod.getReturnType()))) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
Converter converter = getConverter(sourcePd.getPropertyType());
if (converter != null) {
value = converter.convert(targetPd.getPropertyType(), value);
}
writeMethod.invoke(target, value);
} catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
return target;
}
/**
* 这个方法效率极低,不要使用 ,建议使用 BeanHelper.copyListProperties
*
* @param obj
* @param clazz
* @param <T>
* @param <Y>
* @return
*/
@Deprecated()
public static <T, Y> List<T> convertList(List<Y> obj, Class<T> clazz) {
if (obj == null) {
return null;
}
List<T> resultList = JSON.parseArray(JSON.toJSONString(obj), clazz);
return resultList;
}
public static <T> Page<T> toPage(IPage<?> pageList, Class<T> clazz) {
if (pageList == null) {
return null;
}
Page<T> destPage = new Page<>();
destPage.setPages(pageList.getPages());
destPage.setSize(pageList.getSize());
destPage.setCurrent(pageList.getCurrent());
destPage.setTotal(pageList.getTotal());
List<T> destList = copyListProperties(pageList.getRecords(), clazz);
destPage.setRecords(destList);
return destPage;
}
public static <S, T> List<T> copyListProperties(List<S> sources, Class<T> cls, String... ignoreProperties) {
return copyListProperties(sources, cls, null, ignoreProperties);
}
public static <S, T> List<T> copyListProperties(List<S> sources, Class<T> cls, ColaBeanUtilsCallBack<S, T> callBack, String... ignoreProperties) {
List<T> list = new ArrayList<>(sources.size());
if (!CollectionUtils.isEmpty(sources) && sources.get(0) instanceof Map) {
for (S m : sources) {
try {
T t = copyProperties((Map<String, Object>) m, cls, true, ignoreProperties);
if (callBack != null) {
// 回调
callBack.callBack(m, t);
}
list.add(t);
} catch (Exception e) {
e.printStackTrace();
}
}
return list;
}
for (S source : sources) {
T t = null;
try {
t = cls.newInstance();
copyProperties(source, t, ignoreProperties);
if (callBack != null) {
// 回调
callBack.callBack(source, t);
}
list.add(t);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
return list;
}
public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> supplier, String... ignoreProperties) {
return copyListProperties(sources, supplier, null, ignoreProperties);
}
/**
* @author Johnson
* 使用场景:Entity、Bo、Vo层数据的复制,因为BeanUtils.copyProperties只能给目标对象的属性赋值,却不能在List集合下循环赋值,因此添加该方法
* 如:List<AdminEntity> 赋值到 List<AdminVo> ,List<AdminVo>中的 AdminVo 属性都会被赋予到值
* S: 数据源类 ,T: 目标类::new(eg: AdminVo::new)
*/
public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> supplier, ColaBeanUtilsCallBack<S, T> callBack, String... ignoreProperties) {
List<T> list = new ArrayList<>(sources.size());
if (!CollectionUtils.isEmpty(sources) && sources.get(0) instanceof Map) {
for (S m : sources) {
T t = copyProperties((Map<String, Object>) m, supplier, true);
if (callBack != null) {
// 回调
callBack.callBack(m, t);
}
list.add(t);
}
return list;
} else {
for (S source : sources) {
T t = copyProperties(source, supplier, ignoreProperties);
if (callBack != null) {
// 回调
callBack.callBack(source, t);
}
list.add(t);
}
return list;
}
}
public static <M> M copyProperties(Map<String, Object> value, Supplier<M> supplier, String... ignoreField) {
return copyProperties(value, supplier, true, ignoreField);
}
/**
* @param value
* @param supplier
* @param cemalCase 是否下划线转cemalCase
* @param <M>
* @return
*/
public static <M> M copyProperties(Map<String, Object> value, Supplier<M> supplier, boolean cemalCase, String... ignoreField) {
M t = supplier.get();
return copyProperties(value, supplier.get(), cemalCase);
}
public static <M> M copyProperties(Map<String, Object> value, Class<M> clz, String... ignoreField) {
return copyProperties(value, clz, true, ignoreField);
}
public static <M> M copyProperties(Map<String, Object> value, Class<M> clz, boolean cemalCase, String... ignoreField) {
M t = null;
try {
t = clz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
throw new FatalBeanException("对象创建失败", e);
}
return copyProperties(value, t, cemalCase);
}
public static <M> M copyProperties(Map<String, Object> value, M target, boolean cemalCase, String... ignoreField) {
M t = target;
List<String> ignoreList = Arrays.asList(ignoreField);
PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(t.getClass());
Map copy = new HashMap(value.size());
if (cemalCase) {
value.forEach((key, val) -> {
if (ignoreList.contains(key)) {
} else {
key = key.replaceAll("_", "").replaceAll("-", "").toUpperCase();
copy.put(key, val);
}
});
} else {
copy.putAll(value);
}
for (PropertyDescriptor targetPd : targetPds) {
if (targetPd != null) {
try {
Object o = copy.get(targetPd.getName().toUpperCase(Locale.ROOT));
if (null == o) {
continue;
}
Converter converter = getConverter(o.getClass());
if (converter != null) {
o = converter.convert(targetPd.getPropertyType(), o);
}
Method writeMethod = targetPd.getWriteMethod();
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(t, ConverterSupport.cast(o, targetPd.getPropertyType()));
} catch (Exception e) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", e);
}
}
}
return t;
}
然后给一段性能对比测试
//单元测试
public class BeanHelperTest {
@Test
public void convertCompare() {
List<TestVo> mockList = new ArrayList<>(100000);
List<Map<String, Object>> mapList = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
Map<String, Object> other = new HashMap<>();
other.put("sex", "female");
other.put("location", "asian");
List<ChildVo> children=new ArrayList<>(3);
for (int j = 0; j < 3;j++){
ChildVo vo = new ChildVo();
vo.setAge(j);
vo.setBirthday(LocalDateTime.now());
vo.setName(UUIDGenerator.nextId());
vo.setBirthYear(2022L);
vo.setProperties(other);
children.add(vo);
}
TestVo vo = new TestVo();
vo.setAge(Math.round(0));
vo.setBirthday(LocalDateTime.now());
vo.setName(UUIDGenerator.nextId());
vo.setBirthYear(2022L);
vo.setProperties(other);
vo.setChildren(children);
mockList.add(vo);
Map<String, Object> map = new HashMap<>();
map.put("age", vo.getAge());
map.put("birthday", vo.getBirthday());
map.put("name", vo.getName());
map.put("birth_year", 2020);
map.put("properties", other);
mapList.add(map);
}
convertListAsJson(mockList);
testCopyListProperties(mockList);
testCopyListPropertiesWithSupplier(mockList);
testCopyMapListProperties(mapList);
}
public void convertListAsJson(List<TestVo> mockList) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
BeanHelper.convertList(mockList, TestPo.class);
stopWatch.stop();
System.out.printf("10万数据BeanHelper.convertList执行时长:%d 毫秒.%n", stopWatch.getTime(TimeUnit.MILLISECONDS));
}
public void testCopyListProperties(List<TestVo> mockList) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
BeanHelper.copyListProperties(mockList, TestPo.class);
stopWatch.stop();
System.out.printf("10万数据BeanHelper.copyListProperties执行时长:%d 毫秒.%n", stopWatch.getTime(TimeUnit.MILLISECONDS));
}
public void testCopyListPropertiesWithSupplier(List<TestVo> mockList) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
List<TestPo> testPos = BeanHelper.copyListProperties(mockList, TestPo::new);
} catch (Exception e) {
e.printStackTrace();
}
stopWatch.stop();
System.out.printf("10万数据BeanHelper.copyListProperties Supplier方式执行时长:%d 毫秒.%n", stopWatch.getTime(TimeUnit.MILLISECONDS));
}
public void testCopyMapListProperties(List<Map<String, Object>> mapList) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
List<TestPo> testPos = BeanHelper.copyListProperties( mapList, TestPo::new);
System.out.println(testPos.get(0));
} catch (Exception e) {
e.printStackTrace();
}
stopWatch.stop();
System.out.printf("10万数据BeanHelper.copyListProperties Map转换方式执行时长:%d 毫秒.%n", stopWatch.getTime(TimeUnit.MILLISECONDS));
}
综合比对效率
10万数据fastJson JSON.parseArray(JSON.toJSONString(obj), clazz)序列化再返序列化执行时长:3552毫秒.
10万数据org.apache.commons.beanutils.BeanUtils.copyProperties执行时长:2686毫秒.
10万数据自定义BeanHelper.copyListProperties Supplier方式执行时长:220毫秒.
10万数据自定义BeanHelper.copyListProperties Map转换方式执行时长:907毫秒.
完整代码浏览kuizii脚手架-基于spring cloud