Spring-aop 全面解析(從應用到原理)

fulton發表於2017-05-18

寫了很多篇文章了,但寫的文章大家都反映平平(但我卻感覺是自己的嘔心瀝血之作),是時候改變一下寫作技巧了,希望能通過一種愉快的方式使大家學到最多的知識。
以前寫的文章直接上原始碼分析,這會讓不瞭解的人看著很累,得不到想要的效果。本篇文章則從背景-原理-使用-原始碼的順序為大家解析。如果不希望深入瞭解,看到“使用”這個層次就足夠了。希望大家能愉快地看完這篇文章,多少給點反饋唄

一、AOP

AOP 產生的背景

“存在即合理”,任何一種理論或技術的產生,必然有它的原因。瞭解它產生的背景、為了解決的問題有助於我們更好地把握AOP的概念。
軟體開發一直在尋求一種高效開發、護展、維護的方式。從程式導向的開發實踐中,前人將關注點抽象出來,對行為和屬性進行聚合,形成了物件導向的開發思想,其在一定程度上影響了軟體開發的過程。鑑於此,我們在開發的過程中會對軟體開發進行抽象、分割成各個模組或物件。例如,我們會對API進行抽象成四個模組:Controller,Service,Gateway,Command.這很好地解決了業務級別的開發,但對於系統級別的開發我們很難聚焦。比如、對於每一個模組需要進行打日誌、程式碼監控、異常處理。以打日誌為例,我只能將日誌程式碼巢狀在各個物件上,而無法關注日誌本身,而這種現象又偏離了OOP思想。

Spring-aop 全面解析(從應用到原理)

Spring-aop 全面解析(從應用到原理)

為了能夠更好地將系統級別的程式碼抽離出來,去掉與物件的耦合,就產生了面向AOP(面向切面)。如上圖所示,OOP屬於一種橫向擴充套件,AOP是一種縱向擴充套件。AOP依託於OOP,進一步將系統級別的程式碼抽象出來,進行縱向排列,實現低耦合。
至於AOP的確切的概念,我不希望給出抽象複雜的表述,只需要瞭解其作用即可。

1.2 AOP 的家庭成員

1.2.1 PointCut

即在哪個地方進行切入,它可以指定某一個點,也可以指定多個點。
比如類A的methord函式,當然一般的AOP與語言(AOL)會採用多用方式來定義PointCut,比如說利用正規表示式,可以同時指定多個類的多個函式。

1.2.2 Advice

在切入點幹什麼,指定在PointCut地方做什麼事情(增強),打日誌、執行快取、處理異常等等。

1.2.3 Advisor/Aspect

PointCut + Advice 形成了切面Aspect,這個概念本身即代表切面的所有元素。但到這一地步並不是完整的,因為還不知道如何將切面植入到程式碼中,解決此問題的技術就是PROXY

1.2.4 Proxy

Proxy 即代理,其不能算做AOP的家庭成員,更相當於一個管理部門,它管理 了AOP的如何融入OOP。之所以將其放在這裡,是因為Aspect雖然是面向切面核心思想的重要組成部分,但其思想的踐行者卻是Proxy,也是實現AOP的難點與核心據在。

二、AOP的技術實現Proxy

AOP僅僅是一種思想,那為了讓這種思想發光,必然脫離語言本身的技術支援,Java在實現該技術時就是採用的代理Proxy,那我們就去了解一下,如何通過代理實現面向切面

1.靜態代理

Spring-aop 全面解析(從應用到原理)

就像我們去買二手房要經過中介一樣,房主將房源委託給中介,中介將房源推薦給買方。中間的任何手續的承辦都由中介來處理,不需要我們和房主直接打交道。無論對買方還是賣房都都省了很多事情,但同時也要付出代價,對於買房當然是中介費,對於程式碼的話就是效能。下面我們來介紹實現AOP的三種代理方式。
下面我就以買房的過程中需要打日誌為例介紹三種代理方式
靜態和動態是由代理產生的時間段來決定的。靜態代理產生於程式碼編譯階段,即一旦程式碼執行就不可變了。下面我們來看一個例子

