1. 程式人生 > >架構探險筆記4-使框架具備AOP特性(上)

架構探險筆記4-使框架具備AOP特性(上)

對方法進行效能監控,在方法呼叫時統計出方法執行時間。

原始做法:在內個方法的開頭獲取系統時間,然後在方法的結尾獲取時間,最後把前後臺兩次分別獲取的系統時間做一個減法,即可獲取方法執行所消耗的總時間。

專案中大量的方法,如果對每個方法開頭結尾都加上這些程式碼,工作量會很大。現在不用修改現有程式碼,在另一個地方做效能監控,AOP(Aspect Oriented Programming,面向方面程式設計)就是我們尋找的解決方案。

在AOP中,我們需要定義一個Aspect(切面)類來編寫需要橫切業務邏輯的程式碼,也就是效能監控程式碼。此外,我們需要通過一個條件來匹配想要攔截的類,這個條件在AOP中稱為Pointcut(切點)。

案例思路,統計出執行每個Controller類的各個方法所消耗的時間。每個Controller類都有Controller註解,也就是說,我們只需要攔截所有帶有Controller註解的類就行了,切點很容易就能確定下來,剩下的就是做一個切面了。

代理技術

代理,或稱為Proxy,意思就是你不用去做,別人替你去處理。比如說:賺錢方面,我就是我老婆的Proxy;帶小孩方面,我老婆就是我的Proxy;家務事方面,沒有Proxy。

它在程式中開發起到了非常重要的作用,比如AOP,就是針對代理的一種應用。此外,在設計模式中,還有一個“代理模式”,在公司要上網,要在瀏覽器中設定一個Http代理。

Hello World例子

//介面
public interface Hello{
  void say(String name);  
}

//實現類
public class HelloImpl implements Hello{
  @Override
  public void say(String name){
     System.out.println("Hello!"+name);      
  }    
}

如果要在println方法前面和後面分別需要處理一些邏輯,怎麼做呢?把這些邏輯寫死在say方法裡面嗎?這麼做肯定不夠優雅,“菜鳥”一般這樣幹,作為一名資深的程式設計師,我們堅決不能這麼做!

我們要用代理模式,寫一個HelloProxy類,讓它去呼叫HelloImpl的say方法,在呼叫的前後分別進行邏輯處理。

public class HelloProxy implements Hello{
  private Hello hello;

  private HelloProxy(){
    hello=new HelloImpl();
  }  
    
