作者:一灰灰
博客:一灰灰Blog - 知乎 (zhihu.com)
原文:常见Bean拷贝框架使用姿势及性能对比 - 知乎 (zhihu.com)
目录
Bean属性拷贝,主要针对几个常用的拷贝框架进行性能对比,以及功能扩展支持
选用的框架
- cglib (直接使用Spring封装的BeanCopier)
- apache
- MapStruct
- Spring
- HuTool
I.背景
当业务量不大时,不管选择哪个框架都没什么问题,只要功能支持就ok了;但是当数据量大的时候,可能就需要考虑性能问题了;再实际的项目中,正好遇到了这个问题,不仅慢,还发现会有锁竞争,这特么就尼普了
项目中使用的是Spring的 BeanUtils, 版本 3.2.4.RELEASE
, 版本相对较老,主要问题在于org.springframework.beans.CachedIntrospectionResults.forClass
/**
* 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+了,这段代码也早就做了改造了,新版的如下,不再存在上面的这个并发问题了
/**
* 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安装阿里的代码规范插件之后,会有提示
使用姿势比较简单,引入依赖
<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
属性拷贝
@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
<!-- cglib -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.8.RELEASE</version>
<scope>compile</scope>
</dependency>
属性拷贝
@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,引入依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
使用姿势和上面一模一样
@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高
核心依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.1.RELEASE</version>
<scope>compile</scope>
</dependency>
属性拷贝
@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
引入依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.6.0</version>
</dependency>
使用姿势
@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
引入依赖
<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>
使用姿势
@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的成员属性名,类型完全一致
@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 功能测试
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);
}
输出结果如下,满足预期
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 性能测试
接下来我们关注一下不同的工具包,实现属性拷贝的性能对比情况如何
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;
}
输出结果如下
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 |
---|
上面的测试中,存在一个不同的变量,即不是用相同的source对象来测试不同的工具转换情况,但是这个不同并不会太影响不同框架的性能对比,基本上从上面的运行结果来看
- mapstruct, cglib, spring 表现最好
- apache 表现最差
基本趋势相当于:
apache -> 10 * hutool -> 28 * spring -> 45 * cglib -> 83 * mapstruct
如果我们需要实现简单的bean拷贝,选择cglib或者spring的是个不错选择