在上一篇
使用自定義注解實(shí)現(xiàn)MVP中Model和View的注入
中畏浆,使用了自定義的方式進(jìn)行依賴注入這一篇我們將繼續(xù)對(duì)注解進(jìn)行深入了解税稼。在日常的開發(fā)過程中烦秩,我們經(jīng)常會(huì)在同一個(gè)地方使用到相同的代碼,以往我們的處理方式是可以將其進(jìn)行一個(gè)封裝郎仆,然后在
不同的地方進(jìn)行調(diào)用這樣確實(shí)也很方便只祠,但是還有另外的方式,就是自定義注解實(shí)現(xiàn)AOP扰肌。
需求:在開發(fā)過程中有很多頁(yè)面需要判斷登錄抛寝,實(shí)現(xiàn)這樣一個(gè)功能,能夠在不同需要實(shí)現(xiàn)的地方進(jìn)行登錄的校驗(yàn)曙旭!
AOP
AOP
是Aspect Oriented Program
的首字母縮寫AOP盗舰,其意是面向切面編程),其實(shí)很多前端的開發(fā)可能都沒有聽說過這個(gè)桂躏,但是對(duì)于
后端的小伙伴來說這個(gè)是在是太熟悉了钻趋,因?yàn)楹芏鄷r(shí)候他們就靠這個(gè)來進(jìn)行Log
的打印。
那么AOP
到底是什么呢?
AOP定義
先看定義:運(yùn)行時(shí)沼头,動(dòng)態(tài)地將代碼切入到類的指定方法爷绘、指定位置上的編程思想
在解釋AOP
之前书劝,首先得說說和面向切面編程相對(duì)的另一個(gè)編程思想:面向?qū)ο缶幊蹋?code>OOP。在面向?qū)ο蟮乃枷胫型林粒覀円浴耙磺薪詫?duì)象”為原則购对,為不同的對(duì)象賦予不同的
功能,在需要使用到的時(shí)候陶因,我們就對(duì)實(shí)例化對(duì)象骡苞,然后調(diào)用其功能,這樣降低了代碼的復(fù)雜度楷扬,使類可重用解幽。
但是在使用的過程中,會(huì)出現(xiàn)這么一種情況烘苹,類A和類B躲株,都需要進(jìn)行實(shí)現(xiàn)一個(gè)功能(比如:是否登錄的判斷),以往我們的做法很簡(jiǎn)單镣衡,
將這個(gè)登錄判斷的功能寫在一個(gè)類中(這里命名為C)霜定,然后在各自的引用的地方調(diào)用這個(gè)類的方法,確實(shí)這樣是解決了這個(gè)問題廊鸥,但是
這樣卻使A,B 兩個(gè)類與C類之間就會(huì)有耦合望浩。有沒有什么辦法,能讓我們?cè)谛枰臅r(shí)候惰说,隨意地加入代碼呢磨德?
為了解決這樣的問題就出現(xiàn)了面向切面編程的思想,即是:這種在運(yùn)行時(shí)吆视,動(dòng)態(tài)地將代碼切入到類的指定方法典挑、指定位置上的編程思想就是面向切面的編程
AOP和OOP之間的關(guān)系
AOP的實(shí)際操作是將幾個(gè)類之間共有的功能單獨(dú)出來,然后在這幾個(gè)需要的時(shí)候進(jìn)行切入揩环,改變其本來的運(yùn)行方式搔弄。這樣分析下來,我們可以
得出一個(gè)結(jié)論丰滑,即是:面向切面編程(AOP
)其實(shí)是面向?qū)ο缶幊蹋?code>OOP)的一個(gè)補(bǔ)充。
加入AspectJ
AspectJ AspectJ實(shí)際上是對(duì)AOP編程思想的一個(gè)實(shí)現(xiàn)倒庵。
-
在項(xiàng)目的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
-
導(dǎo)入第三方包
compile 'org.aspectj:aspectjrt:1.8.9'
-
- 使用AspectJ編譯器ajc
使用ajc會(huì)對(duì)所有受 aspect 影響的類進(jìn)行織入褒墨,這樣才能使我們的Aspect
//獲取 log實(shí)例
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;
}
//編譯時(shí)做如下處理
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的準(zhǔn)備工作做好了擎宝,那么接下來就是使用了
在Android中使用AOP
先來介紹幾個(gè)概念:
-
Pointcut
:切入點(diǎn)郁妈,就是在程序運(yùn)行過程中,在何處注入我們想運(yùn)行的特定代碼绍申。
注意:這里的何處噩咪,并不是真正意義上的具體位置顾彰,而是可切入的范圍,比如整個(gè)包下面所有類及所有方法胃碾,或者某個(gè)類下面的所有方法涨享。 -
Joint point
:連接點(diǎn),程序中可能作為代碼注入目標(biāo)的特定的點(diǎn)仆百,所以此處才是執(zhí)行注入的具體的位置厕隧。 -
Advice
: 通知,即是在程序運(yùn)行過程中俄周,當(dāng)執(zhí)行到切點(diǎn)位置時(shí)吁讨,執(zhí)行注入到class文件中什么樣的代碼,
比較常用的類型是before
峦朗,around
建丧,after
。從字面上面我們就可以看出其意波势,
就是在目標(biāo)方法執(zhí)行之前茶鹃,執(zhí)行之時(shí)替代目標(biāo)方法,執(zhí)行之后的代碼艰亮。 -
Aspect
: 切面闭翩,其實(shí)就是Pointcut
和Advice
的組合,所以如上可以總結(jié)為在何處做什么迄埃。
創(chuàng)建@CheckLogin注解
可能有人會(huì)問:為什么是創(chuàng)建注解呢疗韵?不能是其的什么類或者對(duì)象么?
AOP
本來就是為了解決耦合才進(jìn)行使用的侄非,如果使用其他的蕉汪,或讓AspectJ與其耦合,那我們使用AOP
干什么呢逞怨?
@Retention(RetentionPolicy.RUNTIME) //保留到源碼中者疤,同時(shí)也保留到class中,最后加載到虛擬機(jī)中
@Target({ElementType.METHOD,ElementType.CONSTRUCTOR}) //可以注解在方法或構(gòu)造上
public @interface CheckLogin {
}
在上次的講解中已經(jīng)提到元注解@Retention
,表示注解的表示方式叠赦,這里再回顧一下:
- SOURCE:只保留在源碼中驹马,不保留在class中,同時(shí)也不加載到虛擬機(jī)中
- CLASS:保留在源碼中除秀,同時(shí)也保留到class中糯累,但是不加載到虛擬機(jī)中
- RUNTIME:保留到源碼中,同時(shí)也保留到class中册踩,最后加載到虛擬機(jī)中
@Target
這個(gè)注解表示注解的作用范圍泳姐,主要有如下:
- ElementType.FIELD 注解作用于變量
- ElementType.METHOD 注解作用于方法
- ElementType.PARAMETER 注解作用于參數(shù)
- ElementType.CONSTRUCTOR 注解作用于構(gòu)造方法
- ElementType.LOCAL_VARIABLE 注解作用于局部變量
- ElementType.PACKAGE 注解作用于包
所以如上的CheckLogin
表示將注解可以注入到構(gòu)造方法和其他方法上,并且保留到源碼中暂吉,同時(shí)也保留到class中胖秒,最后加載到虛擬機(jī)中缎患。
創(chuàng)建Aspect類
到此,才是我們這章的重點(diǎn)阎肝,就是怎么構(gòu)建一個(gè)Aspect
類,這里以CheckLoginAspectJ
為例挤渔。
@Aspect
public class CheckLoginAspectJ {
private static final String TAG = "CheckLogin";
/**
* 找到處理的切點(diǎn)
* * *(..) 可以處理CheckLogin這個(gè)類所有的方法
*/
@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: 請(qǐng)登錄");
Toast.makeText(context, "請(qǐng)登錄", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
return null;
}
}
return joinPoint.proceed();
}
@Pointcut說明
在上方代碼Pointcut
之后緊跟了一個(gè)execution
的表達(dá)式,這個(gè)就代表切入點(diǎn)的位置盗痒,也就是我們上述的何處
解釋一下execution
的用法:
execution
僅僅是AOP中pointcut expression表達(dá)式中的一種蚂蕴。其他還有如下這幾種:
- args():用于匹配當(dāng)前執(zhí)行的方法傳入的參數(shù)為指定類型的執(zhí)行方法
- @args():用于匹配當(dāng)前執(zhí)行的方法傳入的參數(shù)持有指定注解的執(zhí)行
- execution():用于匹配方法執(zhí)行的連接點(diǎn)
- this():用于匹配當(dāng)前AOP代理對(duì)象類型的執(zhí)行方法;注意是AOP代理對(duì)象的類型匹配俯邓,這樣就可能包括引入接口也類型匹配
- target():用于匹配當(dāng)前目標(biāo)對(duì)象類型的執(zhí)行方法骡楼;注意是目標(biāo)對(duì)象的類型匹配,這樣就不包括引入接口也類型匹配
- @target():用于匹配當(dāng)前目標(biāo)對(duì)象類型的執(zhí)行方法稽鞭,其中目標(biāo)對(duì)象持有指定的注解鸟整;
- within():用于匹配指定類型內(nèi)的方法執(zhí)行
- @within():用于匹配所有持有指定注解類型內(nèi)的方法;
- @annotation:用于匹配當(dāng)前執(zhí)行方法持有指定注解的方法
這里重點(diǎn)解釋一下execution
,因?yàn)樵谖覀兊娜粘J褂弥校?code>execution是最多的朦蕴。
類型匹配語(yǔ)法
-
*
:匹配任何數(shù)量字符,即是全部篮条; -
..
:匹配任何數(shù)量字符的重復(fù),如在類型模式中匹配任何數(shù)量子包吩抓;而在方法參數(shù)模式中匹配任何數(shù)量參數(shù)涉茧。 -
+
:匹配指定類型的子類型;僅能作為后綴放在類型模式后邊疹娶。 -
()
:表示方法沒有任何參數(shù) -
(..)
:表示匹配接受任意個(gè)參數(shù)的方法
//匹配String類型
java.lang.String
//匹配java包下任何子包的String類型
java.*.String
//匹配java包及任何子包下的任何類型
java..*
execution表達(dá)式
execution的表達(dá)式如下:
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:方法參數(shù)匹配,必填
- throws-pattern:異常匹配叹哭,可選忍宋。
至此,我們可以知道风罩,上述中代碼代表的匹配意思了
"execution(@com.yw.android.aoptest.aop.CheckLogin * *(..))"
返回類型:com.yw.android.aoptest.aop.CheckLogin
;
聲明類型: * ,表示任何
方法名: *
,任何方法
參數(shù):(..)
,任意個(gè)參數(shù)
即是:匹配com.yw.android.aoptest.aop.CheckLogin
類下的所有聲明和所以任意參數(shù)方法。
@Advice說明
@Around("executionCheckLogin()")
public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
在上述代碼中我們使用的是@Around
,這個(gè)也是很常用的舵稠。
@Around("executionCheckLogin()")
將切面表達(dá)式與通知進(jìn)行綁定超升,使用我們的代碼注入在使用@CheckLogin
的地方生效
,其中參數(shù)是上面切面的方法名入宦。
而在方法中參數(shù)就是JoinPoint
,常用的也就是這個(gè)ProceedingJoinPoint
。
JoinPoint
public interface JoinPoint {
String toString(); //連接點(diǎn)所在位置的相關(guān)信息
String toShortString(); //連接點(diǎn)所在位置的簡(jiǎn)短相關(guān)信息
String toLongString(); //連接點(diǎn)所在位置的全部相關(guān)信息
Object getThis(); //返回AOP代理對(duì)象
Object getTarget(); //返回目標(biāo)對(duì)象
Object[] getArgs(); //返回被通知方法參數(shù)列表
Signature getSignature(); //返回當(dāng)前連接點(diǎn)簽名
SourceLocation getSourceLocation();//返回連接點(diǎn)方法所在類文件中的位置
String getKind(); //連接點(diǎn)類型
StaticPart getStaticPart(); //返回連接點(diǎn)靜態(tài)部分
}
ProceedingJoinPoint
ProceedingJoinPoint
繼承了JoinPoint
public interface ProceedingJoinPoint extends JoinPoint {
public Object proceed() throws Throwable;
public Object proceed(Object[] args) throws Throwable;
}
使用proceed()方法來執(zhí)行目標(biāo)方法,即是被@CheckLogin
注解的方法,我們?cè)賮砜纯次覀兊姆椒?/p>
@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: 請(qǐng)登錄");
Toast.makeText(context, "請(qǐng)登錄", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
return null;
}
}
return joinPoint.proceed();
}
- 先獲取一個(gè)方法前面對(duì)象
MethodSignature
室琢,這個(gè)對(duì)象有兩個(gè)方法:
public interface MethodSignature extends CodeSignature {
Class getReturnType(); /* name is consistent with reflection API */
Method getMethod();
}
一個(gè)是獲取目標(biāo)方法的返回類型乾闰,一個(gè)是目標(biāo)方法的Methond對(duì)象。
然后通過:
signature.getMethod().getAnnotation(CheckLogin.class);
就可以獲取目標(biāo)方法的注解盈滴,如果注解實(shí)例不為空涯肩,說明加了CheckLogin
注解。
Context context = (Context) joinPoint.getThis();
通過上述方法巢钓,可以獲取目標(biāo)方法所在類的對(duì)象病苗,但是這里強(qiáng)轉(zhuǎn)成了Context,也就是說症汹,改注解只能在有上下文的類里使用硫朦。
然后通過登錄的標(biāo)志進(jìn)行判斷,是讓目標(biāo)方法繼續(xù)執(zhí)行背镇,還是跳轉(zhuǎn)至登錄咬展。
簡(jiǎn)單測(cè)試
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","執(zhí)行方法參數(shù)");
}
- 設(shè)置登錄標(biāo)志為未登錄:
I/CheckLogin: checkLogin:
I/CheckLogin: checkLogin: 請(qǐng)登錄
檢測(cè)出未登錄,跳轉(zhuǎn)到了登錄界面
- 設(shè)置登錄標(biāo)志為已登錄:
I/CheckLogin: checkLogin:
I/CheckLogin: checkLogin: 登錄成功
D/tag: 執(zhí)行方法參數(shù)
檢測(cè)出已登錄瞒斩,執(zhí)行目標(biāo)方法破婆。
總結(jié)
AOP
的使用不光在檢測(cè)登錄,還有其他的一些用處:
- 打印日志胸囱,在需要打印日志的地方加上這樣的方式沃粗,就可以打印日志壁榕,是不是比寫一個(gè)打印方法簡(jiǎn)單多了
- 緩存,假設(shè)目標(biāo)方法是個(gè)數(shù)據(jù)請(qǐng)求,那么是不是可以在目標(biāo)方法執(zhí)行之后殃姓,進(jìn)行緩存
- 數(shù)據(jù)校驗(yàn),我們的代碼中很多地方都會(huì)去校驗(yàn)數(shù)據(jù)对供,那么自定義一個(gè)AOP逾苫,然后傳入你需要注解的對(duì)象進(jìn)行校驗(yàn)。
這樣的方式應(yīng)該還有很多柬帕,只是現(xiàn)在還沒有用到哟忍,希望大家可以多多提出自己的想法。
查看項(xiàng)目陷寝,請(qǐng)戳這里