Java常见bean mapper的性能及原理分析

背景

在分层的代码架构中,层与层之间的对象避免不了要做很多转换、赋值等操作,这些操作重复且繁琐,于是乎催生出很多工具来优雅,高效地完成这个操作,有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下运行后的结果如下:

Java常见bean mapper的性能及原理分析_第1张图片

图片

Score表示的是平均运行时间,单位是微秒。从执行效率来看,可以看出 beanCopier > orika > springBeanUtil > dozer > apacheBeanUtil。这样的结果跟它们各自的实现原理有很大的关系,

下面将详细每个工具的使用及实现原理。

Spring的BeanUtils

使用

这个工具可能是大家日常使用最多的,因为是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

使用

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

使用

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方法中要考虑所有的属性。

Dozer

使用

上面提到的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));      
}  

Java常见bean mapper的性能及原理分析_第2张图片

你可能感兴趣的:(java,mybatis,spring,架构,分布式)