public interface IPerson {
    public void doSomething();
}複製程式碼
public class Person implements IPerson {
    public void doSomething(){
        System.out.println("I want wo sell this house");
    }
}複製程式碼

public class PersonProxy {
    private IPerson iPerson;
    private final static Logger logger = LoggerFactory.getLogger(PersonProxy.class);

    public PersonProxy(IPerson iPerson) {
        this.iPerson = iPerson;
    }
    public void doSomething() {
        logger.info("Before Proxy");
        iPerson.doSomething();
        logger.info("After Proxy");
    }

    public static void main(String[] args) {
        PersonProxy personProxy = new PersonProxy(new Person());
        personProxy.doSomething();
    }
}複製程式碼

通過代理類我們實現了將日誌程式碼整合到了目標類,但從上面我們可以看出它具有很大的侷限性:需要固定的類編寫介面(或許還可以接受,畢竟有提倡面向介面程式設計),需要實現介面的每一個函式(不可接受),同樣會造成程式碼的大量重複,將會使程式碼更加混亂。

2.動態代理

那能否通過實現一次程式碼即可將logger織入到所有函式中呢,答案當然是可以的,此時就要用到java中的反射機制


public class PersonProxy implements InvocationHandler{
    private Object delegate;
    private final Logger logger = LoggerFactory.getLogger(this.getClass();

    public Object bind(Object delegate) {
        this.delegate = delegate;
        return Proxy.newProxyInstance(delegate.getClass().getClassLoader(), delegate.getClass().getInterfaces(), this);
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        try {
            logger.info("Before Proxy");
            result = method.invoke(delegate, args);
            logger.info("After Proxy");
        } catch (Exception e) {
            throw e;
        }
        return result;
    }

    public static void main(String[] args) {
        PersonProxy personProxy = new PersonProxy();
        IPerson iperson = (IPerson) personProxy.bind(new Person());
        iperson.doSomething();
    }
}複製程式碼

它的好處理時可以為我們生成任何一個介面的代理類,並將需要增強的方法織入到任意目標函式。但它仍然具有一個侷限性,就是隻有實現了介面的類,才能為其實現代理。

3.CGLIB

CGLIB解決了動態代理的難題,它通過生成目標類子類的方式來實現來實現代理,而不是介面,規避了介面的侷限性。
CGLIB是一個強大的高效能程式碼生成包(生成原理還沒研究過),其在執行時期(非編譯時期)生成被 代理物件的子類,並重寫了被代理物件的所有方法,從而作為代理物件。

public class PersonProxy implements MethodInterceptor {
    private Object delegate;
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public Object intercept(Object proxy, Method method, Object[] args,  MethodProxy methodProxy) throws Throwable {
        logger.info("Before Proxy");
        Object result = methodProxy.invokeSuper(method, args);
        logger.info("After Proxy");
        return result;
    }

    public static Person getProxyInstance() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Person.class);

        enhancer.setCallback(new PersonProxy());
        return (Person) enhancer.create();
    }
}複製程式碼

當然CGLIB也具有侷限性,對於無法生成子類的類(final類),肯定是沒有辦法生成代理子類的。

以上就是三種代理的實現方式,但千成別被迷惑了,在Spring AOP中這些東西已經被封裝了,不需要我們自己實現。要不然得累死,但瞭解AOP的實現原理(即基於代理)還是很有必要的。

三、應用

先建一個目標類(這個類我自己都噁心),還是賣房子的事,討論價格,直接寫死了,意思意思……

public class Person {
    public int tradePrice () {
        return 1000;
    }
}複製程式碼

繼承MethodInteceptor來實現一個Advisor,當然可選擇的有不少,下面都有介紹,也在這裡意思意思……

