一、簡述
1.AOP概念
AOP為
Aspect Oriented Programming
的縮寫,意為:面向切面程式設計,通過預編譯方式和運作期動态代理實作程式功能的統一維護的一種技術。AOP可以對業務邏輯的各個部分進行隔離,進而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。一句話概括:将代碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計。
何為切面?
- 一個和業務沒有任何耦合相關的代碼段,諸如:調用日志,發送郵件,甚至路由分發。一切能為代碼所有且能和代碼充分解耦的代碼都可以作為一個業務代碼的切面。
2.運用場景
項目開發過程中,可能會有這樣的需求,需要我們在方法執行完成後,記錄日志(背景開發中比較常見~),或是計算這個方法的執行時間,在不使用AOP的情況下,我們可以在方法最後調用另一個專門記錄日志的方法,或是在方法體的首尾分别擷取時間,然後通過計算時間差來計算整個方法執行所消耗的時間,這樣也可以完成需求。那如果不隻一個方法要這麼玩怎麼辦?每個方法都寫上一段相同的代碼嗎?後期處理邏輯變了要怎麼辦?其實這種問題我們完全可以用AOP來解決。
3.AOP的實作方式
首先,從切入的時機的角度看,可以分為源碼階段、class階段、dex階段、運作時切入。
對于前三項源碼階段、class階段、dex切入,由于他們都發生在class加載到虛拟機前,我們統稱為靜态切入,而在運作階段發生的改動,我們統稱為動态切入。而具體切入方式可以分為靜态代理和動态代理。
根據Android編譯執行流程來看,不同的時期都有機會可以注入代碼,如下圖所示:
AOP僅僅隻是個概念,實作它的方式(工具和庫)有以下幾種:
切入時機 | 技術架構 |
---|---|
靜态切入 | APT,AspectJ、ASM、Javassit |
動态切入 | jdk動态代理,cglib、Javassit |
- APT:(Annotation Processing Tool )及注解處理器,是一種處理注解的工具,确切的說它是 javac 的一個工具,它用來在編譯時掃描和處理注解。注解處理器以 Java 代碼( 或者編譯過的位元組碼)作為輸入,生成 .java 檔案作為輸出。簡單來說就是在編譯期,通過注解生成 .java 檔案。
- AspectJ:一個 JavaTM 語言的面向切面程式設計的無縫擴充(适用Android)。AspectJ是一個代碼生成架構,它擴充了Java語言,定義了AOP文法,是以它有一個專門的編譯器用來生成遵守Java位元組編碼規範的Class檔案
- Javassist for Android:用于位元組碼操作的知名 java 類庫 Javassist 的 Android 平台移植版。Javassist 是一個編輯位元組碼的架構,作用是修改編譯後的 class 位元組碼。
- ASM:ASM 是一個 Java 位元組碼操控架構。它能被用來動态生成類或者增強既有類的功能。ASM 可以直接産生二進制 class 檔案,也可以在類被加載入 Java 虛拟機之前動态改變類行為。
- ASMDEX:一個類似 ASM 的位元組碼操作庫,運作在Android平台,操作Dex位元組碼。
- DexMaker:Dalvik 虛拟機上,在編譯期或者運作時生成代碼的 Java API。
- jdk動态代理:jdk提供一個Proxy類可以直接給實作接口類的對象直接生成代理對象 API
下面主要介紹一下Android中常用的AspectJ和jdk動态代理實作方式
二、AspectJ
AspectJ 是靜态代理的增強,所謂的靜态代理就是 AOP 架構會在編譯階段生成 AOP 代理類,是以也稱為編譯時增強。
AspectJ 是最早、功能比較強大的 AOP 實作之一,對整套 AOP 機制都有較好的實作,很多其他語言的 AOP 實作,也借鑒或采納了 AspectJ 中很多設計。在 Java 領域,AspectJ 中的很多文法結構基本上已成為 AOP 領域的标準。
1.AspectJ導入
項目gradle添加
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.0'
classpath 'org.aspectj:aspectjtools:1.9.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
主model添加依賴
dependencies {
...
implementation 'org.aspectj:aspectjrt:1.9.4'
}
添加gradle任務,直接粘貼到build.gradle檔案的末尾即可,不要嵌套在别的指令中。
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
final def log = project.logger
final def variants = project.android.applicationVariants
//在建構工程時,執行編輯
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return;
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.9",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
2.AspectJ基礎知識
3.AspectJ實作AOP
3.1 建立自定義注解
用注解來标記切點,一般會使用自定義注解,友善我們拓展。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnoTrace {
String value();
int type();
}
- @Target(ElementType.METHOD):表示該注解隻能注解在方法上。如果想類和方法都可以用,那可以這麼寫:@Target({ElementType.METHOD,ElementType.TYPE}),依此類推。
- @Retention(RetentionPolicy.RUNTIME):表示該注解在程式運作時是可見的(還有SOURCE、CLASS分别指定注解對于那個級别是可見的,一般都是用RUNTIME)。
其中的value和type是自己拓展的屬性,友善存儲一些額外的資訊。
3.2 使用自定義注解标記切點
這個自定義注解隻能注解在方法上(構造方法除外,構造方法也叫構造器,需要使用ElementType.CONSTRUCTOR),像平常使用其它注解一樣使用它即可:
@TestAnnoTrace(value = "test", type = 1)
public void test(View view) {
System.out.println("Hello");
}
3.3 建立切面類
切點表達式的組成如下:
要織入一段代碼到目标類方法的前前後後,必須要有一個切面類,下面就是切面類的代碼:
@Aspect
public class TestAnnoAspect {
//切點表達式使用自定義的注解,一定是@+注解全路徑
@Pointcut("execution(@com.hxl.androidaopdemo.TestAnnoTrace * *(..))")
public void pointcut() {}
@Before("pointcut()")
public void before(JoinPoint point) {
System.out.println("@Before");
}
@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("@Around");
joinPoint.proceed();// 目标方法執行完畢
}
@After("pointcut()")
public void after(JoinPoint point) {
System.out.println("@After");
}
@AfterReturning("pointcut()")
public void afterReturning(JoinPoint point, Object returnValue) {
System.out.println("@AfterReturning");
}
@AfterThrowing(value = "pointcut()", throwing = "ex")
public void afterThrowing(Throwable ex) {
System.out.println("@afterThrowing");
System.out.println("ex = " + ex.getMessage());
}
}
列印結果:
@Before
@Around
Hello
@After
@AfterReturning
少了一個@AfterThrowing通知。這個通知隻有在切點抛出異常時才會執行
3.4 JoinPoint的作用
發現沒有,上面所有的通知都會至少攜帶一個JointPoint參數,這個參數包含了切點的所有資訊,下面就結合按鈕的點選事件方法test()來解釋joinPoint能擷取到的方法資訊有哪些:
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName(); // 方法名:test
Method method = signature.getMethod(); // 方法:public void com.hxl.androidaopdemo.MainActivity.test(android.view.View)
Class returnType = signature.getReturnType(); // 傳回值類型:void
Class declaringType = signature.getDeclaringType(); // 方法所在類名:MainActivity
String[] parameterNames = signature.getParameterNames(); // 參數名:view
Class[] parameterTypes = signature.getParameterTypes(); // 參數類型:View
//上面在編寫自定義注解時就聲明了兩個屬性,分别是value和type,
//通過Method對象得到切點上的注解
TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class);
String value = annotation.value();
int type = annotation.type();
3.5 方法耗時計算的實作
因為@Around是環繞通知,可以在切點的前後分别執行一些操作,AspectJ為了能肯定操作是在切點前還是在切點後,是以在@Around通知中需要手動執行joinPoint.proceed()來确定切點已經執行,故在joinPoint.proceed()之前的代碼會在切點執行前執行,在joinPoint.proceed()之後的代碼會切點執行後執行。于是,方法耗時計算的實作就是這麼簡單:
@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
long beginTime = SystemClock.currentThreadTimeMillis();
joinPoint.proceed();
long endTime = SystemClock.currentThreadTimeMillis();
long dx = endTime - beginTime;
System.out.println("耗時:" + dx + "ms");
}
三、jdk動态代理
動态代理也叫做JDK代理、接口代理。不需要實作目标對象的接口。生成代理對象,使用的是Java的API,動态的在記憶體中構件代理對象(這需要我們指定建立代理對象/目标對象的接口的類型)。
1.實作AOP
在java的動态代理機制中,有兩個重要的類或接口,一個是 InvocationHandler(Interface)、另一個則是Proxy(Class),這一個類和接口是實作我們動态代理所必須用到的。
InvocationHandler:
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
每一個代理實類例的invocation handler都要實作InvocationHandler這個接口。并且每個代理類的執行個體都關聯到了一個handler,當我們通過代理對象調用一個方法的時候,這個方法的調用就會被轉發為由InvocationHandler這個接口的 invoke 方法來進行調用。
- proxy:指代生成的代理對象;
- method:指代的是我們所要調用真實對象的某個方法的Method對象;
- args:指代的是調用真實對象某個方法時接受的參數;
我們來看看Proxy這個類,這個類的作用就是用來動态建立一個代理對象的類,它提供了許多的方法,但用的最多的就是 newProxyInstance 這個方法:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
- loader:一個ClassLoader對象,定義了由哪個ClassLoader對象來對生成的代理對象進行加載
- interfaces:一個Interface對象的數組,表示的是我将要給我需要代理的對象提供一組什麼接口,如果我提供了一組接口給它,那麼這個代理對象就宣稱實作了該接口(多态),這樣我就能調用這組接口中的方法了
- h:一個InvocationHandler對象,表示的是當我這個動态代理對象在調用方法的時候,會關聯到哪一個InvocationHandler對象上。
通過上面的需要傳入接口的參數可以看出,JDK動态代理需要借助接口來實作,如果我們要代理的對象功能沒有抽成任何接口,那麼我們就無法通過JDK動态代理的方式來實作。
好了,在介紹完這兩個接口(類)以後,我們來通過一個執行個體來看看我們的動态代理模式是什麼樣的。首先我們定義了一個Subject類型的接口,為其聲明了兩個方法,這兩個方法表示被代理類需要實作的功能:
public interface Subject {
public void sayGoodBye();
public void sayHello(String str);
}
接着,定義了一個類來實作這個接口,這個類就是我們的真實對象(被代理類),RealSubject類:
public class RealSubject implements Subject {
@Override
public void sayGoodBye() {
System.out.println("RealSubject sayGoodBye");
}
@Override
public void sayHello(String str) {
System.out.println("RealSubject sayHello " + str);
}
}
下一步,我們就要定義一個InvocationHandler了,相當于一個代理處理器。前面說個,每一個動态代理類執行個體的invocation handler 都必須要實作 InvocationHandler 這個接口:
public class SubjectInvocationHandler implements InvocationHandler {
//這個就是我們要代理的真實對象
private Object subject;
//構造方法,給我們要代理的真實對象賦初值
public SubjectInvocationHandler(Object subject) {
this.subject = subject;
}
@Override
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
//在代理真實對象前我們可以添加一些自己的操作
System.out.println("before Method invoke");
System.out.println("Method:" + method);
//當代理對象調用真實對象的方法時,其會自動的跳轉到代理對象關聯的handler對象的invoke方法來進行調用
method.invoke(subject, args);
//在代理真實對象後我們也可以添加一些自己的操作
System.out.println("after Method invoke");
return null;
}
}
**SubjectInvocationHandler并不是真正的代理類,而是用于定義代理類需要擴充、增強那些方法功能的類。**在invoke函數中,對代理對象的所有方法的調用都被轉發至該函數處理。在這裡可以靈活的自定義各種你能想到的邏輯。
最後,來看看我們的Client類:
public static void main(String[] args) {
//被代理類
Subject realSubject = new RealSubject();
//我們要代理哪個類,就将該對象傳進去,最後是通過該被代理對象來調用其方法的
SubjectInvocationHandler handler = new SubjectInvocationHandler(realSubject);
//生成代理類
Subject subject = (Subject) Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
//輸出代理類對象
System.out.println("Proxy : "+ subject.getClass().getName());
System.out.println("Proxy super : "+ subject.getClass().getSuperclass().getName());
System.out.println("Proxy interfaces : "+ subject.getClass().getInterfaces()[0].getName());
//調用代理類sayGoodBye方法
subject.sayGoodBye();
System.out.println("--------");
//調用代理類sayHello方法
subject.sayHello("Test");
}
輸出結果:
Proxy : com.sun.proxy.$Proxy0
Proxy super : java.lang.reflect.Proxy
Proxy interfaces : com.company.ha.Subject
before Method invoke
Method:public abstract void com.company.ha.Subject.sayGoodBye()
RealSubject sayGoodBye
after Method invoke
--------
before Method invoke
Method:public abstract void com.company.ha.Subject.sayHello(java.lang.String)
RealSubject sayHello Test
after Method invoke
如果我們定義的方法有傳回值,那麼可以通過invoke中把該方法的傳回值進行傳回,因為傳回值的對象是Object,是以支援傳回值為空(void)的傳回。
@Override
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
//直接傳回調用對象的傳回值
return method.invoke(subject,args);
}
2.jdk動态代理應用(Retrofit)
Android中的網絡注解架構retrofit内部實作其實就是應用了動态代理技術,通常我們定義網絡接口是這樣的:
public interface ApiStore {
// 員工登入
@FormUrlEncoded
@POST("/resource/d/member/login")
Observable<BaseResponse<LoginResult>> login(@FieldMap Map<String, String> params);
// 登出
@FormUrlEncoded
@POST("/resource/d/member/signOut")
Observable<BaseResponse<LogOutResult>> logout(@FieldMap Map<String, String> params);
//....
}
建立Retrofit:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://ww.xxx.com/")
.build();
ApiStore service = retrofit.create(ApiStore.class);
我們可以看retrofit.create内部具體的實作方式:
public <T> T create(final Class<T> service) {
//...
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
看到沒,retrofit就是通過Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },new InvocationHandler() )的方式建立的一個代理類,invoke中的代碼就是當網絡接口被調用的時候需要做的處理。在這個方法裡,首先根據接口定義的方法,生成一個ServiceMethod對象,在ServiceMethod對象中會反射接口中定義的注解,解析出具體的網絡請求方式,然後拿到封裝好的ServiceMethod對象後,構造一個OkHttpCall對象,以便與進行真正的網絡請求(OkHttp)。
四、動态代理和靜态代理
差別:
- 靜态:由程式員建立代理類或特定工具自動生成源代碼再對其編譯。在程式運作前代理類的.class檔案就已經存在了。
- 動态:在程式運作時運用反射機制動态建立而成。
靜态代理和動态代理的差別和聯系