在上一篇
使用自定義注解實現MVP中Model和View的注入
中,使用了自定義的方式進行依賴注入這一篇我們將繼續對注解進行深入了解。在日常的開發過程中,我們經常會在同一個地方使用到相同的代碼,以往我們的處理方式是可以將其進行一個封裝,然后在
不同的地方進行調用這樣確實也很方便,但是還有另外的方式,就是自定義注解實現AOP。
需求:在開發過程中有很多頁面需要判斷登錄,實現這樣一個功能,能夠在不同需要實現的地方進行登錄的校驗!
AOP
AOP
是Aspect Oriented Program
的首字母縮寫AOP,其意是面向切面編程),其實很多前端的開發可能都沒有聽說過這個,但是對于
后端的小伙伴來說這個是在是太熟悉了,因為很多時候他們就靠這個來進行Log
的打印。
那么AOP
到底是什么呢?
AOP定義
先看定義:運行時,動態地將代碼切入到類的指定方法、指定位置上的編程思想
在解釋AOP
之前,首先得說說和面向切面編程相對的另一個編程思想:面向對象編程(OOP
。在面向對象的思想中,我們以“一切皆對象”為原則,為不同的對象賦予不同的
功能,在需要使用到的時候,我們就對實例化對象,然后調用其功能,這樣降低了代碼的復雜度,使類可重用。
但是在使用的過程中,會出現這么一種情況,類A和類B,都需要進行實現一個功能(比如:是否登錄的判斷),以往我們的做法很簡單,
將這個登錄判斷的功能寫在一個類中(這里命名為C),然后在各自的引用的地方調用這個類的方法,確實這樣是解決了這個問題,但是
這樣卻使A,B 兩個類與C類之間就會有耦合。有沒有什么辦法,能讓我們在需要的時候,隨意地加入代碼呢?
為了解決這樣的問題就出現了面向切面編程的思想,即是:這種在運行時,動態地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程
AOP和OOP之間的關系
AOP的實際操作是將幾個類之間共有的功能單獨出來,然后在這幾個需要的時候進行切入,改變其本來的運行方式。這樣分析下來,我們可以
得出一個結論,即是:面向切面編程(AOP
)其實是面向對象編程(OOP
)的一個補充。
加入AspectJ
AspectJ AspectJ實際上是對AOP編程思想的一個實現。
-
在項目的gradle文件下加入:
dependencies { classpath 'com.android.tools.build:gradle:3.0.0' classpath 'org.aspectj:aspectjtools:1.8.9' classpath 'org.aspectj:aspectjweaver:1.8.9' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }
-
在app的gradle文件下加入:
-
引入aspectjtools
import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main
-
導入第三方包
compile 'org.aspectj:aspectjrt:1.8.9'
-
- 使用AspectJ編譯器ajc
使用ajc會對所有受 aspect 影響的類進行織入,這樣才能使我們的Aspect
//獲取 log實例
final def log = project.logger
//獲取variants
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.8",
"-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;
}
}
}
}
至此,我們就將AspectJ的準備工作做好了,那么接下來就是使用了
在Android中使用AOP
先來介紹幾個概念:
-
Pointcut
:切入點,就是在程序運行過程中,在何處注入我們想運行的特定代碼。
注意:這里的何處,并不是真正意義上的具體位置,而是可切入的范圍,比如整個包下面所有類及所有方法,或者某個類下面的所有方法。 -
Joint point
:連接點,程序中可能作為代碼注入目標的特定的點,所以此處才是執行注入的具體的位置。 -
Advice
: 通知,即是在程序運行過程中,當執行到切點位置時,執行注入到class文件中什么樣的代碼,
比較常用的類型是before
,around
,after
。從字面上面我們就可以看出其意,
就是在目標方法執行之前,執行之時替代目標方法,執行之后的代碼。 -
Aspect
: 切面,其實就是Pointcut
和Advice
的組合,所以如上可以總結為在何處做什么。
創建@CheckLogin注解
可能有人會問:為什么是創建注解呢?不能是其的什么類或者對象么?
AOP
本來就是為了解決耦合才進行使用的,如果使用其他的,或讓AspectJ與其耦合,那我們使用AOP
干什么呢?
@Retention(RetentionPolicy.RUNTIME) //保留到源碼中,同時也保留到class中,最后加載到虛擬機中
@Target({ElementType.METHOD,ElementType.CONSTRUCTOR}) //可以注解在方法或構造上
public @interface CheckLogin {
}
在上次的講解中已經提到元注解@Retention
,表示注解的表示方式,這里再回顧一下:
- SOURCE:只保留在源碼中,不保留在class中,同時也不加載到虛擬機中
- CLASS:保留在源碼中,同時也保留到class中,但是不加載到虛擬機中
- RUNTIME:保留到源碼中,同時也保留到class中,最后加載到虛擬機中
@Target
這個注解表示注解的作用范圍,主要有如下:
- ElementType.FIELD 注解作用于變量
- ElementType.METHOD 注解作用于方法
- ElementType.PARAMETER 注解作用于參數
- ElementType.CONSTRUCTOR 注解作用于構造方法
- ElementType.LOCAL_VARIABLE 注解作用于局部變量
- ElementType.PACKAGE 注解作用于包
所以如上的CheckLogin
表示將注解可以注入到構造方法和其他方法上,并且保留到源碼中,同時也保留到class中,最后加載到虛擬機中。
創建Aspect類
到此,才是我們這章的重點,就是怎么構建一個Aspect
類,這里以CheckLoginAspectJ
為例。
@Aspect
public class CheckLoginAspectJ {
private static final String TAG = "CheckLogin";
/**
* 找到處理的切點
* * *(..) 可以處理CheckLogin這個類所有的方法
*/
@Pointcut("execution(@com.yw.android.aoptest.aop.CheckLogin * *(..))")
public void executionCheckLogin() {
}
/**
* 處理切面
*
* @param joinPoint
* @return
*/
@Around("executionCheckLogin()")
public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
Log.i(TAG, "checkLogin: ");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
CheckLogin checkLogin = signature.getMethod().getAnnotation(CheckLogin.class);
if (checkLogin != null) {
Context context = (Context) joinPoint.getThis();
if (BaseApplication.isLogin) {
Log.i(TAG, "checkLogin: 登錄成功 ");
return joinPoint.proceed();
} else {
Log.i(TAG, "checkLogin: 請登錄");
Toast.makeText(context, "請登錄", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
return null;
}
}
return joinPoint.proceed();
}
@Pointcut說明
在上方代碼Pointcut
之后緊跟了一個execution
的表達式,這個就代表切入點的位置,也就是我們上述的何處
解釋一下execution
的用法:
execution
僅僅是AOP中pointcut expression表達式中的一種。其他還有如下這幾種:
- args():用于匹配當前執行的方法傳入的參數為指定類型的執行方法
- @args():用于匹配當前執行的方法傳入的參數持有指定注解的執行
- execution():用于匹配方法執行的連接點
- this():用于匹配當前AOP代理對象類型的執行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配
- target():用于匹配當前目標對象類型的執行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配
- @target():用于匹配當前目標對象類型的執行方法,其中目標對象持有指定的注解;
- within():用于匹配指定類型內的方法執行
- @within():用于匹配所有持有指定注解類型內的方法;
- @annotation:用于匹配當前執行方法持有指定注解的方法
這里重點解釋一下execution
,因為在我們的日常使用中,execution
是最多的。
類型匹配語法
-
*
:匹配任何數量字符,即是全部; -
..
:匹配任何數量字符的重復,如在類型模式中匹配任何數量子包;而在方法參數模式中匹配任何數量參數。 -
+
:匹配指定類型的子類型;僅能作為后綴放在類型模式后邊。 -
()
:表示方法沒有任何參數 -
(..)
:表示匹配接受任意個參數的方法
//匹配String類型
java.lang.String
//匹配java包下任何子包的String類型
java.*.String
//匹配java包及任何子包下的任何類型
java..*
execution表達式
execution的表達式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
- modifiers-pattern:修飾符匹配,如
public
、private
、protect
,可選。 - ret-type-pattern:返回類型匹配,必填。
- declaring-type-pattern:聲明類型匹配,可選。
- name-pattern(param-pattern):
- name-pattern:方法名匹配,必填
- param-pattern:方法參數匹配,必填
- throws-pattern:異常匹配,可選。
至此,我們可以知道,上述中代碼代表的匹配意思了
"execution(@com.yw.android.aoptest.aop.CheckLogin * *(..))"
返回類型:com.yw.android.aoptest.aop.CheckLogin
;
聲明類型: * ,表示任何
方法名: *
,任何方法
參數:(..)
,任意個參數
即是:匹配com.yw.android.aoptest.aop.CheckLogin
類下的所有聲明和所以任意參數方法。
@Advice說明
@Around("executionCheckLogin()")
public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
在上述代碼中我們使用的是@Around
,這個也是很常用的。
@Around("executionCheckLogin()")
將切面表達式與通知進行綁定,使用我們的代碼注入在使用@CheckLogin
的地方生效
,其中參數是上面切面的方法名。
而在方法中參數就是JoinPoint
,常用的也就是這個ProceedingJoinPoint
。
JoinPoint
public interface JoinPoint {
String toString(); //連接點所在位置的相關信息
String toShortString(); //連接點所在位置的簡短相關信息
String toLongString(); //連接點所在位置的全部相關信息
Object getThis(); //返回AOP代理對象
Object getTarget(); //返回目標對象
Object[] getArgs(); //返回被通知方法參數列表
Signature getSignature(); //返回當前連接點簽名
SourceLocation getSourceLocation();//返回連接點方法所在類文件中的位置
String getKind(); //連接點類型
StaticPart getStaticPart(); //返回連接點靜態部分
}
ProceedingJoinPoint
ProceedingJoinPoint
繼承了JoinPoint
public interface ProceedingJoinPoint extends JoinPoint {
public Object proceed() throws Throwable;
public Object proceed(Object[] args) throws Throwable;
}
使用proceed()方法來執行目標方法,即是被@CheckLogin
注解的方法,我們再來看看我們的方法
@Around("executionCheckLogin()")
public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
Log.i(TAG, "checkLogin: ");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
CheckLogin checkLogin = signature.getMethod().getAnnotation(CheckLogin.class);
if (checkLogin != null) {
Context context = (Context) joinPoint.getThis();
if (BaseApplication.isLogin) {
Log.i(TAG, "checkLogin: 登錄成功 ");
return joinPoint.proceed();
} else {
Log.i(TAG, "checkLogin: 請登錄");
Toast.makeText(context, "請登錄", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
return null;
}
}
return joinPoint.proceed();
}
- 先獲取一個方法前面對象
MethodSignature
,這個對象有兩個方法:
public interface MethodSignature extends CodeSignature {
Class getReturnType(); /* name is consistent with reflection API */
Method getMethod();
}
一個是獲取目標方法的返回類型,一個是目標方法的Methond對象。
然后通過:
signature.getMethod().getAnnotation(CheckLogin.class);
就可以獲取目標方法的注解,如果注解實例不為空,說明加了CheckLogin
注解。
Context context = (Context) joinPoint.getThis();
通過上述方法,可以獲取目標方法所在類的對象,但是這里強轉成了Context,也就是說,改注解只能在有上下文的類里使用。
然后通過登錄的標志進行判斷,是讓目標方法繼續執行,還是跳轉至登錄。
簡單測試
private Button btnAop;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnAop = (Button) findViewById(R.id.btn_aop);
btnAop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onAop();
}
});
}
@CheckLogin
public void onAop(){
Log.d("tag","執行方法參數");
}
- 設置登錄標志為未登錄:
I/CheckLogin: checkLogin:
I/CheckLogin: checkLogin: 請登錄
檢測出未登錄,跳轉到了登錄界面
- 設置登錄標志為已登錄:
I/CheckLogin: checkLogin:
I/CheckLogin: checkLogin: 登錄成功
D/tag: 執行方法參數
檢測出已登錄,執行目標方法。
總結
AOP
的使用不光在檢測登錄,還有其他的一些用處:
- 打印日志,在需要打印日志的地方加上這樣的方式,就可以打印日志,是不是比寫一個打印方法簡單多了
- 緩存,假設目標方法是個數據請求,那么是不是可以在目標方法執行之后,進行緩存
- 數據校驗,我們的代碼中很多地方都會去校驗數據,那么自定義一個AOP,然后傳入你需要注解的對象進行校驗。
這樣的方式應該還有很多,只是現在還沒有用到,希望大家可以多多提出自己的想法。
查看項目,請戳這里