   private void say(String name){
       before();
       hello.say(name);
       after();
   }

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

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

用HelloProxy類實現了Hello介面(和HelloImpl實現相同的介面),並且在構造方法中new出一個HelloImpl類的例項。這樣一來,我們就可以在HelloProxy的say方法裡面去呼叫HelloImpl的say方法了。更重要的是,我們還可以在呼叫的前後分別加上before和after兩個方法,在這兩個方法裡去實現那些前後邏輯。

main方法測試

public static void main(String[] args){
    Hello helloProxy = new HelloProxy();
    helloProxy.say("Jack");
}


//列印結果
Before
Hello! Jack
After

JDK動態代理

於是瘋狂使用代理模式,專案中到處都是XXXProxy的身影,直到有一天,架構師看到了我的程式碼,他驚呆了,對我說“你怎麼這麼喜歡靜態代理呢?你就不會用動態代理嗎?全部重構!”

研究了一下,原來一直用的是靜態代理(上面的例子),到處都是XXXProxy類。一定要將這些垃圾Proxy都重構為“動態代理”。

/**
 * 動態代理
 */
public class DynamicProxy implements InvocationHandler {
    private Object target;
    public DynamicProxy(Object target) {
       this.target = target;
    }
    /* 
     * 用時代理
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        before();
        Object result method.invoke(target, args);
        after();
return result;
    }

}

    //執行
    public static void main(String[] args) {
        Hello hello = new HelloImpl();//用時代理
        DynamicProxy dynamicProxy = new DynamicProxy(hello);
Hello helloProxy
= (Hello)Proxy.newProxyInstance(
      hello.getClass().getClassLoader(),
      hello.getClass().getInterfaces(),
      dynamicProxy
     );
//執行run方法 helloProxy.say("Jack"); }

在這個例子中,DynamicProxy定義了一個Object型別的Object變數,它就是被代理的目標物件,通過建構函式來初始化(“注入”,構造方法初始化叫“正著射”,所以反射初始化叫“反著射”,簡稱“反射”)。

通過DynamicProxy類去包裝Car例項,然後再呼叫JDK給我們的提供的Proxy類的工廠方法newProxyInstance去動態的建立一個Hello介面的代理類,最後呼叫這個代理類的run方法。

Proxy.newProxyInstance方法的引數

引數1:ClassLoader

引數2:該實現類的所有介面

引數3:動態代理物件

呼叫完了用強制型別轉換下

這一塊想辦法封裝一下,避免再次出現到處都是Proxy.newProxyInstance方法的情況。於是將這個DynamicProxy重構一下:

public class DynamicProxy implements InvocationHandler{    
private
Object target; public DynamicProxy(Object target) { this.target = target; }
   @SupressWarnings("unchecked")
public <T> T getProxyInstance() { 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; } //請求前的操作 public void before(){ //預處理 System.out.println(target.getClass()+"被動態代理了,在它執行之前要執行動態代理加入的預處理方法"); } //請求後的操作 public void after(){ //善後處理 System.out.println(target.getClass()+"被動態代理了,在它執行之後要執行動態代理加入的善後方法"); } } public class DynamicProxyDemo { public static void main(String[] args) {
    DynamicProxy dynamicProxy = new DynamicProxy(new HelloImpl());
    Hello helloProxy = dynamicProxy.getProxy();
    helloProxy.say("Jack");
} }

在DynamicProxy裡添加了一個getProxy方法,無需傳入任何引數,將剛才所說的那塊程式碼放在這個方法中,並且該方法返回一個泛型型別,就不會強制轉換型別了。方法頭上@SupressWarnings(“unchecked”)註解表示忽略編譯時的警告(因為Proxy.newProxyInstance方法返回的是一個Object,這裡強制轉換為T了,這是向下轉型,IDE中就會有警告,編譯時也會出現提示)。

呼叫時就簡單了,用2行代理去掉了前面的7行程式碼(省了5行)。

CGlib動態代理

用了DynamicProxy以後,好處是介面變了,這個動態代理類不用動。而靜態代理就不一樣了,介面變了,實現類還要動,代理類也要動。但是動態代理並不是萬能的,它也有搞不定的時候,比如要代理一個沒有任何介面的類,它就沒有用武之地了。

CGlib是一個能代理沒有介面的類,雖然看起來不起眼,但Spring、Hibernate這樣高階的開源框架都用到了它,它是一個在執行期間動態生成位元組碼的工具,也就是動態生成代理類了。

public class CGLibProxy implements MethodInterceptor{
    
    public <T> T getProxy(Class<T> cls){
        return (T) Enhancer.create(cls,this);
    }

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

    ......
}

需要實現CGLib給我們提供的MethodInterceptor實現類,並填充intercept方法。方法中最後一個MethodProxy型別的引數proxy值得注意。CGLib給我們提供的是方法級別的代理,也可以理解為堆方法攔截(也就是“方法攔截器”)。這個功能對於我們程式設計師來說,如同雪中送炭。我們直接呼叫proxy的invokeSuper方法,將被代理的物件obj以及方法引數args傳入其中即可。

與DynamicProxy類似,在CGLibProxy中也添加了一個泛型的getProxy方法,便於我們可以快速地獲取自動生成的代理物件。

public static void main(){
    CGLibProxy cgLibProxy = new CGLibProxy();
    Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class);
    helloProxy.say(Jack);
}

仍然通過2行程式碼就可以返回代理物件,與JDK動態代理不同的是,這裡不需要任何的介面資訊,對誰都可以生成動態代理物件

用2行程式碼返回代理物件還是有些多餘的,不想總是去new這個CGLibProxy物件,最好new一次,以後隨時拿隨時用,於是想到了“單例模式”:

public class CGLibProxy implements MethodInterceptor{
    private static CGLibProxy instance = new CGLibProxy();

    private CGLibProxy(){
        
    }
    private static CGLibProxy getInstance(){
        return instance;
    }
...
getProxy...
intercept... }

加上以上幾行程式碼問題就解決了。需要說明的是,這裡有一個private的構造方法,就是為了限制外界不能再去new它了,換句話說,這個類被閹割了。

public static void main(String[] args){
  Hello helloProxy = CGLibProxy.getInstance().getProxy(HelloImpl.class);
  helloProxy.say("Jack");    
}

這裡只需要一行程式碼就可以獲取代理物件了.

AOP技術

什麼是AOP

AOP(Aspect-Oriented Programming),名字與OOP僅僅差一個字母,其實它是對OOP程式設計方式的一種補充,並非是取而代之。翻譯過來就是“面向切面程式設計”或“面向方面程式設計”。最重要的工作就是寫這個“切面”,那麼什麼事“切面”呢?

切面是AOP中的一個術語,表示從業務邏輯中分離出來的橫切邏輯,比如效能監控、日誌記錄、許可權控制等,這些功能都可以從業務邏輯程式碼中抽離出去。也就是說,通過AOP可以解決程式碼耦合問題,讓職責更加單一。

需要澄清的是,其實很早以前就出現了AOP這個概念。最知名強大的Java開源專案就是AspectJ了。它的前身是AspectWerkz(AOP真正的的老祖宗)。Rod Johnson寫了一個Spring框架,稱為Spring之父。他在Spring的IOC框架基礎上又實現了一套AOP框架,後來掉進了深淵,在無法自拔的時候被迫使用了AspectJ。所以我們現在用的最多的就是Spring+AspectJ這種AOP框架了。

寫死程式碼

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");
    }
}

before與after方法寫死在sayHello方法體中了,這樣的程式碼非常不好。如果我們要統計每一個方法的執行時間,以對效能進行評估,那是不是每個方法的一頭一尾都做點手腳呢?

再比如我們要寫一個JDBC程式,那是不是也要在方法的開頭去連線資料庫,方法的末尾去關閉資料庫連線呢?

這樣寫程式碼只會把程式設計師累死,把架構師氣死!

一定要想辦法對上面的程式碼進行重構,首先給出三個解決方案:

靜態代理

JDK動態代理

CGLib動態代理

靜態代理

最簡單的解決方案就是使用靜態代理模式了,我們單獨為GreetingImpl這個類寫一個代理類:

public class GreetingProxy implements Greeting {
    private GreetingImpl greetingImpl;

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

    @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");
    }
}

就用這個GreetingProxy去代理GreetingImpl,看看客戶端如何來呼叫:

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

這個寫的沒錯,但是有個問題。XxxProxy這樣的類會越來越多(這裡建構函式引數為GreetingImpl,所以換一個子類就要再次寫一個代理類,如果建構函式引數改為介面Greet,這樣再使用Greet的子類時可以使用這個類,當換一個介面時又要去寫一個代理類),如何才能將這些代理類儘可能減少呢?最好只有一個代理類。

這時我們需要使用JDK的動態代理了。

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");
    }
}

這樣所有的代理類都合併到動態代理類中了,但這樣做仍然存在一個問題:JDK給我們提供的動態代理只能代理介面,而不能代理沒有介面的類

public class Client {
    public static void main(String[] args) {
        //靜態代理
        /*Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
        greetingProxy.sayHello("Jack");*/
        //JDK動態代理
        Greeting greeting = new JDKDynamicProxy(new GreetingImpl())
                .getProxy();
        greeting.sayHello("Jack");
    }
}

