引言
二狗:二胖快醒醒,趕緊看看剛才報警郵件,你上次寫的儲存使用者接口耗時(
《二胖的參數校驗坎坷之路》)大大上升,趕緊排查下原因。
二胖:好的,馬上看,内心戲可十足(心裡卻在抱怨,大中午的攪我發财美夢,剛剛夢見我買的股票又漲停了就被叫醒了)。牢騷歸牢騷,自己的問題還是得看啊,畢竟是自己寫的
bug
,含着淚也要把它修複掉。二胖對分析這種問題還是得心應手的,畢竟已經是久經職場的老油條了。
測試環境複現問題
二胖首先通過内部的監控工具看了下這段時間的網絡是否正常,以及
cpu
的使用情況、
資料庫
的耗時等,這些名額看起來都是正常的,唯一稍微有點差別的是這段時間流量上漲了一些,肯定又是公司花錢搞營銷砸廣告了。接着二胖又通過
cat(大衆點評開源監控工具)分析了幾個請求,每個階段的耗時看下來都
ok
。卧槽這可咋辦列居然難倒二胖了,如果生産環境問題可以在測試環境複現就好了,這樣解覺問題就簡單多了。生産不是流量上漲了一些嗎?那測試環境來壓測一把吧,二胖果斷的下載下傳了一個jmeter(壓測工具)在測試環境進行了一把瘋狂的壓測,果然出現了和生産一樣的問題。能夠複現問題就好,這樣離解決問題就近了一大步。
arthas定位問題
問題是複現了,接下來就是找出接口比較耗時的地方了。一般我們找接口耗時較長的地方,都是通過記錄日志列印每一步的耗時。這是比較常見做法,不過二胖記得上次部門技術大拿“二狗”分享過一個神器arthas可以輸出方法路徑上的每個節點上耗時。苦于一直沒有機會拿它來用于實際操作,今天終于可以拿它來好好練手了。安裝什麼的就不介紹了,這個
官網都寫的比較詳細,并且文檔也是中文的,非常容易上手。下面我們就來使用下
arthas
吧。
啟動成功的界面

