說明:本文首發(fā)于微信公眾號并同步至個人博客 http://www.fengzheng.pub/archives/277.html,轉(zhuǎn)載請注明來源。

AOP 是什么東西

首先來說 AOP 并不是 Spring 框架的核心技術(shù)之一,AOP 全稱 Aspect Orient Programming,即面向切面的編程。其要解決的問題就是在不改變源代碼的情況下,實現(xiàn)對邏輯功能的修改。常用的場景包括記錄日志、異常處理、性能監(jiān)控、安全控制(例如攔截器)等,總結(jié)起來就是,凡是想對當(dāng)前功能做變更,但是又不想修改源代碼的情況下,都可以考慮是否可以用 AOP 實現(xiàn)。

為什么要面向切面呢,我直接改源代碼不是很好嗎?當(dāng)然沒有問題,如果情況允許。但是考慮到下面這些情況,我本來寫好了1000個方法,有一天,我想加入一些控制,我想在執(zhí)行方法邏輯之前,檢查一些系統(tǒng)參數(shù),參數(shù)檢查沒問題再執(zhí)行邏輯,否則不執(zhí)行。這種情況怎么辦呢,難道要修改這1000個方法嗎,那簡直就是災(zāi)難。還有,有些線上邏輯執(zhí)行緩慢,但我又不想重新部署環(huán)境,因為那樣會影響線上業(yè)務(wù),這種情況下,也可以考慮 AOP 方式,Btrace 就是這樣一個線上性能排查的神器。

 

Spring AOP 的用法

面向切面編程,名字好像很炫酷,但是使用方式已經(jīng)被 Spring 封裝的非常簡單,只需要簡單的配置即可實現(xiàn)。使用方式不是本文介紹的重點,下面僅演示最簡單最基礎(chǔ)的使用,實現(xiàn)對調(diào)用的方法進(jìn)行耗時計算,并打印出來。

環(huán)境說明: JDK 1.8 ,Spring mvc 版本 4.3.2.RELEASE

1. 首先引用 Spring mvc 相關(guān)的 maven 包,太多了,就不列了,只列出 Spring-aop 相關(guān)的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version> 4.3.2.RELEASE </version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.9</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.9</version>
</dependency>

2. 在 Spring mvc 配置文件中增加關(guān)于 AOP 的配置,內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<beans default-lazy-init="true"
    xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation=" 
       http://www.springframework.org/schema/beans  
       http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://www.springframework.org/schema/mvc  
       http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.3.xsd
       http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
 
    <!-- 自動掃描與裝配bean -->
    <context:component-scan base-package="kite.lab.spring"></context:component-scan>
           <!-- 啟動 @AspectJ 支持 -->
    <aop:aspectj-autoproxy proxy-target-class="true" />
</bean>

3. 創(chuàng)建切面類,并在 kite.lab.spring.service 包下的方法設(shè)置切面,使用 @Around 注解監(jiān)控,實現(xiàn)執(zhí)行時間的計算并輸出,內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
 
@Component
@Aspect
public class PerformanceMonitor {
     
    //配置切入點,該方法無方法體,主要為方便同類中其他方法使用此處配置的切入點
    @Pointcut("execution(* kite.lab.spring.service..*(..))")
    public void aspect(){   }
 
    @Around("aspect()")
    public Object methodTime(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 開始
        Object retVal = pjp.proceed();
        stopWatch.stop();
        // 結(jié)束
        System.out.println(String.format("方法 %s 耗時 %s ms!",pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis()));
        return retVal;
    }
}

4. 被切面監(jiān)控的類定義如下:

1
2
3
4
5
6
7
8
package kite.lab.spring.service;
 
public class Worker {
    public String dowork(){
        System.out.println("生活向來不易,我正在工作!");
        return "";
    }
}

5. 加載 Spring mvc 配置文件,并調(diào)用 Worker 類的方法  

1
2
3
4
5
6
public static void main(String[] args) {
        String filePath = "spring-servlet.xml";
        ApplicationContext ac = new FileSystemXmlApplicationContext(filePath);
        Worker worker = (Worker) ac.getBean("worker");
        worker.dowork();
}

6. 顯示結(jié)果如下:

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)  

說完用法,接下來說一下實現(xiàn)原理,知其然也要知其所以然?! ?/p>

Spring AOP 原理

AOP 的實現(xiàn)原理就是動態(tài)的生成代理類,代理類的執(zhí)行過程為:執(zhí)行我們增加的代碼(例如方法日志記錄)—-> 回調(diào)原方法 ——> 增加的代碼邏輯??磮D比較好理解:

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)