CGLib動態代理

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

public class CGLibDynamicProxy implements MethodInterceptor{
    //單例模式
    private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
    //私有化建構函式,防止new
    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 methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(target,args);
        after();
        return result;
    }

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

注意這裡的座標

        <!--不能超過3.0版本,這裡用2.2-->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2</version>
        </dependency>
        <!--CGLib依賴此包-->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>6.2</version>
        </dependency>

到此為止,能做的都做了,問題似乎全部都解決了。但事情總不會那麼完美,而我們一定要追求完美。

Spring AOP

Rod Johnson搞出了一個AOP框架,Spring AOP:前置增強、後置增強、環繞增強(程式設計式)

上面例子中提到的before方法,在Spring AOP裡就叫Before Advice(前置增強)。有些人將Advice直譯為“通知”,這裡是不太合適的,因為它沒有“通知”的含義,而是對原有程式碼功能的一種“增強”。再者,CGLib中也有一個Enhancer類,它就是一個增強類。

此外,像after這樣的方法就叫After Advice(後置增強),因為它放在後面來增強程式碼的功能。

如果能把before與after結合在一起,那就叫Around Advice(環繞增強),就像漢堡一樣。

前置增強類程式碼(這個類實現了org.spring.framework.aop.MethodBeforeAdvice):

