AOP如何實現及實現原理

馬佩發表於2018-11-23

概述:

最近在開發中遇到了一個剛好可以用AOP實現的例子,就順便研究了AOP的實現原理,把學習到的東西進行一個總結。文章中用到的程式語言為kotlin,需要的可以在IDEA中直接轉為java。 這篇文章將會按照如下目錄展開:

  • AOP簡介
  • 程式碼中實現舉例
  • AOP實現原理
  • 部分原始碼解析

1. AOP簡介

相信大家或多或少的瞭解過AOP,都知道它是面向切面程式設計,在網上搜尋可以找到很多的解釋。這裡我用一句話來總結:AOP是能夠讓我們在不影響原有功能的前提下,為軟體橫向擴充套件功能。 那麼橫向擴充套件怎麼理解呢,我們在WEB專案開發中,通常都遵守三層原則,包括控制層(Controller)->業務層(Service)->資料層(dao),那麼從這個結構下來的為縱向,它具體的某一層就是我們所說的橫向。我們的AOP就是可以作用於這某一個橫向模組當中的所有方法。

我們在來看一下AOP和OOP的區別:AOP是OOP的補充,當我們需要為多個物件引入一個公共行為,比如日誌,操作記錄等,就需要在每個物件中引用公共行為,這樣程式就產生了大量的重複程式碼,使用AOP可以完美解決這個問題。

接下來介紹一下提到AOP就必須要了解的知識點:

  • 切面:攔截器類,其中會定義切點以及通知
  • 切點:具體攔截的某個業務點。
  • 通知:切面當中的方法,宣告通知方法在目標業務層的執行位置,通知型別如下:
    1. 前置通知:@Before 在目標業務方法執行之前執行
    2. 後置通知:@After 在目標業務方法執行之後執行
    3. 返回通知:@AfterReturning 在目標業務方法返回結果之後執行
    4. 異常通知:@AfterThrowing 在目標業務方法丟擲異常之後
    5. 環繞通知:@Around 功能強大,可代替以上四種通知,還可以控制目標業務方法是否執行以及何時執行

2. 程式碼中實現舉例

上面已經大概的介紹了AOP中需要了解的基本知識,也知道了AOP的好處,那怎麼在程式碼中實現呢?給大家舉個例子:我們現在有個學校管理系統,已經實現了對老師和學生的增刪改,又新來個需求,說是對老師和學生的每次增刪改做一個記錄,到時候校長可以檢視記錄的列表。那麼問題來了,怎麼樣處理是最好的解決辦法呢?這裡我羅列了三種解決辦法,我們來看下他的優缺點。

AOP如何實現及實現原理
-最簡單的就是第一種方法,我們直接在每次的增刪改的函式當中直接實現這個記錄的方法,這樣程式碼的重複度太高,耦合性太強,不建議使用。

-其次就是我們最長使用的,將記錄這個方法抽離出來,其他的增刪改呼叫這個記錄函式即可,顯然程式碼重複度降低,但是這樣的呼叫還是沒有降低耦合性。

-這個時候我們想一下AOP的定義,再想想我們的場景,其實我們就是要在不改變原來增刪改的方法,給這個系統增加記錄的方法,而且作用的也是一個層面的方法。這個時候我們就可以採用AOP來實現了。

我們來看下程式碼的具體實現:

  1. 首先我定義了一個自定義註解作為切點
@Target(AnnotationTarget.FUNCTION)  //註解作用的範圍,這裡宣告為函式
@Order(Ordered.HIGHEST_PRECEDENCE)  //宣告註解的優先順序為最高,假設有多個註解,先執行這個
annotation class Hanler(val handler: HandlerType)  //自定義註解類,HandlerType是一個列舉型別,裡面定義的就是學生和老師的增刪改操作,在這裡就不展示具體內容了
複製程式碼
  1. 接下來就是要定義切面類了
@Aspect   //該註解宣告這個類為一個切面類
@Component
class HandlerAspect{

 @Autowired
 private lateinit var handlerService: HandlerService

@AfterReturning("@annotation(handler)")   //當有函式註釋了註解,將會在函式正常返回後在執行我們定義的方法
fun hanler(hanler: Hanler) {
    handlerService.add(handler.operate.value)   //這裡是真正執行記錄的方法
}
}
複製程式碼
  1. 最後就是我們本來的業務方法了
/**
* 刪除學生方法
*/
@Handler(operate= Handler.STUDENT_DELETE)   //當執行到刪除學生方法時,切面類就會起作用了,當學生正常刪除後就會執行記錄方法,我們就可以看到記錄方法生成的資料
fun delete(id:String) {
   studentService.delete(id)
}
複製程式碼

3. AOP實現原理

