在分层的代码架构中,层与层之间的对象避免不了要做很多转换、赋值等操作,这些操作重复且繁琐,于是乎催生出很多工具来优雅,高效地完成这个操作,有BeanUtils、BeanCopier、Dozer、Orika等等,本文将讲述上面几个工具的使用、性能对比及原理分析。
其实这几个工具要做的事情很简单,而且在使用上也是类似的,所以我觉得先给大家看看性能分析的对比结果,让大家有一个大概的认识。我是使用JMH来做性能分析的,代码如下:
要复制的对象比较简单,包含了一些基本类型;有一次warmup,因为一些工具是需要“预编译”和做缓存的,这样做对比才会比较客观;分别复制1000、10000、100000个对象,这是比较常用数量级了吧。
@BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @Fork(1) @Warmup(iterations = 1) @State(Scope.Benchmark) public class BeanMapperBenchmark { @Param({"1000", "10000", "100000"}) private int times; private int time; private static MapperFactory mapperFactory; private static Mapper mapper; static { mapperFactory = new DefaultMapperFactory.Builder().build(); mapperFactory.classMap(SourceVO.class, TargetVO.class) .byDefault() .register(); mapper = DozerBeanMapperBuilder.create() .withMappingBuilder(new BeanMappingBuilder() { @Override protected void configure() { mapping(SourceVO.class, TargetVO.class) .fields("fullName", "name") .exclude("in"); } }).build(); } public static void main(String[] args) throws Exception { Options options = new OptionsBuilder() .include(BeanMapperBenchmark.class.getName()).measurementIterations(3) .build(); new Runner(options).run(); } @Setup public void prepare() { this.time = times; } @Benchmark public void springBeanUtilTest(){ SourceVO sourceVO = getSourceVO(); for(int i = 0; i < time; i++){ TargetVO targetVO = new TargetVO(); BeanUtils.copyProperties(sourceVO, targetVO); } } @Benchmark public void apacheBeanUtilTest() throws Exception{ SourceVO sourceVO = getSourceVO(); for(int i = 0; i < time; i++){ TargetVO targetVO = new TargetVO(); org.apache.commons.beanutils.BeanUtils.copyProperties(targetVO, sourceVO); } } @Benchmark public void beanCopierTest(){ SourceVO sourceVO = getSourceVO(); for(int i = 0; i < time; i++){ TargetVO targetVO = new TargetVO(); BeanCopier bc = BeanCopier.create(SourceVO.class, TargetVO.class, false); bc.copy(sourceVO, targetVO, null); } } @Benchmark public void dozerTest(){ SourceVO sourceVO = getSourceVO(); for(int i = 0; i < time; i++){ TargetVO map = mapper.map(sourceVO, TargetVO.class); } } @Benchmark public void orikaTest(){ SourceVO sourceVO = getSourceVO(); for(int i = 0; i < time; i++){ MapperFacade mapper = mapperFactory.getMapperFacade(); TargetVO map = mapper.map(sourceVO, TargetVO.class); } } private SourceVO getSourceVO(){ SourceVO sourceVO = new SourceVO(); sourceVO.setP1(1); sourceVO.setP2(2L); sourceVO.setP3(new Integer(3).byteValue()); sourceVO.setDate1(new Date()); sourceVO.setPattr1("1"); sourceVO.setIn(new SourceVO.Inner(1)); sourceVO.setFullName("alben"); return sourceVO; } }
在我macbook下运行后的结果如下:
图片
Score表示的是平均运行时间,单位是微秒。从执行效率来看,可以看出 beanCopier > orika > springBeanUtil > dozer > apacheBeanUtil。这样的结果跟它们各自的实现原理有很大的关系,
下面将详细每个工具的使用及实现原理。
这个工具可能是大家日常使用最多的,因为是Spring自带的,使用也简单:BeanUtils.copyProperties(sourceVO, targetVO);
Spring BeanUtils的实现原理也比较简答,就是通过Java的Introspector获取到两个类的PropertyDescriptor,对比两个属性具有相同的名字和类型,如果是,则进行赋值(通过ReadMethod获取值,通过WriteMethod赋值),否则忽略。
为了提高性能Spring对BeanInfo和PropertyDescriptor进行了缓存。
(源码基于:org.springframework:spring-beans:4.3.9.RELEASE)
/** * Copy the property values of the given source bean into the given target bean. * Note: The source and target classes do not have to match or even be derived * from each other, as long as the properties match. Any bean properties that the * source bean exposes but the target bean does not will silently be ignored. * @param source the source bean * @param target the target bean * @param editable the class (or interface) to restrict property setting to * @param ignoreProperties array of property names to ignore * @throws BeansException if the copying failed * @see BeanWrapper */ private static void copyProperties(Object source, Object target, Class> editable, String... ignoreProperties) throws BeansException { Assert.notNull(source, "Source must not be null"); Assert.notNull(target, "Target must not be null"); Class> actualEditable = target.getClass(); if (editable != null) { if (!editable.isInstance(target)) { throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]"); } actualEditable = editable; } //获取target类的属性(有缓存) PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); List ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null); for (PropertyDescriptor targetPd : targetPds) { Method writeMethod = targetPd.getWriteMethod(); if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) { //获取source类的属性(有缓存) PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); if (sourcePd != null) { Method readMethod = sourcePd.getReadMethod(); if (readMethod != null && //判断target的setter方法入参和source的getter方法返回类型是否一致 ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], 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); } //赋值到target writeMethod.invoke(target, value); } catch (Throwable ex) { throw new FatalBeanException( "Could not copy property '" + targetPd.getName() + "' from source to target", ex); } } } } } }
Spring BeanUtils的实现就是这么简洁,这也是它性能比较高的原因。
不过,过于简洁就失去了灵活性和可扩展性了,Spring BeanUtils的使用限制也比较明显,要求类属性的名字和类型一致,这点在使用时要注意。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
Apache的BeanUtils和Spring的BeanUtils的使用是一样的:
BeanUtils.copyProperties(targetVO, sourceVO);
要注意,source和target的入参位置不同。
Apache的BeanUtils的实现原理跟Spring的BeanUtils一样,也是主要通过Java的Introspector机制获取到类的属性来进行赋值操作,对BeanInfo和PropertyDescriptor同样有缓存,但是Apache BeanUtils加了一些不那么使用的特性(包括支持Map类型、支持自定义的DynaBean类型、支持属性名的表达式等等)在里面,使得性能相对Spring的BeanUtils来说有所下降。
(源码基于:commons-beanutils:commons-beanutils:1.9.3)
public void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException { if (dest == null) { throw new IllegalArgumentException ("No destination bean specified"); } if (orig == null) { throw new IllegalArgumentException("No origin bean specified"); } if (log.isDebugEnabled()) { log.debug("BeanUtils.copyProperties(" + dest + ", " + orig + ")"); } // Apache Common自定义的DynaBean if (orig instanceof DynaBean) { final DynaProperty[] origDescriptors = ((DynaBean) orig).getDynaClass().getDynaProperties(); for (DynaProperty origDescriptor : origDescriptors) { final String name = origDescriptor.getName(); // Need to check isReadable() for WrapDynaBean // (see Jira issue# BEANUTILS-61) if (getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)) { final Object value = ((DynaBean) orig).get(name); copyProperty(dest, name, value); } } // Map类型 } else if (orig instanceof Map) { @SuppressWarnings("unchecked") final // Map properties are always of type Map propMap = (Map) orig; for (final Map.Entry entry : propMap.entrySet()) { final String name = entry.getKey(); if (getPropertyUtils().isWriteable(dest, name)) { copyProperty(dest, name, entry.getValue()); } } // 标准的JavaBean } else { final PropertyDescriptor[] origDescriptors = //获取PropertyDescriptor getPropertyUtils().getPropertyDescriptors(orig); for (PropertyDescriptor origDescriptor : origDescriptors) { final String name = origDescriptor.getName(); if ("class".equals(name)) { continue; // No point in trying to set an object's class } //是否可读和可写 if (getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)) { try { //获取源值 final Object value = getPropertyUtils().getSimpleProperty(orig, name); //赋值操作 copyProperty(dest, name, value); } catch (final NoSuchMethodException e) { // Should not happen } } } } }
Apache BeanUtils的实现跟Spring BeanUtils总体上类似,但是性能却低很多,这个可以从上面性能比较看出来。阿里的Java规范是不建议使用的。
BeanCopier在cglib包里,它的使用也比较简单:
@Test public void beanCopierSimpleTest() { SourceVO sourceVO = getSourceVO(); log.info("source={}", GsonUtil.toJson(sourceVO)); TargetVO targetVO = new TargetVO(); BeanCopier bc = BeanCopier.create(SourceVO.class, TargetVO.class, false); bc.copy(sourceVO, targetVO, null); log.info("target={}", GsonUtil.toJson(targetVO)); }
只需要预先定义好要转换的source类和target类就好了,可以选择是否使用Converter,这个下面会说到。
在上面的性能测试中,BeanCopier是所有中表现最好的,那么我们分析一下它的实现原理。
BeanCopier的实现原理跟BeanUtils截然不同,它不是利用反射对属性进行赋值,而是直接使用cglib来生成带有的get/set方法的class类,然后执行。由于是直接生成字节码执行,所以BeanCopier的性能接近手写
get/set。
BeanCopier.create方法
public static BeanCopier create(Class source, Class target, boolean useConverter) { Generator gen = new Generator(); gen.setSource(source); gen.setTarget(target); gen.setUseConverter(useConverter); return gen.create(); } public BeanCopier create() { Object key = KEY_FACTORY.newInstance(source.getName(), target.getName(), useConverter); return (BeanCopier)super.create(key); }
这里的意思是用KEY_FACTORY创建一个BeanCopier出来,然后调用create方法来生成字节码。
KEY_FACTORY其实就是用cglib通过BeanCopierKey接口生成出来的一个类
private static final BeanCopierKey KEY_FACTORY = (BeanCopierKey)KeyFactory.create(BeanCopierKey.class); interface BeanCopierKey { public Object newInstance(String source, String target, boolean useConverter); }
通过设置
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "path");
可以让cglib输出生成类的class文件,我们可以反复译看看里面的代码
下面是KEY_FACTORY的类
public class BeanCopier$BeanCopierKey$$KeyFactoryByCGLIB$$f32401fd extends KeyFactory implements BeanCopierKey { private final String FIELD_0; private final String FIELD_1; private final boolean FIELD_2; public BeanCopier$BeanCopierKey$$KeyFactoryByCGLIB$$f32401fd() { } public Object newInstance(String var1, String var2, boolean var3) { return new BeanCopier$BeanCopierKey$$KeyFactoryByCGLIB$$f32401fd(var1, var2, var3); } public BeanCopier$BeanCopierKey$$KeyFactoryByCGLIB$$f32401fd(String var1, String var2, boolean var3) { this.FIELD_0 = var1; this.FIELD_1 = var2; this.FIELD_2 = var3; } //省去hashCode等方法。。。 }
继续跟踪Generator.create方法,由于Generator是继承AbstractClassGenerator,这个AbstractClassGenerator是cglib用来生成字节码的一个模板类,Generator的super.create其实调用
AbstractClassGenerator的create方法,最终会调用到Generator的模板方法generateClass方法,我们不去细究AbstractClassGenerator的细节,重点看generateClass。
这个是一个生成java类的方法,理解起来就好像我们平时写代码一样。
public void generateClass(ClassVisitor v) { Type sourceType = Type.getType(source); Type targetType = Type.getType(target); ClassEmitter ce = new ClassEmitter(v); //开始“写”类,这里有修饰符、类名、父类等信息 ce.begin_class(Constants.V1_2, Constants.ACC_PUBLIC, getClassName(), BEAN_COPIER, null, Constants.SOURCE_FILE); //没有构造方法 EmitUtils.null_constructor(ce); //开始“写”一个方法,方法名是copy CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, COPY, null); //通过Introspector获取source类和target类的PropertyDescriptor PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(source); PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(target); Map names = new HashMap(); for (int i = 0; i < getters.length; i++) { names.put(getters[i].getName(), getters[i]); } Local targetLocal = e.make_local(); Local sourceLocal = e.make_local(); if (useConverter) { e.load_arg(1); e.checkcast(targetType); e.store_local(targetLocal); e.load_arg(0); e.checkcast(sourceType); e.store_local(sourceLocal); } else { e.load_arg(1); e.checkcast(targetType); e.load_arg(0); e.checkcast(sourceType); } //通过属性名来生成转换的代码 //以setter作为遍历 for (int i = 0; i < setters.length; i++) { PropertyDescriptor setter = setters[i]; //根据setter的name获取getter PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName()); if (getter != null) { //获取读写方法 MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod()); MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod()); //如果用了useConverter,则进行下面的拼装代码方式 if (useConverter) { Type setterType = write.getSignature().getArgumentTypes()[0]; e.load_local(targetLocal); e.load_arg(2); e.load_local(sourceLocal); e.invoke(read); e.box(read.getSignature().getReturnType()); EmitUtils.load_class(e, setterType); e.push(write.getSignature().getName()); e.invoke_interface(CONVERTER, CONVERT); e.unbox_or_zero(setterType); e.invoke(write); //compatible用来判断getter和setter是否类型一致 } else if (compatible(getter, setter)) { e.dup2(); e.invoke(read); e.invoke(write); } } } e.return_value(); e.end_method(); ce.end_class(); } private static boolean compatible(PropertyDescriptor getter, PropertyDescriptor setter) { // TODO: allow automatic widening conversions? return setter.getPropertyType().isAssignableFrom(getter.getPropertyType()); }
即使没有使用过cglib也能读懂生成代码的流程吧,我们看看没有使用useConverter的情况下生成的代码:
public class Object$$BeanCopierByCGLIB$$d1d970c8 extends BeanCopier
{
public Object$$BeanCopierByCGLIB$$d1d970c8()
{
}
public void copy(Object var1, Object var2, Converter var3)
{
TargetVO var10000 = (TargetVO)var2;
SourceVO var10001 = (SourceVO)var1;
var10000.setDate1(((SourceVO)var1).getDate1());
var10000.setIn(var10001.getIn());
var10000.setListData(var10001.getListData());
var10000.setMapData(var10001.getMapData());
var10000.setP1(var10001.getP1());
var10000.setP2(var10001.getP2());
var10000.setP3(var10001.getP3());
var10000.setPattr1(var10001.getPattr1());
}
}
在对比上面生成代码的代码是不是豁然开朗了。
再看看使用useConverter的情况:
public class Object$$BeanCopierByCGLIB$$d1d970c7 extends BeanCopier
{
private static final Class CGLIB$load_class$java$2Eutil$2EDate;
private static final Class CGLIB$load_class$beanmapper_compare$2Evo$2ESourceVO$24Inner;
private static final Class CGLIB$load_class$java$2Eutil$2EList;
private static final Class CGLIB$load_class$java$2Eutil$2EMap;
private static final Class CGLIB$load_class$java$2Elang$2EInteger;
private static final Class CGLIB$load_class$java$2Elang$2ELong;
private static final Class CGLIB$load_class$java$2Elang$2EByte;
private static final Class CGLIB$load_class$java$2Elang$2EString;
public Object$$BeanCopierByCGLIB$$d1d970c7()
{
}
public void copy(Object var1, Object var2, Converter var3)
{
TargetVO var4 = (TargetVO)var2;
SourceVO var5 = (SourceVO)var1;
var4.setDate1((Date)
var3.convert(var5.getDate1(), CGLIB$load_class$java$2Eutil$2EDate, "setDate1"));
var4.setIn((Inner)var3.convert(var5.getIn(), CGLIB$load_class$beanmapper_compare$2Evo$2ESourceVO$24Inner, "setIn"));
var4.setListData((List)var3.convert(var5.getListData(), CGLIB$load_class$java$2Eutil$2EList, "setListData"));
var4.setMapData((Map)var3.convert(var5.getMapData(), CGLIB$load_class$java$2Eutil$2EMap, "setMapData"));
var4.setP1((Integer)var3.convert(var5.getP1(), CGLIB$load_class$java$2Elang$2EInteger, "setP1"));
var4.setP2((Long)var3.convert(var5.getP2(), CGLIB$load_class$java$2Elang$2ELong, "setP2"));
var4.setP3((Byte)var3.convert(var5.getP3(), CGLIB$load_class$java$2Elang$2EByte, "setP3"));
var4.setPattr1((String)var3.convert(var5.getPattr1(), CGLIB$load_class$java$2Elang$2EString, "setPattr1"));
var4.setSeq((Long)var3.convert(var5.getSeq(), CGLIB$load_class$java$2Elang$2ELong, "setSeq"));
}
static void CGLIB$STATICHOOK1()
{
CGLIB$load_class$java$2Eutil$2EDate = Class.forName("java.util.Date");
CGLIB$load_class$beanmapper_compare$2Evo$2ESourceVO$24Inner = Class.forName("beanmapper_compare.vo.SourceVO$Inner");
CGLIB$load_class$java$2Eutil$2EList = Class.forName("java.util.List");
CGLIB$load_class$java$2Eutil$2EMap = Class.forName("java.util.Map");
CGLIB$load_class$java$2Elang$2EInteger = Class.forName("java.lang.Integer");
CGLIB$load_class$java$2Elang$2ELong = Class.forName("java.lang.Long");
CGLIB$load_class$java$2Elang$2EByte = Class.forName("java.lang.Byte");
CGLIB$load_class$java$2Elang$2EString = Class.forName("java.lang.String");
}
static
{
CGLIB$STATICHOOK1();
}
}
BeanCopier性能确实很高,但从源码可以看出BeanCopier只会拷贝名称和类型都相同的属性,而且如果一旦使用Converter,BeanCopier只使用Converter定义的规则去拷贝属性,所以在convert方法中要考虑所有的属性。
上面提到的BeanUtils和BeanCopier都是功能比较简单的,需要属性名称一样,甚至类型也要一样。但是在大多数情况下这个要求就相对苛刻了,要知道有些VO由于各种原因不能修改,有些是外部接口SDK的对象,
有些对象的命名规则不同,例如有驼峰型的,有下划线的等等,各种什么情况都有。所以我们更加需要的是更加灵活丰富的功能,甚至可以做到定制化的转换。
Dozer就提供了这些功能,有支持同名隐式映射,支持基本类型互相转换,支持显示指定映射关系,支持exclude字段,支持递归匹配映射,支持深度匹配,支持Date to String的date-formate,支持自定义转换Converter,支持一次mapping定义多处使用,支持EventListener事件监听等等。不仅如此,Dozer在使用方式上,除了支持API,还支持XML和注解,满足大家的喜好。更多的功能可以参考这里
由于其功能很丰富,不可能每个都演示,这里只是给个大概认识,更详细的功能,或者XML和注解的配置,请看官方文档。
private Mapper dozerMapper;
@Before
public void setup()
{
dozerMapper = DozerBeanMapperBuilder.create()
.withMappingBuilder(new BeanMappingBuilder()
{
@Override
protected void configure()
{
mapping(SourceVO.class, TargetVO.class)
.fields("fullName", "name")
.exclude("in");
}
})
.withCustomConverter(null)
.withEventListener(null)
.build();
}
@Test
public void dozerTest()
{
SourceVO sourceVO = getSourceVO();
log.info("sourceVO={}", GsonUtil.toJson(sourceVO));
TargetVO map = dozerMapper.map(sourceVO, TargetVO.class);
log.info("map={}", GsonUtil.toJson(map));
}