import org.springframework.aop.MethodBeforeAdvice;

public class GreetingBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("Before");
    }
}

後置增強類:

import org.springframework.aop.AfterReturningAdvice;

public class GreetingAfterAdvice implements AfterReturningAdvice {

    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("After");
    }
}

類似的這裡實現了org.springframework.aop.afterReturningAdvice介面。

呼叫

public class Client {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();  //建立代理工廠
        proxyFactory.setTarget(new GreetingImpl());    //攝入目標類物件
        proxyFactory.addAdvice(new GreetingBeforeAdvice());   //新增前置增強
        proxyFactory.addAdvice(new GreetingAfterAdvice());   //新增後置增強
        Greeting greeting = (Greeting) proxyFactory.getProxy();
        greeting.sayHello("Jack");

    }
}

當然我們完全可以用一個增強類,讓它同時實現MethodBeforeAdvice和AfterReturningAdvice這兩個介面,程式碼:

public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice,AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("before");
    }

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("after");
    }
}

這樣我們只需要使用一行程式碼,就可以同時新增前置與後置增強

proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());

剛才有提到過“環繞增強”,其實它可以把“前置增強”與“後置增強”的功能合併起來,無須讓我們同時實現兩個介面。

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.reflect.Method;


public class GreetingAroundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        before();
        Object result = methodInvocation.proceed();
        after();
        return result;
    }

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

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

環繞增強需要實現org.aopalliance.intercept.MethodInterceptor介面。注意,這個介面不是Spring提供的,它是AOP聯盟(一個很高大上的技術聯盟)寫的,Spring只是借用了它,在客戶端彙總同樣也需要將該增強類的物件新增到代理工廠中。

public class Client {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();  //建立代理工廠
        proxyFactory.setTarget(new GreetingImpl());    //攝入目標類物件
        //proxyFactory.addAdvice(new GreetingBeforeAdvice());   //新增前置增強
        //proxyFactory.addAdvice(new GreetingAfterAdvice());   //新增後置增強
        //proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());   //實現兩個介面
        proxyFactory.addAdvice(new GreetingAroundAdvice());  //實現一個Around環繞式介面
        Greeting greeting = (Greeting) proxyFactory.getProxy();
        greeting.sayHello("Jack");
    }
}

以上就是SpringAOP的基本用法,單這只是“程式設計式”而已。Spring AOP如果只是這樣,那就太弱了,它曾經也一度宣傳用Spring配置檔案的方式來定義Bean物件,把程式碼中的new操作全部解脫出來。

SpringAOP:前置增強、後置增強、環繞增強(宣告式)

Spring配置檔案篇日誌

    <!--掃描指定包(將帶有Component註解的類自動定義為SpringBean)-->
    <context:component-scan base-package="com.smart4j.framework"/>

    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interfaces" value="com.smart4j.framework.Greeting"/>   <!--需要代理的介面-->
        <property name="target" ref="greetingImpl"/>   <!--實現介面類-->
        <property name="interceptorNames">   <!--攔截器名稱(也就是增強類名稱,SpringBean的Id)-->
            <list>
                <value>greetingAroundAdvice</value>
            </list>
        </property>
    </bean>

使用ProxyFactoryFactoryBean就可以取代前面的ProxyFactory,其實他們是一回事。interceptorNames改名為adviceNames或許更容易讓人理解。就是網這個屬性裡新增增強類。

此外,如果只有一個增強類,可以使用下面這個方法來簡化

    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interfaces" value="com.smart4j.framework.Greeting"/>   <!--需要代理的介面  name也可以為proxyInterfaces-->
        <property name="target" ref="greetingImpl"/>   <!--實現介面類   name也可以為targetName-->
        <property name="interceptorNames" value="greetingAroundAdvice">   <!--攔截器名稱(也就是增強類名稱,SpringBean的Id)-->
        </property>
    </bean>

需要注意的是,這裡使用了Spring2.5+的"Bean掃描"特性,這樣我們就無需再Spring配置問加你了不斷的定義<bean id="xxx" class="xxx"/>了,從而解脫了我們的雙手。

去掉原本的

    <bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"></bean>

    <bean id="greetingAroundAdvice" class="com.smart4j.framework.aop.GreetingAroundAdvice"></bean>

改為使用@Compoent

@Component
public class GreetingImpl implements Greeting{
。。。
}

