AOP實踐: Java利用注解和反射實現一個方便的函數運行時間統計工具

BelleEdgar 8年前發布 | 13K 次閱讀 AOP Java開發

最初目的

在學習Java的集合類時,有時候想要測試代碼塊的運行時間,以比較不同算法數據結構之間的性能差異。最簡單的做法是在代碼塊的前后記錄時間戳,最后相減得到該代碼塊的運行時間。

下面是Java中的示例:

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

}</code></pre>

初探

顯然上面的代碼看起來非常冗余,由于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();
        }
    });
}

}</code></pre>

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

Java 8為了解決類似的問題,引入了lambda,可以將代碼簡化為 tu.getTime(() -> new Foo().algo()); 。lambda看起來很美,簡化了許多,然而這種寫法對于不熟悉的人寫起來還是不太順手,而且Java 8以下的環境無法這樣寫。

更重要的是從代碼的形式上看, algo() 還是被包在表達式內,仿佛 getTime() 才是主要邏輯一樣。由于之前接觸過Python,此時不禁想到,要是能像Python里那樣用裝飾器來解決就簡潔又方便了:

@getTime
def algo1():

# algo1

@getTime def algo2():

# algo2</code></pre> 

不過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;
    }
}

總結

本文介紹的是一個計時工具比較粗糙的實現,然而這種思路可以同樣應用在權限控制、日志、緩存等方面,方便的對代碼進行解耦,讓通用的功能“切入”原先的代碼,使得開發時可以更專注于業務邏輯。

 

來自:https://segmentfault.com/a/1190000007217125

 

 本文由用戶 BelleEdgar 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!