public class LogsInterceptor implements MethodInterceptor {
    Logger logger = LoggerFactory.getLogger(this.getClass().getName());
    public Object invoke(MethodInvocation invocation) throws Throwable {
        try {
            logger.info("Start bargaining....");
            Object returnValue = invocation.proceed();
        } catch (Exception e) {
            throw e;
        } finally {
            logger.info("Bargaining Over");
        }
        return null;
    }
}複製程式碼

3.1. 配置ProxyFactoryBean,顯式地設定pointCut,advice,advisor

一個個地配置就可以了,這樣雖然麻煩,但是你知道原理呀……


<bean id="pointCut" class="org.springframework.aop.support.NameMatchMethodPointcut">
    <property name="mappedName" value="tradePrice"/>
</bean>
<bean id="myInterceptor" class="com.sankuai.meituan.meishi.poi.tag.LogsInterceptor"></bean>
<bean id="myAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <property name="pointcut" ref="pointCut"/>
    <property name="advice" ref="myInterceptor"/>
</bean>
<bean id="myProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
        </list>
    </property>
</bean>複製程式碼

但上面這個噁心的地方在於,你得一個個地指定義advisor,有幾個目標pointCut,你就得定義幾次,這還不得噁心死

3.2. 配置AutoProxyCreator,這種方式下,還是如以前一樣使用定義的bean,但是從容器中獲得的其實已經是代理物件

為了解決上面的問題AutoProxyCreator 有兩個具體的實現類BeanNameAutoProxyCreator和DefaultAdvisorAutoProxyCreator,先拿一個練練手

<bean id="pointCut1" class="org.springframework.aop.support.NameMatchMethodPointcut">
    <property name="mappedName" value="tradePrice"/>
</bean>
<bean id="pointCut2" class="org.springframework.aop.support.NameMatchMethodPointcut">
    <property name="mappedName" value="tradePrice"/>
</bean>
<bean id="myInterceptor" class="com.sankuai.meituan.meishi.poi.tag.LogsInterceptor"></bean>

<bean id="myProxy" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames">
        <list>
            <value>pointCut1</value>
            <value>pointCut2</value>
        </list>
    </property>
    <property name="interceptorNames">
        <list>
            <value>myInterceptor</value>
        </list>
    </property>
</bean>複製程式碼

是不是感覺好多了,但大家都不喜歡寫這麼東西(不過我還是推薦採用這種配置的)

3.3. 通過aop:config來配置

大家參考一下吧,不推薦

3.4. 通過來配置,使用AspectJ的註解來標識通知及切入點

引入aspectj就可用aspect的註解來實現了,這個才是只關注切面本身就可以了


public class MyAspect {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Pointcut("execution(public void *.method1)")
    public void pointcutName(){}

    @Around("pointcutName()")
    public Object performanceTrace(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        try {
            logger.info("log.....");
            return proceedingJoinPoint.proceed();

        } finally {
            logger.info("log end");
        }
    }
}複製程式碼

切面的代理生成就靠它了……

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>複製程式碼

aspectj的功能是非常強大的,其定義語法和運算這裡就不再深入了,不是本文的重點

四、深度分析

Spring AOP是AOL家庭Loser 也是winner,遵循大道至簡。Spring AOP只支援部分AOP的功能,作為一個輕量級框架,實現了AOP20%的技術,支撐80%的需求,雖然對AspectJ進行了整合,但其內部原理仍然使用是是Spring AOP,所以也只能使用AspectJ的部分功能。

4.1 Spring Aop的家庭成員

以下內容僅侷限於Spring Aop,而不包括其它AOL(Spring Aop相對於其它AOL功能簡單的多,但也預留了對其它AOL的支援)

4.1.1 PointCut

Spring-aop 全面解析(從應用到原理)

我們來看一下PointCut的類圖,以PointCut介面為核心進行擴充套件
PointCut 依賴了ClassFilter和MethodMatcher,ClassFilter用來指定特定的類,MethodMatcher 指定特定的函式,正是由於PointCut僅有的兩個依賴,它只能實現函式級別的AOP。對於屬性、for語句等是無法實現該切點的。
MethodMatcher 有兩個實現類StaticMethodMatcher和DynamicMethodMatcher,它們兩個實現的唯一區別是isRuntime(參考下面的原始碼)。StaticMethodMatcher不在執行時檢測,DynamicMethodMatcher要在執行時實時檢測引數,這也會導致DynamicMethodMatcher的效能相對較差。