Spring AOP 動態(tài)代理可能采用 JDK 動態(tài)代理或 CGlib 動態(tài)生成代理類兩種方式中的一種, 決定用哪一種方式的判斷標(biāo)準(zhǔn)就是被切面的類是否有其實現(xiàn)的接口,如果有對應(yīng)的接口,則采用 JDK 動態(tài)代理,否則采用 CGlib 字節(jié)碼生成機(jī)制動態(tài)代理方式。

代理模式是一種常用的設(shè)計模式,其目的就是為其他對象提供一個代理以控制對某個對象的訪問。代理類負(fù)責(zé)為委托類預(yù)處理消息,過濾消息并轉(zhuǎn)發(fā)消息,以及進(jìn)行消息被委托類執(zhí)行后的后續(xù)處理。代理類和委托類實現(xiàn)相同的接口,所以調(diào)用者調(diào)用代理類和調(diào)用委托類幾乎感覺不到差別。

是不是看完了定義,感覺正好可以解決切面編程方式要解決的問題。下圖是基本的靜態(tài)代理模式圖:

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)

而動態(tài)代理的意思是運(yùn)行時動態(tài)生成代理實現(xiàn)類,由于 JVM 的機(jī)制,需要直接操作字節(jié)碼,生成新的字節(jié)碼文件,也就是 .class 文件。

JDK 動態(tài)代理

JDK 動態(tài)代理模式采用 sun 的 ProxyGenerator 的字節(jié)碼框架。要說明的是,只有實現(xiàn)了接口的類才能使用 JDK 動態(tài)代理技術(shù),實現(xiàn)起來也比較簡單。

1. 只要實現(xiàn) InvocationHandler 接口,并覆寫 invoke方法即可。具體實現(xiàn)代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package kite.lab.spring.aop.jdkaop;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
/**
 * JdkProxy
 *
 * @author fengzheng
 */
public class JdkProxy implements InvocationHandler {
 
    private Object target;
 
    /**
     * 綁定委托對象并返回一個代理類
     *
     * @param target
     * @return
     */
    public Object bind(Object target) {
        this.target = target;
        //取得代理對象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), this);
    }
  
    /**
     * 調(diào)用方法
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        Object result = null;
        System.out.println("事物開始");
        //執(zhí)行方法
        result = method.invoke(target, args);
        System.out.println("事物結(jié)束");
        return result;
    }
}

Proxy.newProxyInstance 方法用于動態(tài)生成實際生成的代理類,三個參數(shù)依次為被代理類的類加載器、被代理類所實現(xiàn)的接口和當(dāng)前代理攔截器。

覆寫的 invoke 中可以加入我們增加的業(yè)務(wù)邏輯,然后回調(diào)原方法。

2. 被代理的類仍然用的前面 spring aop 介紹的那個worker 類,只不過我們需要讓這個類實現(xiàn)自接口,接口定義如下:

1
2
3
4
5
6
7
8
9
package kite.lab.spring.service;
 
/**
 * IWorker
 *
**/
public interface IWorker {
    String dowork();
}

3. 實際調(diào)用如下:

1
2
3
4
5
public static void main(String[] args) {
        JdkProxy jdkProxy = new JdkProxy();
        IWorker worker = (IWorker) jdkProxy.bind(new Worker());
        worker.dowork();
    }

原理說明: jdkProxy.bind 會生成一個實際的代理類,這個生成過程是利用的字節(jié)碼生成技術(shù),生成的代理類實現(xiàn)了IWorker 接口,我們調(diào)用這個代理類的 dowork 方法的時候,實際在代理類中是調(diào)用了 JdkProxy (也就是我們實現(xiàn)的這個代理攔截器)的 invoke 方法,接著執(zhí)行我們實現(xiàn)的 invoke 方法,也就執(zhí)行了我們加入的邏輯,從而實現(xiàn)了切面編程的需求。

我們把動態(tài)生成的代理類字節(jié)碼文件反編譯一下,也就明白了。由于代碼較長,只摘出相關(guān)部分。

首先看到類的接口和繼承關(guān)系:

public final class $Proxy0 extends Proxy implements IWorker 代理類被命名為 $Proxy0 ,繼承了 Proxy ,并且實現(xiàn)了IWorker ,這是關(guān)鍵點。

找到 dowork 方法,代碼如下:

1
2
3
4
5
6
7
8
9
public final String dowork() throws  {
        try {
            return (String)super.h.invoke(this, m3, (Object[])null);
        catch (RuntimeException | Error var2) {
            throw var2;
        catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

super.h 就是我們實現(xiàn)的JdkProxy 這個類,可以看到調(diào)用了這個類的 invoke 方法,并且傳入了參數(shù) m3 ,再來看 m3 是什么

 m3 = Class.forName("kite.lab.spring.service.IWorker").getMethod("dowork", new Class0);   

看到了吧,m3 就是 dowork 方法,是不是流程就明確了。

但是,并不是所有的被代理的類(要被切面的類)都實現(xiàn)了某個接口,沒有實現(xiàn)接口的情況下,JDK 動態(tài)代理就不行了,這時候就要用到 CGlib 字節(jié)碼框架了。

CGLIB 動態(tài)代理

CGlib庫使用了ASM這一個輕量但高性能的字節(jié)碼操作框架來轉(zhuǎn)化字節(jié)碼,它可以在運(yùn)行時基于一個類動態(tài)生成它的子類。厲害了吧,不管有沒有接口,凡是類都可以被繼承,擁有這樣的特點,原則上來說,它可以對任何類進(jìn)行代碼攔截,從而達(dá)到切面編程的目的。

CGlib 不需要我們非常了解字節(jié)碼文件(.class 文件)的格式,通過簡單的 API 即可實現(xiàn)字節(jié)碼操作。

基于這樣的特點,CGlib 被廣泛用于如 Spring AOP 等基于 代理模式的AOP框架中。

下面就基于 CGlib 實現(xiàn)一個簡單的動態(tài)代理模式。

1. 創(chuàng)建攔截類實現(xiàn) MethodInterceptor接口,并覆intercept 方法,在此方法中加入我們增加的邏輯,代碼如下:

1
2
3
4
5
6
7
8
public class MyAopWithCGlib implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("嘿,你在干嘛?");
        methodProxy.invokeSuper(o, objects);
        System.out.println("是的,你說的沒錯。");
        return null;
    }

2. 被代理的類依然是上面的 Worker 類,并且不需要接口。

3. 客戶端調(diào)用代理方法的代碼如下:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "cglib");
        MyAopWithCGlib aop = new MyAopWithCGlib();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Worker.class);
        enhancer.setCallback(aop);
        Worker worker = (Worker) enhancer.create();
        worker.dowork();
    }