我們現在瞭解了程式碼中如何實現,那麼AOP實現的原理是什麼呢?之前看了一個部落格說到,提到AOP大家都知道他的實現原理是動態代理,顯然我之前就是不知道的,哈哈,但是相信閱讀文章的你們一定是知道的。

講到動態代理就不得不說代理模式了, 代理模式的定義:給某一個物件提供一個代理,並由代理物件控制對原物件的引用。代理模式包含如下角色:subject:抽象主題角色,是一個介面。該介面是物件和它的代理共用的介面; RealSubject:真實主題角色,是實現抽象主題介面的類; Proxy:代理角色,內部含有對真實物件RealSubject的引用,從而可以操作真實物件。代理物件提供與真實物件相同的介面,以便代替真實物件。同時,代理物件可以在執行真實物件操作時,附加其他的操作,相當於對真實物件進行封裝。如下圖所示:

AOP如何實現及實現原理
AOP如何實現及實現原理

那麼代理又分為靜態代理和動態代理,這裡寫兩個小的demo,動態代理採用的就是JDK代理。舉個例子就是現在一個班上的學生需要交作業,現在由班長代理交作業,那麼班長就是代理,學生就是被代理的物件。

3.1 靜態代理

首先,我們建立一個Person介面。這個介面就是學生(被代理類),和班長(代理類)的公共介面,他們都有交作業的行為。這樣,學生交作業就可以讓班長來代理執行。

/**
 * Created by Mapei on 2018/11/7
 * 建立person介面
 */
public interface Person {
    //交作業
    void giveTask();
}
複製程式碼

Student類實現Person介面,Student可以具體實施交作業這個行為。

/**
 * Created by Mapei on 2018/11/7
 */
public class Student implements Person {
    private String name;
    public Student(String name) {
        this.name = name;
    }

    public void giveTask() {
        System.out.println(name + "交語文作業");
    }
}
複製程式碼

StudentsProxy類,這個類也實現了Person介面,但是還另外持有一個學生類物件,那麼他可以代理學生類物件執行交作業的行為。

/**
 * Created by Mapei on 2018/11/7
 * 學生代理類,也實現了Person介面,儲存一個學生實體,這樣就可以代理學生產生行為
 */
public class StudentsProxy implements Person{
    //被代理的學生
    Student stu;

    public StudentsProxy(Person stu) {
        // 只代理學生物件
        if(stu.getClass() == Student.class) {
            this.stu = (Student)stu;
        }
    }

    //代理交作業,呼叫被代理學生的交作業的行為
    public void giveTask() {
        stu.giveTask();
    }
}
複製程式碼

下面測試一下,看代理模式如何使用:

/**
 * Created by Mapei on 2018/11/7
 */
public class StaticProxyTest {
    public static void main(String[] args) {
        //被代理的學生林淺,他的作業上交有代理物件monitor完成
        Person linqian = new Student("林淺");

        //生成代理物件,並將林淺傳給代理物件
        Person monitor = new StudentsProxy(linqian);

        //班長代理交作業
        monitor.giveTask();
    }
}
複製程式碼

執行結果:

AOP如何實現及實現原理

這裡並沒有直接通過林淺(被代理物件)來執行交作業的行為,而是通過班長(代理物件)來代理執行了。這就是代理模式。代理模式就是在訪問實際物件時引入一定程度的間接性,這裡的間接性就是指不直接呼叫實際物件的方法,那麼我們在代理過程中就可以加上一些其他用途。比如班長在幫林淺交作業的時候想告訴老師最近林淺的進步很大,就可以輕鬆的通過代理模式辦到。在代理類的交作業之前加入方法即可。這個優點就可以運用在spring中的AOP,我們能在一個切點之前執行一些操作,在一個切點之後執行一些操作,這個切點就是一個個方法。這些方法所在類肯定就是被代理了,在代理過程中切入了一些其他操作。

3.2 動態代理

動態代理和靜態代理的區別是,靜態代理的的代理類是我們自己定義好的,在程式執行之前就已經變異完成,但是動態代理的代理類是在程式執行時建立的。相比於靜態代理,動態代理的優勢在於可以很方便的對代理類的函式進行統一的處理,而不用修改每個代理類中的方法。比如我們想在每個代理方法之前都加一個處理方法,我們上面的例子中只有一個代理方法,如果還有很多的代理方法,就太麻煩了,我們來看下動態代理是怎麼去實現的。

首先還是定義一個Person介面:

/**
 * Created by Mapei on 2018/11/7
 * 建立person介面
 */
public interface Person {
    //交作業
    void giveTask();
}
複製程式碼

接下來是建立需要被代理的實際類,也就是學生類:

/**
 * Created by Mapei on 2018/11/7
 */
public class Student implements Person {
    private String name;
    public Student(String name) {
        this.name = name;
    }

    public void giveTask() {
        System.out.println(name + "交語文作業");
    }
}
複製程式碼