@Component
public class GreetingAroundAdvice implements MethodInterceptor {
。。。
}

程式碼量確實少了,我們將配置性的程式碼放入配置檔案,這樣也有助於後期維護。更重要的是,程式碼值關注於業務邏輯,而將配置放入檔案中,這是一條最佳實踐!

除了上面提到的那三個增強意外,其實還有兩個增強也需要了解一下,關鍵的時候要能想到它們才行。

Spring AOP:丟擲增強

程式報錯,丟擲異常了,一般的做法是列印控制檯到日誌檔案中,這樣很多地方都得去處理,有沒有一個一勞永逸的方法呢?那就是Throws Advice(丟擲增強)

@Component
public class GreetingThrowAdvice implements ThrowsAdvice {

    public void afterThrowing(Method method, Object[] args, Object target,Exception e){
        System.out.println("-------------Throw Exception-----------------");
        System.out.println("Target class: "+target.getClass().getName());
        System.out.println("Method Name: "+method.getName());
        System.out.println("Exception Message: "+e.getMessage());
        System.out.println("---------------------------------------------");
    }
}

配置spring.xml

    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="proxyInterfaces" value="com.smart4j.framework.Greeting"/>   <!--需要代理的介面-->
        <property name="targetName" value="greetingImpl"/>   <!--實現介面類-->
        <property name="interceptorNames">   <!--攔截器名稱(也就是增強類名稱,SpringBean的Id)-->
            <list>
               <!-- <value>greetingAroundAdvice</value>-->
                <value>greetingThrowAdvice</value>
            </list>
        </property>
    </bean>

結果

 丟擲增強需要實現org.springframework.aop.ThrowsAdvice介面,在介面方法中可獲取方法、引數、目標物件、異常物件等資訊。我們可以把這些資訊統一寫入到日誌中,當然也可以持久化到資料庫中。

Spring AOP:引入增強

以上提到的都是對方法的增強,那能否對類進行增強呢?用AOP的行話來講,對方法的增強叫Weaving(織入),而對類的增強叫Introduction(引入),Introduction Advice(引入增強)就是對類的功能增強,它也是Spring AOP提供的最後一種增強。

定義介面

public interface Apology {
    void saySorry(String name);
}

但是我們不想在程式碼中讓GreetingImpl直接去實現這個介面,而想在程式執行的時候動態地實現它。因為加入實現了這個介面,那麼久一定要改寫GreetingImpl這個類,關鍵是我們不想改它,或許在真實場景中,這個類有一萬行程式碼。於是,我們需要藉助Spring的引入增強。

@Component
public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology{

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        return super.invoke(mi);
    }

    @Override
    public void saySorry(String name) {
        System.out.println("Sorry "+name);
    }
}

以上一個引入增強類,擴充套件了org.springframework.aop.support.DelegatingIntroductionInterceptor類,同時也實現了新定義的Apology介面。在類中首先覆蓋了父類的invoke()方法,然後實現了Apology介面的方法。我們相擁這個增強類去豐富GreetingImpl類的功能,那麼這個GreetingImpl類無須直接實現Apology介面,就可以直接在程式執行的時候呼叫Apology介面的方法了。

配置Spring.xml

    <!--引入增強-->
    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="proxyInterfaces" value="com.smart4j.framework.aop.Apology"/>   <!--需要動態實現的介面-->
        <property name="targetName" value="greetingImpl"/>   <!--目標類-->
        <property name="interceptorNames" value="greetingIntroAdvice"/>   <!--攔截器名稱(也就是增強類名稱,SpringBean的Id)-->
        <property name="proxyTargetClass" value="true"/>   <!--代理目標類,(預設為false,代理介面)-->
    </bean>

需要注意proxyTargetClass屬性,它表明是否代理目標類,預設為false,也就是代理介面,此時Spring就用JDK動態代理;如果為TRUE,那麼Spring就用CGLib動態代理。

呼叫

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //獲取Spring Context
        Greeting greeting = (Greeting) context.getBean("greetingProxy");  //從Context中根據id獲取Bean物件(其實也就是一個代理)
        greeting.sayHello("jack");   //呼叫代理方法

        Apology apology = (Apology) greeting;   //將目標類增強向上轉型為Apology介面(這是引入增強給我們帶來的特性,也是"介面動態實現"功能)
        apology.saySorry("jack");