public abstract class StaticMethodMatcher implements MethodMatcher {

   @Override
   public final boolean isRuntime() {
      return false;
   }

   @Override
   public final boolean matches(Method method, Class<?> targetClass, Object[] args) {
      // should never be invoked because isRuntime() returns false
      throw new UnsupportedOperationException("Illegal MethodMatcher usage");
   }
}複製程式碼

public abstract class DynamicMethodMatcher implements MethodMatcher {

   @Override
   public final boolean isRuntime() {
      return true;
   }

   /**
    * Can override to add preconditions for dynamic matching. This implementation
    * always returns true.
    */
   @Override
   public boolean matches(Method method, Class<?> targetClass) {
      return true;
   }

}複製程式碼

類似繼承於StaticMethodMatcher和DynamicMethodMatcher也有兩個分支StaticMethodMatcherPointcut和DynamicMethodMatcherPointcut,StaticMethodMatcherPointcut是我們最常用,其具體實現有兩個NameMatchMethodPointcut和JdkRegexpMethodPointcut,一個通過name進行匹配,一個通過正規表示式匹配。
有必要對另外一個分支說一下ExpressionPointcut,它的出現是了對AspectJ的支援,所以其具體實現也有AspectJExpressionPointcut
最左邊的三個給我們提供了三個更強功能的PointCut
AnnotationMatchingPointcut:可以指定某種型別的註解
ComposiblePointcut:進行與或操作
ControlFlowPointcut:這個有些特殊,它是一種控制流,例如類A 呼叫B.method(),它可以指定當被A呼叫時才進行攔截。

3.1.2 Advice

我們來看一下Advice 的類圖,先看一下介面的分類:

Spring-aop 全面解析(從應用到原理)

AfterAdvice是指函式呼叫結束之後增強,它又包括兩種情況:異常退出和正常退出;BeforeAdvice指函式呼叫之前增強;Inteceptor有點特殊,它是由AOP聯盟定義的標準,也是為了方便Spring AOP 擴充套件,以便對其它AOL支援。Interceptor有很多擴充套件,比如Around Advice的功能實現(具體實現是Advisor的內容了,接下來再看)

3.1.3 Advisor

同樣Advisor按照Advice去分也可以分成兩條線路,一個是來源於Spring AOP 的型別,一種是來自AOP聯盟的Interceptoor, IntroductionAdvisor就是對MethodInterceptor的繼承和實現

Spring-aop 全面解析(從應用到原理)

所以接下類我們還是分成兩類來研究其具體實現:
Spring AOP的PointcutAdvisor

Spring-aop 全面解析(從應用到原理)

AbstractPointcutAdvisor 實現了Ordered,為多個Advice指定順序,順序為Int型別,越小優先順序越高,
AbstractGenericPointcutAdvisor 指定了Advice,除了Introduction之外的型別
下面具體的Advisor實現則對應於PointCut 的型別,具體指定哪個pointCut,

Introduction型別,與上面的基本類似,不再介紹了

Spring-aop 全面解析(從應用到原理)

3.1.4 Proxy

這一節才最關鍵,它決定了如何具體實現AOP,所以這一節也將會難理解一些,
先看一下類圖,看起來也挺簡單

Spring-aop 全面解析(從應用到原理)

ProxyConfig設定了幾個引數

private boolean proxyTargetClass = false;

private boolean optimize = false;

boolean opaque = false;

boolean exposeProxy = false;

private boolean frozen = false;複製程式碼

private boolean proxyTargetClass = false;
代理有兩種方式:一種是介面代理(上文提到過的動態代理),一種是CGLIB。預設有介面的類採用介面代理,否則使用CGLIB。如果設定成true,則直接使用CGLIB;
原文註釋如下