建立StuInvocationHandler類,實現InvocationHandler介面,這個類中持有一個被代理物件的例項target。InvocationHandler中有一個invoke方法,所有執行代理物件的方法都會被替換成執行invoke方法。

/**
 * Created by Mapei on 2018/11/7
 */
public class StuInvocationHandler<T> implements InvocationHandler {
    //invocationHandler持有的被代理物件
    T target;

    public StuInvocationHandler(T target) {
        this.target = target;
    }

    /**
     * proxy:代表動態代理物件
     * method:代表正在執行的方法
     * args:代表呼叫目標方法時傳入的實參
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理執行" +method.getName() + "方法");
        Object result = method.invoke(target, args);
        return result;
    }
}
複製程式碼

那麼接下來我們就可以具體的建立代理物件了。

/**
 * Created by Mapei on 2018/11/7
 * 代理類
 */
public class ProxyTest {
    public static void main(String[] args) {

        //建立一個例項物件,這個物件是被代理的物件
        Person linqian = new Student("林淺");

        //建立一個與代理物件相關聯的InvocationHandler
        InvocationHandler stuHandler = new StuInvocationHandler<Person>(linqian);

        //建立一個代理物件stuProxy來代理linqian,代理物件的每個執行方法都會替換執行Invocation中的invoke方法
        Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);

        //代理執行交作業的方法
        stuProxy.giveTask();
    }
}
複製程式碼

我們執行代理測試類,首先我們建立了一個需要被代理的學生林淺,將林淺傳入stuHandler中,我們在建立代理物件stuProxy時,將stuHandler作為引數,那麼所有執行代理物件的方法都會被替換成執行invoke方法,也就是說,最後執行的是StuInvocationHandler中的invoke方法。所以在看到下面的執行結果也就理所當然了。

AOP如何實現及實現原理

那麼到這裡問題就來了,為什麼代理物件執行的方法都會通過InvocationHandler中的invoke方法來執行,帶著這個問題,我們需要看一下動態代理的原始碼,對他進行簡單的分析。

上面我們使用Proxy類的newProxyInstance方法建立了一個動態代理物件,看一下他的原始碼:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
  }
複製程式碼

然後,我們需要重點關注Class<?> cl = getProxyClass0(loader, intfs)這句程式碼,這裡產生了代理類,這個類就是動態代理的關鍵,由於是動態生成的類檔案,我們將這個類檔案列印到檔案中。

        byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", Student.class.getInterfaces());
        String path = "/Users/mapei/Desktop/okay/65707.class";

        try{
            FileOutputStream fos = new FileOutputStream(path);
            fos.write(classFile);
            fos.flush();
            System.out.println("代理類class檔案寫入成功");
        }catch (Exception e) {
            System.out.println("寫檔案錯誤");
        }
複製程式碼

對這個class檔案進行反編譯,我們看看jdk為我們生成了什麼樣的內容:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.Person;

public final class $Proxy0 extends Proxy implements Person
{
  private static Method m1;
  private static Method m2;
  private static Method m3;
  private static Method m0;
  
  /**
  *注意這裡是生成代理類的構造方法,方法引數為InvocationHandler型別,看到這,是不是就有點明白
  *為何代理物件呼叫方法都是執行InvocationHandler中的invoke方法,而InvocationHandler又持有一個
  *被代理物件的例項,就可以去呼叫真正的物件例項。
  */
  public $Proxy0(InvocationHandler paramInvocationHandler)
    throws 
  {
    super(paramInvocationHandler);
  }
  
