淺談服務埋點(1)——AOP

  年會圓滿結束了,我們的年會系統整體表現也還算不錯,但唯一遺憾的是到最後搖一搖的時候,系統卡住了,不過還好最後挺了過來。
  在以往編寫應用程序的時候,我們通常會記錄日誌,以便出了問題之後事後有跡可循,這是一種靜態分析。這次UIOC事件的發生,讓我意識到系統性能的監控,或者說某一時刻運行的情況,比如當前系統中對外提供了多少次服務,這些服務的響應時間是多少,隨時間變化的情況是什麼樣的,系統出錯的頻率是多少,這些動態的實時信息對於監控整個系統的運行健康狀況來說多麼的重要。
  我們發現我們要做的這些監控實際和我們的業務是毫無關係的,說到這裏可能瞭解Spring的朋友們就會想到了AOP。
  所以這一篇我先談一談基礎知識–AOP

—————————————請叫我大分割線———————————–

一.爲什麼要用AOP

  面向對象的特點是繼承、多態和封裝。而封裝就要求將功能分散到不同的對象中去,這在軟件設計中往往稱爲職責分配。實際上也就是說,讓不同的類設計不同的方法。這樣代碼就分散到一個個的類中去了。這樣做的好處是降低了代碼的複雜程度,使類可重用。
  但是人們也發現,在分散代碼的同時,也增加了代碼的重複性。什麼意思呢?比如說,我們在兩個類中,可能都需要在每個方法中做日誌。按面向對象的設計方法,我們就必須在兩個類的方法中都加入日誌的內容。也許他們是完全相同的,但就是因爲面向對象的設計讓類與類之間無法聯繫,而不能將這些重複的代碼統一起來。
  也許有人會說,那好辦啊,我們可以將這段代碼寫在一個獨立的類獨立的方法裏,然後再在這兩個類中調用。但是,這樣一來,這兩個類跟我們上面提到的獨立的類就有耦合了,它的改變會影響這兩個類。那麼,有沒有什麼辦法,能讓我們在需要的時候,隨意地加入代碼呢?

二.什麼是AOP

  這種在運行時,動態地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程。

  一般而言,我們管切入到指定類指定方法的代碼片段稱爲切面,而切入到哪些類、哪些方法則叫切入點。有了AOP,我們就可以把幾個類共有的代碼,抽取到一個切片中,等到需要時再切入對象中去,從而改變其原有的行爲。
  這樣看來,AOP其實只是OOP的補充而已。OOP從橫向上區分出一個個的類來,而AOP則從縱向上向對象中加入特定的代碼,有了AOP,OOP變得立體了。
  實現AOP的技術,主要分爲兩大類:一是採用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行爲的執行;二是採用靜態織入的方式,引入特定的語法創建“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的代碼。

  接下來是一張在知乎上比較傳神的圖片。
  這裏寫圖片描述
  面向切面,面向方面,也叫刀削麪。

這裏白麪條好比系統的主流業務代碼,各種調料和滷汁則好比那些處理瑣碎事務的代碼。這種將業務代碼和瑣碎事務代碼分離的機制,能夠讓你在製作的時候只需要考慮自己這一部分的好壞,而不需要考慮其他。而且我需要加一個其他的瑣碎的功能就非常方便(加一個配菜即可,不需要重新做一碗)。

  
—————————————請叫我分割線———————————–

三、AOP應用實戰

  前面說了一堆的理論,接下來我們看看如何應用。

1、寫死代碼

先一接口

public interface Greeting {
    void sayHello(String name);
}

實現類

public class GreetingImpl implements Greeting {

    @Override
    public void sayHello(String name) {
        before();
        System.out.println("Hello! " + name);
        after();
    }

    private void before() {
        System.out.println("Before");
    }

    private void after() {
        System.out.println("After");
    }
}

這不必多說,這樣的代碼無疑滿足了所有low代碼的特點,程序員被累死,架構師被氣死。
接下來提出三個重構的解決方案:

2、靜態代理

最簡單的方法是採用設置模式中的代理模式:

public class GreetingProxy implements Greeting {

    private GreetingImpl greetingImpl;

    public GreetingProxy(GreetingImpl greetingImpl) {
        this.greetingImpl = greetingImpl;
    }

    @Override
    public void sayHello(String name) {
        before();
        greetingImpl.sayHello(name);
        after();
    }

    private void before() {
        System.out.println("Before");
    }

    private void after() {
        System.out.println("After");
    }
}

用GreetingProxy這個代理類去代理GreetingImpl實現類。接着,客戶端調用:

public class Client {

    public static void main(String[] args) {
        Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
        greetingProxy.sayHello("Jack");
    }
}

這樣做的問題也很明顯:XXXProxy這樣的代理類會越來越多。
這時需要用到JDK提供的動態代理了:

3、JDK動態代理

public class JDKDynamicProxy implements InvocationHandler {

    private Object target;

    public JDKDynamicProxy(Object target) {
        this.target = target;
    }

    @SuppressWarnings("unchecked")
    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            this
        );
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);
        after();
        return result;
    }

    private void before() {
        System.out.println("Before");
    }

    private void after() {
        System.out.println("After");
    }
}
public class Client {