代碼第一行是要將動態(tài)生成的字節(jié)碼文件持久化到磁盤,方便反編譯觀察。

利用 CGlib 的 Enhancer 對象,設(shè)置它的繼承父類,設(shè)置回調(diào)類,即上面實現(xiàn)的 攔截類,然后用create 方法創(chuàng)造一個 Worker 類,實際這個類是 Worker 類的子類,然后調(diào)用dowork方法。執(zhí)行結(jié)果如下:

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)  

可以看到我們織入的代碼起作用了。

4. 上面功能比較簡單,它會橫向切入被代理類的所有方法中,下來我們稍微做的復(fù)雜一點??刂埔幌?,讓有些方法被織入代碼,有些不被織入代碼,模仿 Spring aop ,我們新增一個注解,用于注解哪些方法要被橫向切入。注解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package kite.lab.spring.aop.AopWithCGlib;
 
import java.lang.annotation.*;
 
/**
 * CGLIB
 *
 * @author fengzheng
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface CGLIB {
    String value() default "";
}

5. 然后再 Worker 中增加一個方法,并應(yīng)用上面的注解  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package kite.lab.spring.service;
 
import kite.lab.spring.aop.AopWithCGlib.CGLIB;
 
/**
 * Worker
 *
 * @author fengzheng
 */
public class Worker  {
    public String dowork(){
        System.out.println("生活向來不易,我正在工作!");
        return "";
    }
 
    @CGLIB(value = "cglib")
    public void dowork2(){
        System.out.println("生活如此艱難,我在奔命!");
    }
}

我們在 dowrok2 上應(yīng)用了上面的注解

6. 在攔截方法中加入注解判斷邏輯,如果加了上面的注解,就加入織入的代碼邏輯,否則不加入,代碼如下:  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Annotation[] annotations = method.getDeclaredAnnotations();
        boolean isCglib = false;
        for(Annotation annotation: annotations){
            if (annotation.annotationType().getName().equals("kite.lab.spring.aop.AopWithCGlib.CGLIB")){
                isCglib = true;
            }
        }
        if(isCglib) {
            System.out.println("嘿,你在干嘛?");
            methodProxy.invokeSuper(o, objects);
            System.out.println("是的,你說的沒錯。");
        }
        return null;
    }


http://www.cnblogs.com/fengzheng/p/7228627.html