AOP實踐: Java利用註解和反射實現一個方便的函數性能測量工具

最初目的

在學習Java的集合類時,有時候想要測試代碼塊的運行時間,以比較不一樣算法數據結構之間的性能差別。最簡單的作法是在代碼塊的先後記錄時間戳,最後相減獲得該代碼塊的運行時間。java

下面是Java中的示例:python

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    algo(); // 執行代碼塊
    long end = System.currentTimeMillis();
    System.out.println(end - start);
}

當須要同時打印多個方法的運行時間以進行比較的時候就會變成這樣:算法

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    algo1(); // 算法1
    long end = System.currentTimeMillis();
    System.out.println(end - start);
    
    long start = System.currentTimeMillis();
    algo2(); // 算法2
    long end = System.currentTimeMillis();
    System.out.println(end - start);
  
    long start = System.currentTimeMillis();
    algo3(); // 算法3
    long end = System.currentTimeMillis();
    System.out.println(end - start);
  
    // more
}

初探

顯然上面的代碼看起來很是冗餘,因爲Java不支持func(func)這樣的直接傳遞函數指針,本人又不想引入JDK之外過重的工具,因此嘗試寫一個回調來實現代碼塊的傳遞:緩存

public interface Callback {
    void execute();
}
public class TimerUtil {
    public void getTime(Callback callback) {
        long start = System.currentTimeMillis();
        callback.execute();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}
// 測試類
public class Foo {
    
    void algo1() {
        // algo1
    }
    
    void algo2() {
        // algo2
    }
    
    void algo3() {
        // algo3
    }
    
    public static void main(String[] foo){
        TimerUtil tu = new TimerUtil();
        tu.getTime(new Callback() {
            @Override
            public void execute() {
                new Foo().algo1();
            }
        });
        tu.getTime(new Callback() {
            @Override
            public void execute() {
                new Foo().algo2();
            }
        });
        tu.getTime(new Callback() {
            @Override
            public void execute() {
                new Foo().algo3();
            }
        });
    }
}

發現此時雖然封裝了計時、打印等業務無關的代碼,然而對使用者來講代碼量並無減小多少。若仔細觀察,其實測試類中仍有一堆結構重複的代碼,真正的業務藏在一堆匿名類中間,視覺上干擾很大。數據結構

Java 8爲了解決相似的問題,引入了lambda,能夠將代碼簡化爲tu.getTime(() -> new Foo().algo());。lambda看起來很美,簡化了許多,然而這種寫法對於不熟悉的人寫起來仍是不太順手,並且Java 8如下的環境沒法這樣寫。ide

更重要的是從代碼的形式上看,algo() 仍是被包在表達式內,彷彿getTime()纔是主要邏輯同樣。因爲以前接觸過Python,此時不由想到,要是能像Python裏那樣用裝飾器來解決就簡潔又方便了:函數

@getTime
def algo1():
    # algo1

@getTime
def algo2():
    # algo2

不過Java中也沒有這樣的語法糖,只有註解,因而思考是否能夠利用反射和註解來「反轉」這種喧賓奪主的狀況並使代碼更具可讀性。工具

實現

先看實現以後的效果:性能

// 測試類Foo
public class Foo {

    @Timer
    public void algo1() {
        ArrayList<Integer> l = new ArrayList<>();
        for (int i = 0; i < 10000000; i++) {
            l.add(1);
        }
    }

    @Timer
    public void algo2() {
        LinkedList<Integer> l = new LinkedList<>();
        for (int i = 0; i < 10000000; i++) {
            l.add(1);
        }
    }

    public void algo3() {
        Vector<Integer> v = new Vector<>();
        for (int i = 0; i < 10000000; i++) {
            v.add(1);
        }
    }

    public static void main(String[] foo){
        TimerUtil tu = new TimerUtil();
        tu.getTime();
    }
}

運行輸出以下:學習

能夠看到此時加了@Timer註解的algo1()和algo2()的運行時間被統計了,而沒加@Timer的algo3()未被統計在內。

思路

使用反射獲取棧中當前類(測試類)的信息,遍歷其中的方法,若方法包含@Timer註解,則執行該方法並進行時間戳相減。

實現這樣的效果僅需一個自定義註解和一個工具類:

@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}
public class TimerUtil {

    public void getTime() {
        // 獲取當前類名
        String className = Thread.currentThread().getStackTrace()[2].getClassName();
        System.out.println("current className(expected): " + className);
        try {
            Class c = Class.forName(className);
            Object obj = c.newInstance();
            Method[] methods = c.getDeclaredMethods();
            for (Method m : methods) {
                // 判斷該方法是否包含Timer註解
                if (m.isAnnotationPresent(Timer.class)) {
                    m.setAccessible(true);
                    long start = System.currentTimeMillis();
                    // 執行該方法
                    m.invoke(obj);
                    long end = System.currentTimeMillis();
                    System.out.println(m.getName() + "() time consumed: " + String.valueOf(end - start) + "\\\\n");
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

升級

在同時統計多個方法時,要是能可視化的打印出相似Performance Index同樣的柱狀圖,能夠更直觀的比較他們之間的性能差別,就像這樣:

耗時最久的方法的Index固定爲100,剩餘的按相對的Index降序排列。

思路

修改TimerUtil,在以前的getTime()中返回一個HashMap,儲存方法名: 耗時的鍵值結構。而後降序排序HashMap返回一個LinkedHashMap。最後遍歷LinkedHashMap根據百分比求得各個方法的Index並輸出相關信息。

public class TimerUtil {

    // 修改getTime()
    public HashMap<String, Long> getMethodsTable() {
        HashMap<String, Long> methodsTable = new HashMap<>();
        String className = Thread.currentThread().getStackTrace()[3].getClassName();
        // ...
        return methodsTable;
    }

    public void printChart() {
        Map<String, Long> result = sortByValue(getMethodsTable());
        double max = result.values().iterator().next();
        for (Map.Entry<String, Long> e : result.entrySet()) {
            double index = e.getValue() / max * 100;
            for (int i = 0; i < index; i++) {
                System.out.print("=");
            }
            System.out.println(e.getKey() + "()" + " Index:" + (long) index + " Time:" + e.getValue());
        }
    }

    <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
        List<Map.Entry<K, V>> list = new LinkedList<>(map.entrySet());
        // desc order
        Collections.sort(list, new Comparator<Map.Entry<K, V>>() {
            public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) {
                return (o2.getValue()).compareTo(o1.getValue());
            }
        });
        Map<K, V> result = new LinkedHashMap<>();
        for (Map.Entry<K, V> entry : list) {
            result.put(entry.getKey(), entry.getValue());
        }
        return result;
    }
}

總結

本文介紹的是一個APM (Algorithm Performance Measurement) 工具比較粗糙簡陋的實現,然而這種思路能夠一樣應用在權限控制、日誌、緩存等方面,方便的對代碼進行解耦,讓通用的功能「切入」原先的代碼,使得開發時能夠更專一於業務邏輯。

相關文章
相關標籤/搜索