下面我們根據arthas提供的
trace
指令來看看接口的耗時都是在哪裡。
我們從上面可以看出主要耗時是集中在
org.apache.commons.beanutils.BeanUtils#copyProperties
這個方法上面的,不就一個實體之間的屬性指派轉換嗎,需要這麼耗時這麼久嗎?不科學啊,
apache
提供的方法還能這麼
low
嗎?帶着這些問題我們看看其他提供的屬性拷貝的工具類效率如何。
使用JMH對常見屬性指派操作性能比較
- 使用
、get
方法複制。set
-
的cglib
。BeanCopier
-
Spring
BeanUtils
-
apache
BeanUtils
-
下面我們就來對上面這些操作來進行一波性能比較。MapStruct
編寫下面的測試類。
/**
* @author:
* @Date: 2020/7/11
* @Description:
*/
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(6)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BeanCopyTest {
@Param(value = {"1","10","100"})
private int count;
public UserBO bo;
public BeanCopier copier;
@Setup(Level.Trial) // 初始化方法,在全部Benchmark運作之前進行
public void init() {
copier = BeanCopier.create(UserBO.class, UserVO.class, false);
bo = new UserBO();
bo.setUserName("java金融");
bo.setAge(1);
bo.setIdCard("88888888");
bo.setEmail("java金融@qq.com");
}
public static void main(String[] args) throws RunnerException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Options opt = new OptionsBuilder().include(BeanCopyTest.class.getSimpleName()).result("result.json").resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
/**
* 使用mapStruct來操作
*/
@Benchmark
public void mapStruct() {
for (int i = 1; i <= count; i++) {
UserVO vo = UserMapping.INSTANCE.converter(bo);
}
}
/**
* 手動set和Get
*/
@Benchmark
public void setAndGet() {
for (int i = 1; i <= count; i++) {
UserVO userVO = new UserVO();
userVO.setUserName(bo.getUserName());
userVO.setEmail(bo.getEmail());
userVO.setSex(bo.getSex());
userVO.setIdCard(bo.getIdCard());
userVO.setAge(bo.getAge());
}
}
/**
* 使用cglib的copy方法
*/
@Benchmark
public void cglibBeanCopier() {
for (int i = 1; i <= count; i++) {
UserVO vo = new UserVO();
copier.copy(bo, vo, null);
}
}
/**
* 使用spring提供的copyProperties方法
*/
@Benchmark
public void springBeanUtils() {
for (int i = 1; i <= count; i++) {
UserVO vo = new UserVO();
BeanUtils.copyProperties(bo, vo);
}
}
/**
* 使用apache的copyProperties方法
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
@Benchmark
public void apacheBeanUtils() throws InvocationTargetException, IllegalAccessException {
for (int i = 1; i <= count; i++) {
UserVO vo = new UserVO();
org.apache.commons.beanutils.BeanUtils.copyProperties(vo, bo);
}
}
最後的測試結果如下所示:
Benchmark (count) Mode Cnt Score Error Units
BeanCopyTest.apacheBeanUtils 1 avgt 5 2462103.419 ± 2292830.495 ns/op
BeanCopyTest.apacheBeanUtils 10 avgt 5 21025926.689 ± 11254755.603 ns/op
BeanCopyTest.apacheBeanUtils 100 avgt 5 193235312.113 ± 37929707.246 ns/op
BeanCopyTest.cglibBeanCopier 1 avgt 5 4.936 ± 1.187 ns/op
BeanCopyTest.cglibBeanCopier 10 avgt 5 4.820 ± 1.963 ns/op
BeanCopyTest.cglibBeanCopier 100 avgt 5 4.269 ± 0.890 ns/op
BeanCopyTest.mapStruct 1 avgt 5 4.809 ± 1.720 ns/op
BeanCopyTest.mapStruct 10 avgt 5 4.947 ± 1.320 ns/op
BeanCopyTest.mapStruct 100 avgt 5 4.440 ± 1.191 ns/op
BeanCopyTest.setAndGet 1 avgt 5 3.780 ± 1.785 ns/op
BeanCopyTest.setAndGet 10 avgt 5 3.930 ± 1.788 ns/op
BeanCopyTest.setAndGet 100 avgt 5 4.069 ± 2.181 ns/op
BeanCopyTest.springBeanUtils 1 avgt 5 1190.563 ± 165.574 ns/op
BeanCopyTest.springBeanUtils 10 avgt 5 10887.244 ± 1228.026 ns/op
BeanCopyTest.springBeanUtils 100 avgt 5 109686.562 ± 7485.261 ns/op
- 從上述結論中我們可以發現性能最好的是排名 用
get
方法複制,其次是set
和mapStruct
,再接着是cglib的BeanCopier
,最後的是Spring的beanUtils
apache的BeanUtils
- 如果對上述測試性能感興趣的話,代碼都已上傳到
上可自行下載下傳運作對比下結果。 代碼位址github
- 關于對
的使用就不介紹了,感興趣的可自行谷歌。不過如果要進行性能比較的話,真心推薦使用下,結果可以通過導出JMH
檔案然後生成圖表。json
為什麼apacheBeanUtils性能最差
apacheBeanUtils
spring
beanUtils
都是底層都是使用反射來進行指派的,為什麼
apacheBeanUtils
的性能要差一大截列。源碼之下無秘密,下面我們來看看這個方法的源碼。
Apache BeanUtils
列印了大量的日志、以及各種轉換、類型的判斷等等導緻性能變差。
- 而
spring
直接使用反射省,幹淨利索,核心代碼見下圖。beanUtil
Apache的代碼居然也有"bug"? - 其實在《阿裡巴巴開發手冊》(可在公衆号【java金融】回複“泰山”擷取)裡面也有說明屬性的
避免使用copy
apcheBeanUtils
Apache的代碼居然也有"bug"? - 如果生産環境已經大量使用
的話需要替換Apache BeanUtils
的話需要注意下他們兩個雖然提供的方法都是spring BeanUtils
但是他們的參數是反的,這點需要注意下,不要直接換個引入的包名完事。copyProperties
總結
- 實際使用中的話一般是不會使用
get
方法複制,容易漏掉屬性并且也是一個體力活。推薦使用set
,在編譯過程中,mapStruct
将生成該接口的實作,并且它還可以實作不同名字的映射,比如可以把MapStruct
映射到name
,靈活性比較高。username
- 二胖感覺今天收獲滿滿啊,一下學到了
jmeter
arthas
三個軟體的使用。JMH
結束
- 由于自己才疏學淺,難免會有纰漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
- 如果你覺得文章還不錯,你的轉發、分享、贊賞、點贊、留言就是對我最大的鼓勵。
- 感謝您的閱讀,十分歡迎并感謝您的關注。
Apache的代碼居然也有"bug"?