    public static void main(String[] args) {
        Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy();
        greeting.sayHello("Jack");
    }
}

JDKDynamicProxy實現了InvocationHandler接口,那麼必須實現該接口的invoke方法,該方法直接通過反射去invoke method,在調用前後分別處理before和after,最後返回result。
它的好處在於接口變了,這個動態的代理類不用動;而靜態代理則是接口變了,實現類也要動代理類也要動。但jdk動態代理的缺陷是:它只能代理接口,而不能沒有代理沒有接口的類。

4、CGlib動態代理

我們使用開源的 CGLib 類庫可以代理沒有接口的類,這樣就彌補了 JDK 的不足。CGLib 動態代理類是這樣玩的:

public class CGLibDynamicProxy implements MethodInterceptor {

    private static CGLibDynamicProxy instance = new CGLibDynamicProxy();

    private CGLibDynamicProxy() {
    }

    public static CGLibDynamicProxy getInstance() {
        return instance;
    }

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> cls) {
        return (T) Enhancer.create(cls, this);
    }

    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        before();
        Object result = proxy.invokeSuper(target, args);
        after();
        return result;
    }

    private void before() {
        System.out.println("Before");
    }

    private void after() {
        System.out.println("After");
    }
}

以上代碼中了 Singleton 模式,那麼客戶端調用也更加輕鬆了:

public class Client {

    public static void main(String[] args) {
        Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
        greeting.sayHello("Jack");
    }
}

與JDKDynamicProxy類似,在CGLibDynamicProxy 中也添加一個泛型的getProxy方法,便於我們可以快速地獲取自動生成的代理對象。

以上是基於代理的經典AOP,當引入了簡單的聲明式AOP和基於註解的AOP後,這種代理方式看起來非常笨重和複雜,感覺現在應該很少用到了,只不過我都看了,就寫上來了。


5、聲明式SpringAop

5.1、配置切入點Pointcut

Spring所有的切面和通知其都必須放在一個<aop:config> 內(可以配置包含多個<aop:config>元素),每一個<aop:config>可以包含pointcut,advisor和aspect元素。
(它們必須按照順序聲明)

切點用於定位應該在什麼地方應用切面的通知,切點和通知是切面最基本的元素。
在SpringAOP中,需要使用AspectJ的切點表達式語言來定義切點。

這裏寫圖片描述

最爲常用的是excution()

這裏寫圖片描述

我們使用execution()指示器選擇Instrument的play()方法。*和(..)表示了我們不關心返回值和參數列表。
現在假設我們需要配置切點僅匹配com.springinaction.springidol包,在此場景下,可以使用within()指示器來限制匹配。

這裏寫圖片描述

同理可以使用 || 和!操作符。

如何聲明:

<aop:config>
    <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service..(..))"/>
</aop:config>

5.2、配置切面asspect

  • <aop:aspect>
<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        <aop:pointcut id="businessService"
             expression="execution(* com.xyz.myapp.service..(..))"/>
         ...
    </aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>

通知advice 分爲以下幾種:
這裏寫圖片描述

前置、後置、環繞

首先有個觀衆類(Audience),有入座、關機、鼓掌、要求退款 等功能。
這裏寫圖片描述

把他註冊爲Spring應用上下文的一個Bean。接下來利用AOP配置,將其變成一個切面。

這裏寫圖片描述
這裏寫圖片描述

這樣的配置就能在表演者Perform的前、後、以及異常的時候調用觀衆的方法,而這些事件表演者毫不知情(做到真正意義的解耦)。
由於我們的這些切點都是一樣的,所以可以做如下提取。

這裏寫圖片描述

環繞通知

如果希望觀衆一直關注演出,並彙報每個表演的時長,那麼前置/後置通知的方法是:前置通知保存開始時間,後置通知通過減法彙報時長。這樣做不僅麻煩、而且容易引發線程安全問題。
而環繞通知可以很好的解決,它只需要在一個方法中實現,所以不需要使用成員變量保存狀態。

這裏寫圖片描述

從代碼中我們可以看出,環繞通知有一個特點:它要求通知方法的第一個參數必須是ProceedingJoinPoint類型,然後joinpoint.proceed()是執行被通知的方法。
在這個切面中,watchPerformance()方法包含之前的4個通知方法的所有邏輯,但所有的邏輯都放在一個方法中了,而且該方法還負責自身的異常處理。接下來要做的就是配置<aop:around>了。

這裏寫圖片描述

通知傳參
有時候通知並不僅僅是對方法進行簡單包裝,還需要校驗傳遞給方法的參數值,這時候就需要爲通知傳遞參數。
現在有個實現MindReader接口的魔術表演者Magican,他想表演監聽觀衆的內心感應和顯示他們在想什麼。

實現了Thinker的志願者Volunteer參與了這次表演。

這裏寫圖片描述

接下來便是Magican利用Spring AOP技術來實現監聽Volunteer內心感應的表演了。

這裏寫圖片描述

這裏切點標識了thinkOfSomething()方法,指定了thoughts參數。before標籤的arg-names元素意味着該參數必須傳遞給Magician的interceptThoughts()方法。


由於篇幅原因,還有一些利用AOP引入新方法(introduction)、註解AOP、AspectJ等內容以後再總結吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章