sarySorry方法原來是可以被greetingImpl物件來直接呼叫的,只需將其強制轉換為該介面即可。

SpringAOP:切面

之前談到的AOP框架其實可以將它理解為一個攔截器框架,但這個攔截器似乎非常武斷。比如說,如果它攔截了一個類,那麼它就攔截這個類中所有的方法。類似的,當我們在使用動態代理的時候,其實也遇到了這個問題。需要在程式碼中對所攔截的方法名加以判斷,才能過濾出我們需要攔截的方法,這種做法確實不太優雅。在大量的真實專案中,似乎我們只需要攔截特定的方法就行了,沒必要攔截所有的方法。於是,Spring藉助很重要的工具---Advisor(切面),來解決這個問題。它也是AOP中的核心,是我們關注的重點。

也就是說,我們可以通過切面,將增強類與攔截匹配條件組合在一起,然後將這個切面配置到ProxyFactory中,從而生成代理。

這裡提到這個“攔截匹配條件”在AOP中就叫作Pointcut(切點),其實說白了就是一個基於表示式的攔截條件。Advisor(切面)封裝了Advice(增強)與Pointcut(切點)。

@Component
public class GreetingImpl implements Greeting {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello! "+name);
    }

    /*切面新增方法*/
    public void goodMorning(String name){
        System.out.println("Good Morning!"+name);
    }

    public void goodNight(String name){
        System.out.println("Good Night!"+name);
    }
}

在Spring AOP中,最好用的是基於正則表示式的切面類。

配置

    <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="advice" ref="greetingAroundAdvice"/>  <!--增強-->
        <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/>    <!--切點(正則表示式)-->
    </bean>
    <!--配置一個代理類-->
    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="targetName" value="greetingImpl"/>   <!--目標類-->
        <property name="interceptorNames" value="greetingAdvisor"/>   <!--切面(替換之前的攔截器名稱)-->
        <property name="proxyTargetClass" value="true"/>   <!--代理目標類,(預設為false,代理介面)-->
    </bean>

呼叫

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //獲取Spring Context
        GreetingImpl greeting = (GreetingImpl) context.getBean("greetingProxy");  //從Context中根據id獲取Bean物件(其實也就是一個代理)
        greeting.sayHello("jack");   //呼叫未被代理方法
        greeting.goodMorning("Jhon");
        greeting.goodNight("Sawer");

結果

注意以上代理物件中的配置的interceptorNames,它不再是一個增強,而是一個切面,因為已經將增強封裝到該切面中了。此外,切面還定義了一個切點(正則表示式),其目的是為了只對滿足切點匹配條件的方法進行攔截。

這裡的切點表示式是基於正則表示式的。其中.*代表匹配所有字元,翻譯過來就是匹配GreetingImpl類中以good開頭的方法。

除了RegexpMethodPointcutAdvisor以外,在Spring AOP中還提供了幾個切面類,比如:

  • DefaultPointcutAdvisor - 預設切面(可擴充套件它來自定義切面)
  • NameMatchMethodPointcutAdvisor - 根據方法名稱進行匹配的切面
  • StaticMethodMatcherPointcutAdvisor - 用於匹配靜態方法的切面

總的來說,讓使用者去配置一個或少數幾個代理,似乎還可以接受,但隨著專案的擴大,代理配置就會越來越多,配置的重複勞動就多了,麻煩不說,還很容易出錯。。能否讓Spring框架為我們自動生成代理呢?

Spring AOP:自動代理(掃描Bean名稱)

Spring AOP提供了一個可以根據Bean名稱來自動生成代理的工具,它就是BeanNameAutoProxyCreator。配置如下:

    <!--自動代理-->
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="beanNames" value="*Impl"/>   <!--為字尾是Impl的Bean生成代理-->
        <property name="interceptorNames" value="greetingAroundAdvice"/>   <!--增強(這裡沒用切面)-->
        <property name="optimize" value="true"/>   <!--是否對代理生成策略進行優化-->
    </bean>

呼叫

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //獲取Spring Context
        GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl");  //從Context中根據id獲取Bean物件(自動掃描的id為首字母小寫的類名)
        greeting.sayHello("jack");

 

