Bean属性拷贝,主要针对几个常用的拷贝框架进行性能对比,以及功能扩展支持
选用的框架
cglib (直接使用Spring封装的BeanCopier)
apache
MapStruct
Spring
HuTool
I.背景 当业务量不大时,不管选择哪个框架都没什么问题,只要功能支持就ok了;但是当数据量大的时候,可能就需要考虑性能问题了;再实际的项目中,正好遇到了这个问题,不仅慢,还发现会有锁竞争,这特么就尼普了
项目中使用的是Spring的 BeanUtils, 版本 3.2.4.RELEASE
, 版本相对较老,主要问题在于org.springframework.beans.CachedIntrospectionResults.forClass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 static CachedIntrospectionResults forClass (Class beanClass) throws BeansException { CachedIntrospectionResults results; Object value; synchronized (classCache) { value = classCache.get(beanClass); } if (value instanceof Reference) { Reference ref = (Reference) value; results = (CachedIntrospectionResults) ref.get(); } else { results = (CachedIntrospectionResults) value; } if (results == null ) { if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class .getClassLoader ()) || isClassLoaderAccepted (beanClass .getClassLoader ())) { results = new CachedIntrospectionResults(beanClass); synchronized (classCache) { classCache.put(beanClass, results); } } else { if (logger.isDebugEnabled()) { logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe" ); } results = new CachedIntrospectionResults(beanClass); synchronized (classCache) { classCache.put(beanClass, new WeakReference<CachedIntrospectionResults>(results)); } } } return results; }
看上面的实现,每次获取value都加了一个同步锁,而且还是锁的全局的classCache
,这就有些过分了啊,微妙的是这段代码注释,谷歌翻译之后为
我们不想在这里使用同步。 对象引用是原子的,因此我们可以只在启动时进行偶尔的不必要查找。
这意思大概是说我就在启动的时候用一下,并不会频繁的使用,所以使用了同步代码块也问题不大…
但是在BeanUtils#copyProperties
中就蛋疼了,每次都会执行这个方法,扎心了
当然我们现在一般用的Spring5+了,这段代码也早就做了改造了,新版的如下,不再存在上面的这个并发问题了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @SuppressWarnings ("unchecked" )static CachedIntrospectionResults forClass (Class<?> beanClass) throws BeansException { CachedIntrospectionResults results = strongClassCache.get(beanClass); if (results != null ) { return results; } results = softClassCache.get(beanClass); if (results != null ) { return results; } results = new CachedIntrospectionResults(beanClass); ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse; if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class .getClassLoader ()) || isClassLoaderAccepted (beanClass .getClassLoader ())) { classCacheToUse = strongClassCache; } else { if (logger.isDebugEnabled()) { logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe" ); } classCacheToUse = softClassCache; } CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results); return (existing != null ? existing : results); }
II. 不同框架使用姿势 接下来我们看一下几种常见的bean拷贝框架的使用姿势,以及对比测试
1. apache BeanUtils 阿里规范中,明确说明了,不要使用它,idea安装阿里的代码规范插件之后,会有提示
使用姿势比较简单,引入依赖
1 2 3 4 5 6 <dependency > <groupId > commons-beanutils</groupId > <artifactId > commons-beanutils</artifactId > <version > 1.9.4</version > </dependency >
属性拷贝
1 2 3 4 5 6 7 8 9 10 @Component public class ApacheCopier { public <K, T> T copy (K source, Class<T> target) throws IllegalAccessException, InstantiationException, InvocationTargetException { T res = target.newInstance(); BeanUtils.copyProperties(res, source); return res; } }
2. cglib BeanCopier cglib是通过动态代理的方式来实现属性拷贝的,与上面基于反射实现方式存在本质上的区别,这也是它性能更优秀的主因
在Spring环境下,一般不需要额外的引入依赖;或者直接引入spring-core
1 2 3 4 5 6 7 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-core</artifactId > <version > 5.2.8.RELEASE</version > <scope > compile</scope > </dependency >
属性拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Component public class SpringCglibCopier { public <K, T> T copy (K source, Class<T> target) throws IllegalAccessException, InstantiationException { BeanCopier copier = BeanCopier.create(source.getClass(), target, false ); T res = target.newInstance(); copier.copy(source, res, null ); return res; } }
当然也可以直接使用纯净版的cglib,引入依赖
1 2 3 4 5 <dependency > <groupId > cglib</groupId > <artifactId > cglib</artifactId > <version > 3.3.0</version > </dependency >
使用姿势和上面一模一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Component public class PureCglibCopier { public <K, T> T copy (K source, Class<T> target) throws IllegalAccessException, InstantiationException { BeanCopier copier = BeanCopier.create(source.getClass(), target, false ); T res = target.newInstance(); copier.copy(source, res, null ); return res; } }
3. spring BeanUtils 这里使用的是spring 5.2.1.RELEASE
, 就不要拿3.2来使用了,不然并发下的性能实在是感人
基于内省+反射,借助getter/setter方法实现属性拷贝,性能比apache高
核心依赖
1 2 3 4 5 6 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.2.1.RELEASE</version> <scope>compile</scope> </dependency>
属性拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Component public class SpringBeanCopier { public <K, T> T copy (K source, Class<T> target) throws IllegalAccessException, InstantiationException { T res = target.newInstance(); BeanUtils.copyProperties(source, res); return res; } }
hutool 提供了很多的java工具类,从测试效果来看它的性能比apache会高一点,当低于spring
引入依赖
1 2 3 4 5 <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-core</artifactId > <version > 5.6.0</version > </dependency >
使用姿势
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component public class HutoolCopier { public <K, T> T copy (K source, Class<T> target) throws Exception { return BeanUtil.toBean(source, target); } }
5. MapStruct MapStruct 性能更强悍了,缺点也比较明显,需要声明bean的转换接口,自动代码生成的方式来实现拷贝,性能媲美直接的get/set
引入依赖
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > 1.4.2.Final</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > 1.4.2.Final</version > </dependency >
使用姿势
1 2 3 4 5 6 7 8 9 10 11 12 13 @Mapper public interface MapStructCopier { Target copy (Source source) ; } @Component public class MapsCopier { private MapStructCopier mapStructCopier = Mappers.getMapper(MapStructCopier.class ) ; public Target copy (Source source, Class<Target> target) { return mapStructCopier.copy(source); } }
缺点也比较明显,需要显示的接口转换声明
6. 测试 定义两个Bean,用于转换测试,两个bean的成员属性名,类型完全一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class Source { private Integer id; private String user_name; private Double price; private List<Long> ids; private BigDecimal marketPrice; } @Data public class Target { private Integer id; private String user_name; private Double price; private List<Long> ids; private BigDecimal marketPrice; }
6.1 功能测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private Random random = new Random();public Source genSource () { Source source = new Source(); source.setId(random.nextInt()); source.setIds(Arrays.asList(random.nextLong(), random.nextLong(), random.nextLong())); source.setMarketPrice(new BigDecimal(random.nextFloat())); source.setPrice(random.nextInt(120 ) / 10.0 d); source.setUser_name("一灰灰Blog" ); return source; } private void copyTest () throws Exception { Source s = genSource(); Target ta = apacheCopier.copy(s, Target.class ) ; Target ts = springBeanCopier.copy(s, Target.class ) ; Target tc = springCglibCopier.copy(s, Target.class ) ; Target tp = pureCglibCopier.copy(s, Target.class ) ; Target th = hutoolCopier.copy(s, Target.class ) ; Target tm = mapsCopier.copy(s, Target.class ) ; System.out.println("source:\t" + s + "\napache:\t" + ta + "\nspring:\t" + ts + "\nsCglib:\t" + tc + "\npCglib:\t" + tp + "\nhuTool:\t" + th + "\nmapStruct:\t" + tm); }
输出结果如下,满足预期
1 2 3 4 5 6 7 source : Source(id=1337715455, user_name=一灰灰Blog, price=7.1, ids=[7283949433132389385, 3441022909341384204, 8273318310870260875], marketPrice=0.04279220104217529296875)apache: Target(id=1337715455, user_name=一灰灰Blog, price=7.1, ids=[7283949433132389385, 3441022909341384204, 8273318310870260875], marketPrice=0.04279220104217529296875) spring: Target(id=1337715455, user_name=一灰灰Blog, price=7.1, ids=[7283949433132389385, 3441022909341384204, 8273318310870260875], marketPrice=0.04279220104217529296875) sCglib: Target(id=1337715455, user_name=一灰灰Blog, price=7.1, ids=[7283949433132389385, 3441022909341384204, 8273318310870260875], marketPrice=0.04279220104217529296875) pCglib: Target(id=1337715455, user_name=一灰灰Blog, price=7.1, ids=[7283949433132389385, 3441022909341384204, 8273318310870260875], marketPrice=0.04279220104217529296875) huTool: Target(id=1337715455, user_name=一灰灰Blog, price=7.1, ids=[7283949433132389385, 3441022909341384204, 8273318310870260875], marketPrice=0.04279220104217529296875) mapStruct: Target(id=1337715455, user_name=一灰灰Blog, price=7.1, ids=[7283949433132389385, 3441022909341384204, 8273318310870260875], marketPrice=0.04279220104217529296875)
6.2 性能测试 接下来我们关注一下不同的工具包,实现属性拷贝的性能对比情况如何
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public void test () throws Exception { autoCheck(Target2.class , 10000) ; autoCheck(Target2.class , 10000) ; autoCheck(Target2.class , 10000_0 ) ; autoCheck(Target2.class , 50000_0 ) ; autoCheck(Target2.class , 10000_00 ) ; } private <T> void autoCheck (Class<T> target, int size) throws Exception { StopWatch stopWatch = new StopWatch(); runCopier(stopWatch, "apacheCopier" , size, (s) -> apacheCopier.copy(s, target)); runCopier(stopWatch, "springCglibCopier" , size, (s) -> springCglibCopier.copy(s, target)); runCopier(stopWatch, "pureCglibCopier" , size, (s) -> pureCglibCopier.copy(s, target)); runCopier(stopWatch, "hutoolCopier" , size, (s) -> hutoolCopier.copy(s, target)); runCopier(stopWatch, "springBeanCopier" , size, (s) -> springBeanCopier.copy(s, target)); runCopier(stopWatch, "mapStruct" , size, (s) -> mapsCopier.copy(s, target)); System.out.println((size / 10000 ) + "w -------- cost: " + stopWatch.prettyPrint()); } private <T> void runCopier (StopWatch stopWatch, String key, int size, CopierFunc func) throws Exception { stopWatch.start(key); for (int i = 0 ; i < size; i++) { Source s = genSource(); func.apply(s); } stopWatch.stop(); } @FunctionalInterface public interface CopierFunc <T > { T apply (Source s) throws Exception ; }
输出结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 1w -------- cost: StopWatch '' : running time = 583135900 ns --------------------------------------------- ns % Task name --------------------------------------------- 488136600 084% apacheCopier 009363500 002% springCglibCopier 009385500 002% pureCglibCopier 053982900 009% hutoolCopier 016976500 003% springBeanCopier 005290900 001% mapStruct 10w -------- cost: StopWatch '' : running time = 5607831900 ns --------------------------------------------- ns % Task name --------------------------------------------- 4646282100 083% apacheCopier 096047200 002% springCglibCopier 093815600 002% pureCglibCopier 548897800 010% hutoolCopier 169937400 003% springBeanCopier 052851800 001% mapStruct 50w -------- cost: StopWatch '' : running time = 27946743000 ns --------------------------------------------- ns % Task name --------------------------------------------- 23115325200 083% apacheCopier 481878600 002% springCglibCopier 475181600 002% pureCglibCopier 2750257900 010% hutoolCopier 855448400 003% springBeanCopier 268651300 001% mapStruct 100w -------- cost: StopWatch '' : running time = 57141483600 ns --------------------------------------------- ns % Task name --------------------------------------------- 46865332600 082% apacheCopier 1019163600 002% springCglibCopier 1033701100 002% pureCglibCopier 5897726100 010% hutoolCopier 1706155900 003% springBeanCopier 619404300 001% mapStruct
-
1w
10w
50w
100w
apache
0.488136600s / 084%
4.646282100s / 083%
23.115325200s / 083%
46.865332600s / 083%
spring cglib
0.009363500s / 002%
0.096047200s / 002%
0.481878600s / 002%
1.019163600s / 002%
pure cglibg
0.009385500s / 002%
0.093815600s / 002%
0.475181600s / 002%
1.033701100s / 002%
hutool
0.053982900s / 009%
0.548897800s / 010%
2.750257900s / 010%
5.897726100s / 010%
spring
0.016976500s / 003%
0.169937400s / 003%
0.855448400s / 003%
1.706155900s / 003%
mapstruct
0.005290900s / 001%
0.052851800s / 001%
0.268651300s / 001%
0.619404300s / 001%
total
0.583135900s
5.607831900s
27.946743000s
57.141483600s
上面的测试中,存在一个不同的变量,即不是用相同的source对象来测试不同的工具转换情况,但是这个不同并不会太影响不同框架的性能对比,基本上从上面的运行结果来看
mapstruct, cglib, spring 表现最好
apache 表现最差
基本趋势相当于:
apache -> 10 hutool -> 28 spring -> 45 cglib -> 83 mapstruct
如果我们需要实现简单的bean拷贝,选择cglib或者spring的是个不错选择
III. 其他 一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
2. 声明 尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
3. 扫描关注 一灰灰blog