/**
 * Set whether to proxy the target class directly, instead of just proxying
 * specific interfaces. Default is "false".
 * <p>Set this to "true" to force proxying for the TargetSource's exposed
 * target class. If that target class is an interface, a JDK proxy will be
 * created for the given interface. If that target class is any other class,
 * a CGLIB proxy will be created for the given class.
 * <p>Note: Depending on the configuration of the concrete proxy factory,
 * the proxy-target-class behavior will also be applied if no interfaces
 * have been specified (and no interface autodetection is activated).
 * @see org.springframework.aop.TargetSource#getTargetClass()
 */複製程式碼

private boolean optimize = false;是否進行優化,不同代理的優化一般是不同的。如代理物件生成之後,就會忽略Advised的變動。


/**
 * Set whether proxies should perform aggressive optimizations.
 * The exact meaning of "aggressive optimizations" will differ
 * between proxies, but there is usually some tradeoff.
 * Default is "false".
 * <p>For example, optimization will usually mean that advice changes won't
 * take effect after a proxy has been created. For this reason, optimization
 * is disabled by default. An optimize value of "true" may be ignored
 * if other settings preclude optimization: for example, if "exposeProxy"
 * is set to "true" and that's not compatible with the optimization.
 */複製程式碼

opaque 是否強制轉化為advised


/**
 * Set whether proxies created by this configuration should be prevented
 * from being cast to {@link Advised} to query proxy status.
 * <p>Default is "false", meaning that any AOP proxy can be cast to
 * {@link Advised}.
 */複製程式碼

exposeProxy:AOP生成物件時,繫結到ThreadLocal, 可以通過AopContext獲取

 /**
 * Set whether the proxy should be exposed by the AOP framework as a
 * ThreadLocal for retrieval via the AopContext class. This is useful
 * if an advised object needs to call another advised method on itself.
 * (If it uses {@code this}, the invocation will not be advised).
 * <p>Default is "false", in order to avoid unnecessary extra interception.
 * This means that no guarantees are provided that AopContext access will
 * work consistently within any method of the advised object.
 */複製程式碼

frozen:代理資訊一旦設定,是否允許改變

/**
 * Set whether this config should be frozen.
 * <p>When a config is frozen, no advice changes can be made. This is
 * useful for optimization, and useful when we don't want callers to
 * be able to manipulate configuration after casting to Advised.
 */複製程式碼

AdvisedSupport則的作用是設定生成代理物件所需要的全部資訊。

ProxyCreatorSupport則完成生成代理的相關工作。
至於它們兩個具體做了哪些工作,這裡不展開講了,因為內容很多,不同的advisor和pointCut所進行的操作封裝有所不同, 在使用到的時候再仔細看原始碼,很容易看懂。

五、最佳實踐

那什麼時候來使用AOP呢?我就私自定義一下:當一個功能和物件本身沒有必段關係,重複出現在多個模組時,就應該用AOP來解耦。
有人總結了一下使用情景(參考, 是否認同自己決定):
Authentication 許可權
Caching 快取
Context passing 內容傳遞
Error handling 錯誤處理
Lazy loading 懶載入
Debugging  除錯
logging, tracing, profiling and monitoring 記錄跟蹤 優化 校準
Performance optimization 效能優化
Persistence  持久化
Resource pooling 資源池
Synchronization 同步
Transactions 事務

5.1 日誌

看了很多資料都是用日誌做AOP的例子,其實這個並不太好,因為AOP很難完全實現log的行為,但對於某一型別的日誌處理還是有用的。
例如:對Command的異常進行統一處理,對Controller層的請求進行打日誌
請參考日誌:github.com/ameizi/DevA…

5.2 程式碼效能測試

利用PerformanceMonitorInterceptor來協助應用效能優化, spring自帶的

www.cnblogs.com/f1194361820…
當然還有這個JamonPerformanceMonitorInterceptor

相關文章