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;
2
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;
4 @Override
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 {
2 @Inject
3 private Service service;
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();
17 }
18 }
盡管切面允許注入其依賴,但是這里需要注意的是,如果切面依賴仍然走切面的話那么程序就陷入了死循環,很久就會堆溢出。
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<?>>):返回值為指定類型的方法
通常只需要使用上面的方法或者組合方法就能滿足我們的需求。
通過上面的學習可以看出,Guice的AOP還是很弱的,目前僅僅支持方法級別上的,另外靈活性也不是很高。
上一篇:Google Guice 入門教程04 - 依賴注入(04)
下一篇:Google Guice 入門教程06 – Web 和 Servlet
©2009-2014 IMXYLZ
|求賢若渴