以上使用BeanNameAutoProxyCreator只為字尾為"Impl"的Bean生成代理。需要注意的是,這個地方我們不能定義代理介面,也及時interfaces屬性,因為我們根本就不知道這些Bean到底實現了多少介面。此時不能代理介面,而只能代理類。所以這裡提供了一個新的配置項,它就是optimize。若為true時,則可對代理生成策略進行優化(預設是false)。也就是說,如果該類有介面,就代理介面(JDK動態代理);如果沒有介面,就代理類(使用CGLib動態代理)。並非像之前使用的proxyTargetClass屬性那樣,強制代理類,而不考慮代理介面的方式。

既然CGLib可以代理任何類,那為什麼還要用JDK的動態代理呢?

根據實際專案經驗得知,CGLib建立代理的速度比較慢,但建立代理後執行的速度卻非常快,而JDK動態代理正好相反。如果在執行的時候不斷地用CGLib去建立代理,系統的效能會大打折扣,所以建議一般在系統初始化的時候用CGLib去建立代理,並放入Spring的ApplicationContext中以備後用。

這個例子只能匹配目標類,而不能進一步匹配其中指定的方法,要匹配方法,就要考慮使用切面與切點了。Spring AOP基於切面也提供了一個自動代理生成器:DefaultAdvisorAutoProxyCreator。

Spring AOP:自動代理(掃描切面配置)

為了匹配目標類中的指定方法,我們讓然需要在Spring中配置切面與切點:

    <!--自動代理 - 掃描切面配置-->
    <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/>
        <property name="advice" ref="greetingAroundAdvice"/>
    </bean>

    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
        <property name="optimize" value="true" />
    </bean>

這裡無須再配置代理,因為代理將由DefaultAdvisorAutoProxyCreator自動生成。也就是說,這個類可以掃描所有的切面類,併為其自動生成代理。

看來不論怎麼簡化,Rod始終解決不了切面的配置這件繁重的手工勞動。在Spring配置檔案中,仍然會存在大量的切面配置。然而在很多情況下,Spring AOP所提供的切面類真的不太夠用了,比如像攔截指定註解的方法,我們就必須擴充套件DefaultPointcutAdvisor類,自定義一個切面類,然後在Spring配置檔案中進行切面配置。Rod的解決方案似乎已經掉進了切面類的深淵,最重要的是切面,最麻煩的也是切面。所以要把切面配置給簡化掉。

Spring+AspectJ

神一樣的rod總算認識到了這一點,接受了網友們的建議,集成了AspectJ,同時也保留了以上提到的切面與代理配置方式(為了相容老專案,更為了維護自己的面子)。將Spring與AspectJ整合與直接使用AspectJ是不同的,我們不需要定義AspectJ類(它擴充套件了Java語法的一種新的語言,還需要特定的編譯器),只需要使用AspectJ切點表示式即可(它是比正則表示式更加友好的表現形式)。

1.Spring+AspectJ(基於註解:通過AspectJ execution表示式攔截方法)

定義一個Aspect切面類

@Aspect    /*切面*/
@Component
public class GreetingAspect {

    @Around("execution(* com.smart4j.framework.GreetingImpl.*(..))")   /*切點*/
    public Object around(ProceedingJoinPoint pjp) throws Throwable {    /*增強*/
        before();
        Object result = pjp.proceed();
        after();
        return result;
    }

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

注意:類上面標註的Aspect註解表明該類是一個Aspect(其實就是Advisor)。該類無須實現任何的介面,只需定義一個方法(方法叫什麼名字都無所謂),在方法上標註Around註解,在註解中使用AspectJ切點表示式。方法的引數中包括一個ProceedingJoinPoint物件,它在AOP中稱為Joinpoint(連線點),可以通過該物件獲取方法的任何資訊,例如,方法名、引數等。

解析下切點表示式execution(* com.smart4j.framework.GreetingImpl.*(..))

  • execution表示攔截方法,括號中可定義需要匹配的規則。
  • 第一個"*"表示方法的返回值是任意的;
  • 第二個"*"表示匹配該類中的所有方法;
  • (..)表示方法的引數是任意的。

是不是比正則表示式可讀性更強呢?如果想匹配指定的方法,只需將第二個“*”改為指定的方法名即可。

配置如下

