210408-常见Bean拷贝框架使用姿势及性能对比

文章目录
  1. I.背景
  2. II. 不同框架使用姿势
    1. 1. apache BeanUtils
    2. 2. cglib BeanCopier
    3. 3. spring BeanUtils
    4. 4. hutool BeanUtil
    5. 5. MapStruct
    6. 6. 测试
      1. 6.1 功能测试
      2. 6.2 性能测试
  3. III. 其他
    1. 1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
    2. 2. 声明
    3. 3. 扫描关注

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
/**
* Create CachedIntrospectionResults for the given bean class.
* <P>We don't want to use synchronization here. Object references are atomic,
* so we can live with doing the occasional unnecessary lookup at startup only.
* @param beanClass the bean class to analyze
* @return the corresponding CachedIntrospectionResults
* @throws BeansException in case of introspection failure
*/
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
/**
* Create CachedIntrospectionResults for the given bean class.
* @param beanClass the bean class to analyze
* @return the corresponding CachedIntrospectionResults
* @throws BeansException in case of introspection failure
*/
@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
<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<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();
// 注意,第一个参数为target,第二个参数为source
// 与其他的正好相反
BeanUtils.copyProperties(res, source);
return res;
}
}

2. cglib BeanCopier

cglib是通过动态代理的方式来实现属性拷贝的,与上面基于反射实现方式存在本质上的区别,这也是它性能更优秀的主因

在Spring环境下,一般不需要额外的引入依赖;或者直接引入spring-core

1
2
3
4
5
6
7
<!--      cglib  -->
<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 {
/**
* cglib 对象转换
*
* @param source
* @param target
* @param <K>
* @param <T>
* @return
* @throws IllegalAccessException
* @throws InstantiationException
*/
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 {
/**
* cglib 对象转换
*
* @param source
* @param target
* @param <K>
* @param <T>
* @return
* @throws IllegalAccessException
* @throws InstantiationException
*/
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 {

/**
* 对象转换
*
* @param source
* @param target
* @param <K>
* @param <T>
* @return
* @throws IllegalAccessException
* @throws InstantiationException
*/
public <K, T> T copy(K source, Class<T> target) throws IllegalAccessException, InstantiationException {
T res = target.newInstance();
BeanUtils.copyProperties(source, res);
return res;
}
}

4. hutool BeanUtil

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 {

/**
* bean 对象转换
*
* @param source
* @param target
* @param <K>
* @param <T>
* @return
*/
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.0d);
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. 其他

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

QrCode

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×