  //這個靜態塊本來是在最後的,我把它拿到前面來,方便描述
   static
  {
    try
    {
      //看看這兒靜態塊兒裡面的住giveTask通過反射得到的名字m3,其他的先不管
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m3 = Class.forName("proxy.Person").getMethod("giveTask", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException)
    {
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException)
    {
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }
 
  /**
  * 
  *這裡呼叫代理物件的giveMoney方法,直接就呼叫了InvocationHandler中的invoke方法,並把m3傳了進去。
  *this.h.invoke(this, m3, null);我們可以對將InvocationHandler看做一箇中介類,中介類持有一個被代理物件,在invoke方法中呼叫了被代理物件的相應方法。通過聚合方式持有被代理物件的引用,把外部對invoke的呼叫最終都轉為對被代理物件的呼叫。
  */
  public final void giveTask()
    throws 
  {
    try
    {
      this.h.invoke(this, m3, null);
      return;
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

}
複製程式碼

看完了動態代理的原始碼,我們接下來就要看一下Spring中AOP實現的原始碼是怎樣的?

4. 部分原始碼解析

aop建立代理的原始碼分析

  1. 看一下bean如何被包裝為proxy
       	protected Object createProxy(
   		Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {
   		
   	if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
   		AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
   	}

       // 1.建立proxyFactory,proxy的生產主要就是在proxyFactory做的
   	ProxyFactory proxyFactory = new ProxyFactory();
   	proxyFactory.copyFrom(this);

   	if (!proxyFactory.isProxyTargetClass()) {
   		if (shouldProxyTargetClass(beanClass, beanName)) {
   			proxyFactory.setProxyTargetClass(true);
   		}
   		else {
   			evaluateProxyInterfaces(beanClass, proxyFactory);
   		}
   	}

       // 2.將當前bean適合的advice,重新封裝下,封裝為Advisor類,然後新增到ProxyFactory中
   	Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
   	for (Advisor advisor : advisors) {
   		proxyFactory.addAdvisor(advisor);
   	}

   	proxyFactory.setTargetSource(targetSource);
   	customizeProxyFactory(proxyFactory);

   	proxyFactory.setFrozen(this.freezeProxy);
   	if (advisorsPreFiltered()) {
   		proxyFactory.setPreFiltered(true);
   	}

       // 3.呼叫getProxy獲取bean對應的proxy
   	return proxyFactory.getProxy(getProxyClassLoader());
   }
複製程式碼
  1. 建立何種型別的Proxy?JDKProxy還是CGLIBProxy?
	public Object getProxy(ClassLoader classLoader) {
		return createAopProxy().getProxy(classLoader);
	}
    // createAopProxy()方法就是決定究竟建立何種型別的proxy
	protected final synchronized AopProxy createAopProxy() {
		if (!this.active) {
			activate();
		}
        // 關鍵方法createAopProxy()
		return getAopProxyFactory().createAopProxy(this);
	}
	
    // createAopProxy()
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        // 1.config.isOptimize()是否使用優化的代理策略,目前使用與CGLIB
        // config.isProxyTargetClass() 是否目標類本身被代理而不是目標類的介面
        // hasNoUserSuppliedProxyInterfaces()是否存在代理介面
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
            
            // 2.如果目標類是介面類(目標物件實現了介面),則直接使用JDKproxy
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
            
            // 3.其他情況則使用CGLIBproxy
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}
複製程式碼
  1. getProxy()方法
   final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable// JdkDynamicAopProxy類結構,由此可知,其實現了InvocationHandler,則必定有invoke方法,來被呼叫,也就是使用者呼叫bean相關方法時,此invoke()被真正呼叫
   // getProxy()
   public Object getProxy(ClassLoader classLoader) {
   	if (logger.isDebugEnabled()) {
   		logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
   	}
   	Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
   	findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
       
       // JDK proxy 動態代理的標準用法
   	return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
   }
複製程式碼
  1. invoke()方法法
    //使用了JDK動態代理模式,真正的方法執行在invoke()方法裡,看到這裡在想一下上面動態代理的例子,是不是就完全明白Spring原始碼實現動態代理的原理了。
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		MethodInvocation invocation;
		Object oldProxy = null;
		boolean setProxyContext = false;
 
		TargetSource targetSource = this.advised.targetSource;
		Class<?> targetClass = null;
		Object target = null;
 
		try {
            // 1.以下的幾個判斷,主要是為了判斷method是否為equals、hashCode等Object的方法
			if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
				// The target does not implement the equals(Object) method itself.
				return equals(args[0]);
			}
			else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
				// The target does not implement the hashCode() method itself.
				return hashCode();
			}
			else if (method.getDeclaringClass() == DecoratingProxy.class) {
				// There is only getDecoratedClass() declared -> dispatch to proxy config.
				return AopProxyUtils.ultimateTargetClass(this.advised);
			}
			else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
					method.getDeclaringClass().isAssignableFrom(Advised.class)) {
				// Service invocations on ProxyConfig with the proxy config...
				return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
			}
 
			Object retVal;
 
			if (this.advised.exposeProxy) {
				// Make invocation available if necessary.
				oldProxy = AopContext.setCurrentProxy(proxy);
				setProxyContext = true;
			}
 
			// May be null. Get as late as possible to minimize the time we "own" the target,
			// in case it comes from a pool.
			target = targetSource.getTarget();
			if (target != null) {
				targetClass = target.getClass();
			}
			// 2.獲取當前bean被攔截方法連結串列
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
 
			// 3.如果為空,則直接呼叫target的method
			if (chain.isEmpty()) {
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			}
            // 4.不為空,則逐一呼叫chain中的每一個攔截方法的proceed,這裡的一系列執行的原因以及proceed執行的內容,我 在這裡就不詳細講了,大家感興趣可以自己去研讀哈
			else {
				// We need to create a method invocation...
				invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				// Proceed to the joinpoint through the interceptor chain.
				retVal = invocation.proceed();
			}
 
			...
			return retVal;
		}
	}
	}
複製程式碼

那麼到了這裡,我要講的內容就差不多結束了,如果有什麼不對的,或者有什麼疑惑,歡迎大家指點!

相關文章