    <!--掃描指定包(將帶有Component註解的類自動定義為SpringBean)-->
    <context:component-scan base-package="com.smart4j.framework"/>
    <aop:aspectj-autoproxy proxy-target-class="true"/>

兩行配置就行了,不需要配置大量的代理,更不需要配置大量的切面!proxy-target-class屬性,它的值預設是false,預設只能代理介面(使用JDK動態代理),當為true時,才能代理目標類(使用CGLib動態代理)。

Spring與AspectJ結合功能遠遠不止這些,我們還可以攔截指定註解的方法。

2.Spring+AspectJ(基於註解:通過AspectJ @annotation表示式攔截方法)

註解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tag {
}

以上定義一個Tag註解,此註解可標註在方法上,在執行時生效。

@Aspect    /*切面*/
@Component
public class GreetingAspect {

    @Around("@annotation(com.smart4j.framework.aspectj.Tag)")   /*切點 - 有Tag標記的Method*/
    public Object around(ProceedingJoinPoint pjp) throws Throwable {    /*增強*/
        before();
        Object result = pjp.proceed();
        after();
        return result;
    }

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

直接將Tag註解定義在想要攔截的方法上

@Component
public class GreetingImpl implements Greeting {
    @Tag   /*AspectJ 註解*/
    @Override
    public void sayHello(String name) {
        System.out.println("Hello! "+name);
    }
}

在以上例項中只有一個方法,如果有多個方法,我們只想攔截其中的某一些時,這種解決方案會更加有價值

除了Around註解外,其實還有幾個相關的註解,稍微歸納一下:

  • Before - 前置增強
  • After - 後置增強
  • Around - 環繞增強
  • AfterThrowing - 丟擲增強
  • DeclareParents - 引入增強

此外還有一個AfterReturning(返回後增強),也可理解為Finally增強,相當於finally語句,它是在方法結束後執行的,也就是說,它比After晚一些。

3.Spring+AspectJ(引入增強)

為了實現基於AspectJ的引入增強,我們同樣需要定義一個Aspect類:

@Aspect    /*切面*/
@Component
public class GreetingAspect {
    /*引入增強*/
    @DeclareParents(value = "com.smart4j.framework.GreetingImpl",defaultImpl = ApologyImpl.class)
    private Apology apology;
}

在Aspect類中定義一個需要引入增強的介面,它也就是執行時需要動態實現的介面。在這個介面上標註了DeclareParents註解,該註解有兩個屬性:

  • Value - 目標類;
  • defaultImpl - 引入介面的預設實現類。

我們只需要對引入的介面提供一個預設實現類即可完成增強:

public class ApologyImpl implements Apology {
    @Override
    public void saySorry(String name) {
        System.out.println("Sorry! " + name);
    }
}

執行

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //獲取Spring Context
        GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl");  //從Context中根據id獲取Bean物件(自動掃描的id為首字母小寫的類名)
        greeting.sayHello("jack");

        Apology apology = (Apology) greeting;   //將目標類增強向上轉型為Apology介面(這是引入增強給我們帶來的特性,也是"介面動態實現"功能)
        apology.saySorry("jack");

從SpringApplicationContext中獲取greetingImpl物件(其實是個代理物件),可轉型為自己靜態實現的介面Greeting,也可轉型為自己動態實現的介面Apology,切換起來非常方便。

使用AspectJ的引入增強比原來的SpringAOP的引入增強更加方便了,而且還可面向介面程式設計(以前只能面向實現類)。

這一切已經非常強大並且非常靈活了,但仍然還是由使用者不能嘗試這些特性,因為他們還在使用JDK1.4(根本就沒有註解這個東西),怎麼辦呢?SpringAOP為那些遺留系統也考慮到了。

3.Spring+AspectJ(基於配置)

除了使用Aspect註解來定義切面之外,SpringAOP也提供了基於配置的方式來定義切面類:

    <!--AspectJ - 基於配置-->
    <bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"/>
    <bean id="greetingAspect" class="com.smart4j.framework.aspectj.GreetingAspect"/>

    <aop:config>
        <aop:aspect ref="greetingAspect">
            <aop:around method="around" pointcut="execution(* com.smart4j.framework.GreetingImpl.*(..))"/>
        </aop:aspect>
    </aop:config>

使用<aop:config>元素來進行AOP配置,在其子元素中配置切面,包括增強型別、目標方法、切點等資訊。

無論使用者是不能使用註解,還是不願意使用註解,SpringAOP都能提供全方位的服務。

AOP思維導圖

 

各類增強型別所對應的解決方案

SpringAOP整體架構UML類圖

原始碼