天天看點

Apache的代碼居然也有"bug"?

引言

二狗:二胖快醒醒,趕緊看看剛才報警郵件,你上次寫的儲存使用者接口耗時(

《二胖的參數校驗坎坷之路》

)大大上升,趕緊排查下原因。

二胖:好的,馬上看,内心戲可十足(心裡卻在抱怨,大中午的攪我發财美夢,剛剛夢見我買的股票又漲停了就被叫醒了)。牢騷歸牢騷,自己的問題還是得看啊,畢竟是自己寫的

bug

,含着淚也要把它修複掉。二胖對分析這種問題還是得心應手的,畢竟已經是久經職場的老油條了。

測試環境複現問題

二胖首先通過内部的監控工具看了下這段時間的網絡是否正常,以及

cpu

的使用情況、

資料庫

的耗時等,這些名額看起來都是正常的,唯一稍微有點差別的是這段時間流量上漲了一些,肯定又是公司花錢搞營銷砸廣告了。接着二胖又通過

cat

(大衆點評開源監控工具)分析了幾個請求,每個階段的耗時看下來都

ok

。卧槽這可咋辦列居然難倒二胖了,如果生産環境問題可以在測試環境複現就好了,這樣解覺問題就簡單多了。生産不是流量上漲了一些嗎?那測試環境來壓測一把吧,二胖果斷的下載下傳了一個jmeter(壓測工具)在測試環境進行了一把瘋狂的壓測,果然出現了和生産一樣的問題。能夠複現問題就好,這樣離解決問題就近了一大步。

arthas定位問題

問題是複現了,接下來就是找出接口比較耗時的地方了。一般我們找接口耗時較長的地方,都是通過記錄日志列印每一步的耗時。這是比較常見做法,不過二胖記得上次部門技術大拿“二狗”分享過一個神器arthas可以輸出方法路徑上的每個節點上耗時。苦于一直沒有機會拿它來用于實際操作,今天終于可以拿它來好好練手了。安裝什麼的就不介紹了,這個

官網

都寫的比較詳細,并且文檔也是中文的,非常容易上手。下面我們就來使用下

arthas

吧。

啟動成功的界面

Apache的代碼居然也有"bug"?

下面我們根據arthas提供的

trace

指令來看看接口的耗時都是在哪裡。

Apache的代碼居然也有"bug"?

我們從上面可以看出主要耗時是集中在

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           
Apache的代碼居然也有"bug"?
  • 從上述結論中我們可以發現性能最好的是排名 用

    get

    set

    方法複制,其次是

    mapStruct

    cglib的BeanCopier

    ,再接着是

    Spring的beanUtils

    ,最後的是

    apache的BeanUtils

  • 如果對上述測試性能感興趣的話,代碼都已上傳到

    github

    上可自行下載下傳運作對比下結果。 代碼位址
  • 關于對

    JMH

    的使用就不介紹了,感興趣的可自行谷歌。不過如果要進行性能比較的話,真心推薦使用下,結果可以通過導出

    json

    檔案然後生成圖表。

為什麼apacheBeanUtils性能最差

apacheBeanUtils

spring

beanUtils

都是底層都是使用反射來進行指派的,為什麼

apacheBeanUtils

的性能要差一大截列。源碼之下無秘密,下面我們來看看這個方法的源碼。

Apache的代碼居然也有"bug"?

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"?