03.01 Java Bean Copy方案對比


Java Bean Copy方案對比


一、問題分析

背景

相同server機器上的相同方法在方法調用鏈任何參數都一致的情況消耗時間差別非常大,舉例說明,類A有方法demo(), 通過分析發現同一臺機器(也是一個jvm進程)對該方法的兩次調用消耗時間竟然有200ms的差距。同時,方法實現上沒有使用任何的併發以及緩存,唯一特殊的是方法內使用了Apache BeanUtils.copyProperties,懷疑是這個方法有貓膩,於是開始重點分析該方法實現。

分析過程

現象分析

猜想如果是BeanUtils.copyProperties有問題,那麼現象上應該是調用BeanUtils.copyProperties完成bean copy的過程可能會偶然出現性能問題,於是寫了一個demo 循環調用BeanUtils.copyProperties完成bean copy,demo可以參考下文。

驗證結果:

  • 單線程模型下,第一次訪問BeanUtils.copyProperties耗時有200-300ms左右,後續訪問幾乎都是0ms,也就是微秒級別。
  • 併發模型下,每個線程訪問BeanUtils.copyProperties會有一次200-300ms耗時, 也就是高性能耗時次數與併發線程數一致。

根據以上驗證結果猜測:

  • BeanUtils.copyProperties有一種線程級別的“緩存”,第一次刷新緩存耗時較長,後續直接讀”緩存”耗時較短。
  • 這種“緩存”是線程粒度。

源碼剖析

首先,要獲取一個BeanUtilsBean實例,猜測這應該是一個單例模型的實現。

Java Bean Copy方案對比

接下來我們看其實現,原來是一個“假單例”,並且是一個線程對應一個BeanUtils實例,接著看下去。

Java Bean Copy方案對比

實現上為了保證線程安全使用了synchronized ,隨後debug了一下性能耗時主要在initalvalue(),可以看到線程內只有第一次調用get會初始化執行該方法,那麼理論上說得通了。

Java Bean Copy方案對比

通過分析源碼很容易解釋為啥同一個方法調用會偶然耗時較長了,主要兩個原因:1. 兩個方法在不同線程執行,如果其中一個線程是第一次調用,框架需要先初始化BeanUtils實例,需要消耗200ms左右的性能。2. 併發訪問同一個BeanUtils實例時會出現線程阻塞。

二 、Apache, Cglib, Spring bean copy 性能對比

目前主流的bean copy框架有apache, cglib, springframework 等,寫法上大同小異,作為開發者我們更關注的偏向於性能,下文demo將綜合對比apache,cglib,springframework以及傳統的java bean 屬性set等四種方案的性能指標。

2.1 代碼結構介紹


首先,定義一個通用接口,包含一個方法copyBean

<code>package com.free.life.base.beancopy;

/**
* bean copy common facade.
*
* @author yzq
* @date 2018/01/16
*/
public interface BeanCopyFacade {

/**
* bean copy.
*
* @param sourceBean source bean
* @param targetBean target bean
* @throws Exception root exception
*/

void copyBean(S sourceBean, T targetBean) throws Exception;
}

/<code>

使用apache BeanUtils實現方式

<code>package com.free.life.base.beancopy;

import org.apache.commons.beanutils.BeanUtils;

/**
* apache copyProperties.
*
* @author yzq
* @date 2018/01/16
*/
public class ApacheBeanCopy implements BeanCopyFacade<sourcebean> {

@Override
public void copyBean(SourceBean sourceBean, TargetBean targetBean) throws Exception {
long start = System.nanoTime();
BeanUtils.copyProperties(targetBean, sourceBean);
long end = System.nanoTime();

System.out.println(String.format("%s consume %d microsecond", "apache copy property", (end - start) / 1000));
}
}

/<sourcebean>/<code>

使用cglib BeanCopier實現

<code>package com.free.life.base.beancopy;

import net.sf.cglib.beans.BeanCopier;

/**
* cglib BeanCopier copy.
*
* @author yzq
* @date 2018/01/16
*/
public class CglibBeanCopy implements BeanCopyFacade<sourcebean> {

private BeanCopier beanCopier = BeanCopier.create(SourceBean.class, TargetBean.class, false);

@Override

public void copyBean(SourceBean sourceBean, TargetBean targetBean) throws Exception {
long start = System.nanoTime();
beanCopier.copy(sourceBean, targetBean, null);
long end = System.nanoTime();

System.out.println(String.format("%s consume %d microsecond", "cglib BeanCopier", (end - start) / 1000));
}
}

/<sourcebean>/<code>

使用spring BeanUtils

<code>package com.free.life.base.beancopy;

import org.springframework.beans.BeanUtils;

