2.aop 面向切面程式設計
2.1 aop入門
在前面的章節主要講guice的依賴注入,有了依賴注入的基礎後我們再來看guice的aop。我們先從一個例子入手,深入淺出的去了解guice的aop的原理和實作。
首先我們定義服務service,這個服務有一個簡單的方法sayhello,當然了我們有一個服務的預設實作serviceimpl,然後使用@implementedby将服務和預設實作關聯起來,同時将服務的實作标注為單例模式。
1 @implementedby(serviceimpl.class)
2 public interface service {
3 void sayhello();
4 }
在服務的實作serviceimpl中,我們sayhello方法就是輸出一行資訊,這行資訊包含服務的類名,hashcode以及方法名稱和執行的時間。
1 @singleton
2 public class serviceimpl implements service {
3
4 @override
5 @named("log")
6 public void sayhello() {
7 system.out.println(string.format("[%s#%d] execute %s at %d", this.getclass().getsimplename(),hashcode(),"sayhello",system.nanotime()));
8 }
9
10 }
11
接下來定義一個aop的實作。在aopalliance中(大家都認可的aop聯盟)實作我們的方法攔截器。這個攔截器 loggermethodinterceptor 也沒有做什麼特别的事情,隻是記錄些執行的時間,當然了由于執行時間比較短我們用納秒來描述(盡管不是那麼精确)。
在methodinvocation中我們一定要調用proceed()方法,這樣我們的服務才能被執行。當然了如果為了做某些控制我們就能決定是否調用服務代碼了。
1 import static java.lang.system.out;
3 import org.aopalliance.intercept.methodinterceptor;
4 import org.aopalliance.intercept.methodinvocation;
5
6 public class loggermethodinterceptor implements methodinterceptor {
7
8 @override
9 public object invoke(methodinvocation invocation) throws throwable {
10 string methodname = invocation.getmethod().getname();
11 long starttime=system.nanotime();
12 out.println(string.format("before method[%s] at %s", methodname, starttime));
13 object ret = null;
14 try {
15 ret = invocation.proceed();
16 } finally {
17 long endtime=system.nanotime();
18 out.println(string.format(" after method[%s] at %s, cost(ns):%d", methodname, endtime,(endtime-starttime)));
19 }
20 return ret;
21 }
22 }
23
最後才是我們的用戶端程式,注意在這裡我們需要綁定一個攔截器,這個攔截器比對任何類的帶有log注解的方法。是以這就是為什麼我們服務的實作方法需要用log标注的原因了。
1 public class aopdemo {
2 @inject
3 private service service;
4
5 public static void main(string[] args) {
6 injector inj = guice.createinjector(new module() {
7 @override
8 public void configure(binder binder) {
9 binder.bindinterceptor(matchers.any(),//
10 matchers.annotatedwith(names.named("log")),//
11 new loggermethodinterceptor());
12 }
13 });
14 inj.getinstance(aopdemo.class).service.sayhello();
15 inj.getinstance(aopdemo.class).service.sayhello();
16 inj.getinstance(aopdemo.class).service.sayhello();
17 }
18 }
19
我們的程式輸出了我們期望的結果。
before method[sayhello] at 7811306067456
[serviceimpl$$enhancerbyguice$$96717882#33353934] execute sayhello at 7811321912287
after method[sayhello] at 7811322140825, cost(ns):16073369
before method[sayhello] at 7811322315064
[serviceimpl$$enhancerbyguice$$96717882#33353934] execute sayhello at 7811322425280
after method[sayhello] at 7811322561835, cost(ns):246771
before method[sayhello] at 7811322710141
[serviceimpl$$enhancerbyguice$$96717882#33353934] execute sayhello at 7811322817521
after method[sayhello] at 7811322952455, cost(ns):242314
關于此結果有幾點說明。
(1)由于使用了aop我們的服務得到的不再是我們寫的服務實作類了,而是一個繼承的子類,這個子類應該是在記憶體中完成的。
(2)除了第一次調用比較耗時外(可能guice内部做了比較多的處理),其它調用事件為0毫秒(我們的服務本身也沒做什麼事)。
(3)确實完成了我們期待的aop功能。
我們的例子暫且說到這裡,來看看aop的相關概念。
2.2 aop相關概念
老實說aop有一套完整的體系,光是概念就有一大堆,而且都不容易了解。這裡我們結合例子和一些場景來大緻了解下這些概念。
通知(advice)
所謂通知就是我們切面需要完成的功能。比如2.1例子中通知就是記錄方式執行的耗時,這個功能我們就稱之為一個通知。
比 如說在很多系統中我們都會将操作者的操作過程記錄下來,但是這個記錄過程又不想對服務侵入太多,這樣就可以使用aop來完成,而我們記錄日志的這個功能就 是一個通知。通知除了描述切面要完成的工作外還需要描述何時執行這個工作,比如是在方法的之前、之後、之前和之後還是隻在有異常抛出時。
連接配接點(joinpoint)
連 接點描述的是我們的通知在程式執行中的時機,這個時機可以用一個“點”來描述,也就是瞬态。通常我們這個瞬态有以下幾種:方法運作前,方法運作後,抛出異 常時或者讀取修改一個屬性等等。總是我們的通知(功能)就是插入這些點來完成我們額外的功能或者控制我們的執行流程。比如說2.1中的例子,我們的通知 (時間消耗)不僅在方法執行前記錄執行時間,在方法的執行後也輸出了時間的消耗,那麼我們的連接配接點就有兩個,一個是在方法運作前,還有一個是在方法運作 後。
切入點(pointcut)
切 入點描述的是通知的執行範圍。如果通知描述的是“什麼時候”做“什麼事”,連接配接點描述有哪些“時候”,那麼切入點可以了解為“什麼地方”。比如在2.1例 子中我們切入點是所有guice容器管理的服務的帶有@named(“log”)注解的方法。這樣我們的通知就限制在這些地方,這些地方就是所謂的切入 點。
切面(aspect)
切面就是通知和切入點的結合。就是說切面包括通知和切入點兩部分,由此可見我們所說的切面就是通知和切入點。通俗的講就是在什麼時候在什麼地方做什麼事。
引入(introduction)
引入是指允許我們向現有的類添加新的方法和屬性。個人覺得這個特性盡管很強大,但是大部分情況下沒有多大作用,因為如果一個類需要切面來增加新的方法或者屬性的話那麼我們可以有很多更優美的方式繞過此問題,而是在繞不過的時候可能就不是很在乎這個功能了。
目标(target)
目标是被通知的對象,比如我們2.1例子中的serviceimpl 對象。
代理(proxy)
代理是目标對象被通知引用後建立出來新的對象。比如在2.1例子中我們拿到的service對象都不是serviceimpl本身,而是其包裝的子類serviceimpl$$enhancerbyguice$$96717882。
織入(weaving)
所謂織入就是把切面應用到目标對象來建立新的代理對象的過程。通常情況下我們有幾種實際來完成織入過程:
編譯時:就是在java源檔案程式設計成class時完成織入過程。aspectj就存在一個編譯器,運作在編譯時将切面的位元組碼編譯到目标位元組碼中。
類加載時:切面在目标類加載到jvm虛拟機中時織入。由于是在類裝載過程發生的,是以就需要一個特殊的類裝載器(classloader),aspectj就支援這種特性。
運作時:切面在目标類的某個運作時刻被織入。一般情況下aop的容器會建立一個新的代理對象來完成目标對象的功能。事實上在2.1例子中guice就是使用的此方式。
guice支援aop的條件是:
類必須是public或者package (default)
類不能是final類型的
方法必須是public,package或者protected
方法不能使final類型的
執行個體必須通過guice的@inject注入或者有一個無參數的構造函數
2.3 切面注入依賴
如果一個切面(攔截器)也需要注入一些依賴怎麼辦?沒關系,guice允許在關聯切面之前将切面的依賴都注入。比如看下面的例子。
我們有一個前置服務,就是将所有調用的方法名稱輸出。
1 @implementedby(beforeserviceimpl.class)
2 public interface beforeservice {
3
4 void before(methodinvocation invocation);
5 }
6
1 public class beforeserviceimpl implements beforeservice {
2
3 @override
4 public void before(methodinvocation invocation) {
5 system.out.println("before method "+invocation.getmethod().getname());
6 }
7 }
8
然後有一個切面,這個切面依賴前置服務,然後輸出一條方法調用結束語句。
1 public class aftermethodinterceptor implements methodinterceptor {
2 @inject
3 private beforeservice beforeservice;
5 public object invoke(methodinvocation invocation) throws throwable {
6 beforeservice.before(invocation);
7 object ret = null;
8 try {
9 ret = invocation.proceed();
10 } finally {
11 system.out.println("after "+invocation.getmethod().getname());
12 }
13 return ret;
14 }
15 }
在aopdemo2中示範了如何注入切面的依賴。在第9行,aftermethodinterceptor 請求guice注入其依賴。
1 public class aopdemo2 {
4 public static void main(string[] args) {
5 injector inj = guice.createinjector(new module() {
6 @override
7 public void configure(binder binder) {
8 aftermethodinterceptor after= new aftermethodinterceptor();
9 binder.requestinjection(after);
10 binder.bindinterceptor(matchers.any(),//
11 matchers.annotatedwith(names.named("log")),//
12 after);
13 }
14 });
15 aopdemo2 demo=inj.getinstance(aopdemo2.class);
16 demo.service.sayhello();
盡管切面允許注入其依賴,但是這裡需要注意的是,如果切面依賴仍然走切面的話那麼程式就陷入了死循環,很久就會堆溢出。
2.4 matcher
binder綁定一個切面的api是
com.google.inject.binder.bindinterceptor(matcher<? super class<?>>, matcher<? super method>, methodinterceptor...)
第一個參數是比對類,第二個參數是比對方法,第三個數組參數是方法攔截器。也就是說目前為止guice隻能攔截到方法,然後才做一些切面工作。
對于matcher有如下api:
com.google.inject.matcher.matcher.matches(t)
com.google.inject.matcher.matcher.and(matcher<? super t>)
com.google.inject.matcher.matcher.or(matcher<? super t>)
其中第2、3個方法我沒有發現有什麼用,好像guice不适用它們,目前沒有整明白。
對于第一個方法,如果是比對class那麼這裡t就是一個class<?>的類型,如果是比對method就是一個method對象。不好了解吧。看一個例子。
1 public class serviceclassmatcher implements matcher<class<?>>{
2 @override
3 public matcher<class<?>> and(matcher<? super class<?>> other) {
4 return null;
5 }
6 @override
7 public boolean matches(class<?> t) {
8 return t==serviceimpl.class;
9 }
10 @override
11 public matcher<class<?>> or(matcher<? super class<?>> other) {
12 return null;
13 }
14 }
在前面的例子中我們是使用的matchers.any()對象比對所有類而通過标注來識别方法,這裡可以隻比對serviceimpl類來控制服務運作流程。
事實上guice裡面有一個matcher的抽象類com.google.inject.matcher.abstractmatcher<t>,我們隻需要覆寫其中的matches方法即可。
大多數情況下我們隻需要使用matchers提供的預設類即可。matchers中有如下api:
com.google.inject.matcher.matchers.any():任意類或者方法
com.google.inject.matcher.matchers.not(matcher<? super t>):不滿足此條件的類或者方法
com.google.inject.matcher.matchers.annotatedwith(class<? extends annotation>):帶有此注解的類或者方法
com.google.inject.matcher.matchers.annotatedwith(annotation):帶有此注解的類或者方法
com.google.inject.matcher.matchers.subclassesof(class<?>):比對此類的子類型(包括本身類型)
com.google.inject.matcher.matchers.only(object):與指定類型相等的類或者方法(這裡是指equals方法傳回true)
com.google.inject.matcher.matchers.identicalto(object):與指定類型相同的類或者方法(這裡是指同一個對象)
com.google.inject.matcher.matchers.inpackage(package):包相同的類
com.google.inject.matcher.matchers.insubpackage(string):子包中的類(包括此包)
com.google.inject.matcher.matchers.returns(matcher<? super class<?>>):傳回值為指定類型的方法
通常隻需要使用上面的方法或者組合方法就能滿足我們的需求。