/**
* spring framework copy bean.
*
* @author yzq
* @date 2018/01/16
*/
public class SpringBeanCopy implements BeanCopyFacade<sourcebean> {

@Override
public void copyBean(SourceBean sourceBean, TargetBean targetBean) throws Exception {
long start = System.nanoTime();
BeanUtils.copyProperties(sourceBean, targetBean);
long end = System.nanoTime();

System.out.println(String.format("%s consume %d microsecond", "spring copyProperties", (end - start) / 1000));
}
}

/<sourcebean>/<code>

使用 java setter

<code>package com.free.life.base.beancopy;

/**
* use setter/getter
*
* @author yzq
* @date 2018/01/16
*/
public class JavaBeanCopy implements BeanCopyFacade<sourcebean> {

@Override

public void copyBean(SourceBean sourceBean, TargetBean targetBean) throws Exception {
long start = System.nanoTime();
targetBean.setId(sourceBean.getId());
targetBean.setName(sourceBean.getName());
targetBean.setResult(sourceBean.getResult());
targetBean.setContent(sourceBean.getContent());
long end = System.nanoTime();

System.out.println(String.format("%s consume %d microsecond", "use setter", (end - start) / 1000));
}
}

/<sourcebean>/<code>

Main方法入口測試多種方案性能

<code>package com.free.life.base.beancopy;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* bean copy demo.
*
* @author yzq
* @date 2018/01/16
*/
public class BeanCopyDemo {

private static BeanCopyFacade apacheBeanCopy;
private static BeanCopyFacade cglibBeanCopy;
private static BeanCopyFacade springBeanCopy;
private static BeanCopyFacade javaBeanCopy;

static {
apacheBeanCopy = new ApacheBeanCopy();
cglibBeanCopy = new CglibBeanCopy();
springBeanCopy = new SpringBeanCopy();
javaBeanCopy = new JavaBeanCopy();
}

public static void main(String[] args) throws Exception {
final Integer loopCount = 10;

SourceBean sourceBean = new SourceBean();
sourceBean.setId(1);
sourceBean.setName("yzq");
sourceBean.setResult(Boolean.TRUE);
sourceBean.setContent("bean copy test.");


TargetBean targetBean = new TargetBean();

multiThread(loopCount, sourceBean, targetBean);

singleThreadTest(loopCount, sourceBean, targetBean);
}

private static void multiThread(Integer loopCount, SourceBean sourceBean, TargetBean targetBean) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < loopCount; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
try {
apacheBeanCopy.copyBean(sourceBean, targetBean);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}

private static void singleThreadTest(Integer loopCount, SourceBean sourceBean, TargetBean targetBean)
throws Exception {
System.out.println("---------------- apache ----------------------");

for (int i = 0; i < loopCount; i++) {

apacheBeanCopy.copyBean(sourceBean, targetBean);
}

System.out.println("---------------- cglib ----------------------");

for (int i = 0; i < loopCount; i++) {
cglibBeanCopy.copyBean(sourceBean, targetBean);
}

System.out.println("----------------- spring ---------------------");

for (int i = 0; i < loopCount; i++) {
springBeanCopy.copyBean(sourceBean, targetBean);
}

System.out.println("----------------- setter ---------------------");

for (int i = 0; i < loopCount; i++) {
javaBeanCopy.copyBean(sourceBean, targetBean);
}

}

}
/<code>

2.2 測試結果


  • 運行環境:macbook pro i7,4core,16gjdk:1.8.0_144
  • 測試方式:循環1w次調用
  • 測試結果均值(單位為*微秒*):apache: 200cglib: 1spring: 20setter: 0

綜上: 性能 setter > cglib > spring > apache

三 、使用建議

bean copy對比傳統的做法優缺點

優點

  • 寫法優雅簡潔。
  • 一些相對高階的使用方式比較簡潔,比如反射方式獲取類屬性值。

缺點

  • 性能較差,因為beancopy框架背後的實現都是通過java反射機制去做的,通常情況性能不會比normal方式更優。
  • 引用查找難,bean copy的實現會隱藏對象屬性的設置的調用,比如copy(source,taget) 我想查看target屬性A有哪些地方被設值了,那麼通過IDE查找引用會漏掉bean copy的引用。

實踐建議

  • bean copy場景較少或者對性能要求較高的部分避免使用任何bean copy框架。
  • 如果要使用bean copy框架,優先使用cglib,且要做性能測試。同時需要注意:cglib使用BeanCopier.create()也是非常耗時,避免多次調用,儘可能做成全局初始化一次
  • 可以使用lombok builder或者自己封裝builder模式相對優雅的代替setter/bean copy
  • 關注IDE的任何一個警告信息,儘可能消除任何警告信息。

最後, 集團規約銘記在心。

一點感想

  • 代碼沒有秘密,代碼是可信的,同時它也是不可信的。
  • 永遠不要忽略工具的作用,工具是這個世界的解決方案。


分享到:


相關文章: