本篇文章給大家分享的是有關(guān)Android中怎么實(shí)現(xiàn)面向切面編程,小編覺得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
創(chuàng)新互聯(lián)建站擁有十多年成都網(wǎng)站建設(shè)工作經(jīng)驗(yàn),為各大企業(yè)提供網(wǎng)站設(shè)計(jì)、網(wǎng)站建設(shè)服務(wù),對于網(wǎng)頁設(shè)計(jì)、PC網(wǎng)站建設(shè)(電腦版網(wǎng)站建設(shè))、app軟件開發(fā)公司、wap網(wǎng)站建設(shè)(手機(jī)版網(wǎng)站建設(shè))、程序開發(fā)、網(wǎng)站優(yōu)化(SEO優(yōu)化)、微網(wǎng)站、空間域名等,憑借多年來在互聯(lián)網(wǎng)的打拼,我們在互聯(lián)網(wǎng)網(wǎng)站建設(shè)行業(yè)積累了很多網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、網(wǎng)絡(luò)營銷經(jīng)驗(yàn),集策劃、開發(fā)、設(shè)計(jì)、營銷、管理等網(wǎng)站化運(yùn)作于一體,具備承接各種規(guī)模類型的網(wǎng)站建設(shè)項(xiàng)目的能力。
1、AOP的概念
如果你用java做過后臺(tái)開發(fā),那么你一定知道AOP這個(gè)概念。如果不知道也無妨,套用百度百科的介紹,也能讓你明白這玩意是干什么的:
AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù),是軟件開發(fā)中的一個(gè)熱點(diǎn),也是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。
2、項(xiàng)目場景
項(xiàng)目開發(fā)過程中,可能會(huì)有這樣的需求,需要我們在方法執(zhí)行完成后,記錄日志(后臺(tái)開發(fā)中比較常見~),或是計(jì)算這個(gè)方法的執(zhí)行時(shí)間,在不使用AOP的情況下,我們可以在方法最后調(diào)用另一個(gè)專門記錄日志的方法,或是在方法體的首尾分別獲取時(shí)間,然后通過計(jì)算時(shí)間差來計(jì)算整個(gè)方法執(zhí)行所消耗的時(shí)間,這樣也可以完成需求。那如果不只一個(gè)方法要這么玩怎么辦?每個(gè)方法都寫上一段相同的代碼嗎?后期處理邏輯變了要怎么辦?最后老板說這功能不要了我們還得一個(gè)個(gè)刪除?
很明顯,這是不可能的,我們不僅僅是代碼的搬運(yùn)工,我們還是有思考能力的軟件開發(fā)工程師。這么low的做法絕對不干,這種問題我們完全可以用AOP來解決,不就是在方法前和方法后插入一段代碼嗎?AOP分分鐘搞定。
3、AOP的實(shí)現(xiàn)方式
要注意了,AOP僅僅只是個(gè)概念,實(shí)現(xiàn)它的方式(工具和庫)有以下幾種:
AspectJ: 一個(gè) JavaTM 語言的面向切面編程的無縫擴(kuò)展(適用Android)。
Javassist for Android: 用于字節(jié)碼操作的知名 java 類庫 Javassist 的 Android 平臺(tái)移植版。
DexMaker: Dalvik 虛擬機(jī)上,在編譯期或者運(yùn)行時(shí)生成代碼的 Java API。
ASMDEX: 一個(gè)類似 ASM 的字節(jié)碼操作庫,運(yùn)行在Android平臺(tái),操作Dex字節(jié)碼。
本篇的主角就是AspectJ,下面就來看看AspectJ方式的AOP如何在Android開發(fā)中進(jìn)行使用吧。
二、AspectJ的引入
對于eclipse與Android Studio的引入是不一樣的,本篇只介紹Android Studio如何引入AspectJ,eclipse請自行百度。Android Studio需要在app模塊的build.gradle文件中引入,總共分為3個(gè)步驟:
1)添加核心依賴
dependencies { ... compile 'org.aspectj:aspectjrt:1.8.9' }
2)編寫gradle編譯腳本
buildscript { repositories { mavenCentral() } dependencies { classpath 'org.aspectj:aspectjtools:1.8.9' classpath 'org.aspectj:aspectjweaver:1.8.9' } }
AspectJ需要依賴maven倉庫。
3)添加gradle任務(wù)
dependencies { ... } // 貼上面那段沒用的代碼是為了說明:下面的任務(wù)代碼與dependencies同級 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.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; } } } }
直接粘貼到build.gradle文件的末尾即可,不要嵌套在別的指令中。
三、AOP的基本知識
在使用AspectJ之前,還是需要先介紹下AOP的基本知識,熟悉的看官可以跳過這部分。
1、AOP術(shù)語
通知、增強(qiáng)處理(Advice):就是你想要的功能,也就是上面說的日志、耗時(shí)計(jì)算等。
連接點(diǎn)(JoinPoint):允許你通知(Advice)的地方,那可就真多了,基本每個(gè)方法的前、后(兩者都有也行),或拋出異常是時(shí)都可以是連接點(diǎn)(spring只支持方法連接點(diǎn))。AspectJ還可以讓你在構(gòu)造器或?qū)傩宰⑷霑r(shí)都行,不過一般情況下不會(huì)這么做,只要記住,和方法有關(guān)的前前后后都是連接點(diǎn)。
切入點(diǎn)(Pointcut):上面說的連接點(diǎn)的基礎(chǔ)上,來定義切入點(diǎn),你的一個(gè)類里,有15個(gè)方法,那就有十幾個(gè)連接點(diǎn)了對吧,但是你并不想在所有方法附件都使用通知(使用叫織入,下面再說),你只是想讓其中幾個(gè),在調(diào)用這幾個(gè)方法之前、之后或者拋出異常時(shí)干點(diǎn)什么,那么就用切入點(diǎn)來定義這幾個(gè)方法,讓切點(diǎn)來篩選連接點(diǎn),選中那幾個(gè)你想要的方法。
切面(Aspect):切面是通知和切入點(diǎn)的結(jié)合?,F(xiàn)在發(fā)現(xiàn)了吧,沒連接點(diǎn)什么事,連接點(diǎn)就是為了讓你好理解切點(diǎn)搞出來的,明白這個(gè)概念就行了。通知說明了干什么和什么時(shí)候干(什么時(shí)候通過before,after,around等AOP注解就能知道),而切入點(diǎn)說明了在哪干(指定到底是哪個(gè)方法),這就是一個(gè)完整的切面定義。
織入(weaving) 把切面應(yīng)用到目標(biāo)對象來創(chuàng)建新的代理對象的過程。
引入(introduction) 允許我們向現(xiàn)有的類添加新方法屬性。這不就是把切面(也就是新方法屬性:通知定義的)用到目標(biāo)類中嗎
目標(biāo)(target) 引入中所提到的目標(biāo)類,也就是要被通知的對象,也就是真正的業(yè)務(wù)邏輯,他可以在毫不知情的情況下,被咋們織入切面。二自己專注于業(yè)務(wù)本身的邏輯。
代理(proxy) 怎么實(shí)現(xiàn)整套AOP機(jī)制的,都是通過代理,這個(gè)一會(huì)兒給細(xì)說。
目標(biāo)對象 – 項(xiàng)目原始的Java組件。
AOP代理 – 由AOP框架生成java對象。
AOP代理方法 = advice + 目標(biāo)對象的方法。
2、AOP注解與使用
@Aspect:聲明切面,標(biāo)記類
@Pointcut(切點(diǎn)表達(dá)式):定義切點(diǎn),標(biāo)記方法
@Before(切點(diǎn)表達(dá)式):前置通知,切點(diǎn)之前執(zhí)行
@Around(切點(diǎn)表達(dá)式):環(huán)繞通知,切點(diǎn)前后執(zhí)行
@After(切點(diǎn)表達(dá)式):后置通知,切點(diǎn)之后執(zhí)行
@AfterReturning(切點(diǎn)表達(dá)式):返回通知,切點(diǎn)方法返回結(jié)果之后執(zhí)行
@AfterThrowing(切點(diǎn)表達(dá)式):異常通知,切點(diǎn)拋出異常時(shí)執(zhí)行
@Pointcut、@Before、@Around、@After、@AfterReturning、@AfterThrowing需要在切面類中使用,即在使用@Aspect的類中。
1)切點(diǎn)表達(dá)式是什么?
這就是切點(diǎn)表達(dá)式:execution (* com.lqr..*.*(..))。切點(diǎn)表達(dá)式的組成如下:
execution(<修飾符模式>? <返回類型模式> <方法名模式>(<參數(shù)模式>) <異常模式>?)
除了返回類型模式、方法名模式和參數(shù)模式外,其它項(xiàng)都是可選的。
修飾符模式指的是public、private、protected,異常模式指的是NullPointException等。
對于切點(diǎn)表達(dá)式的理解不是本篇重點(diǎn),下面列出幾個(gè)例子說明一下就好了:
@Before("execution(public * *(..))") public void before(JoinPoint point) { System.out.println("CSDN_LQR"); }
匹配所有public方法,在方法執(zhí)行之前打印"CSDN_LQR"。
@Around("execution(* *to(..))") public void around(ProceedingJoinPoint joinPoint) { System.out.println("CSDN"); joinPoint.proceed(); System.out.println("LQR"); }
匹配所有以"to"結(jié)尾的方法,在方法執(zhí)行之前打印"CSDN",在方法執(zhí)行之后打印"LQR"。
@After("execution(* com.lqr..*to(..))") public void after(JoinPoint point) { System.out.println("CSDN_LQR"); }
匹配com.lqr包下及其子包中以"to"結(jié)尾的方法,在方法執(zhí)行之后打印"CSDN_LQR"。
@AfterReturning("execution(int com.lqr.*(..))") public void afterReturning(JoinPoint point, Object returnValue) { System.out.println("CSDN_LQR"); }
匹配com.lqr包下所有返回類型是int的方法,在方法返回結(jié)果之后打印"CSDN_LQR"。
@AfterThrowing(value = "execution(* com.lqr..*(..))", throwing = "ex") public void afterThrowing(Throwable ex) { System.out.println("ex = " + ex.getMessage()); }
匹配com.lqr包及其子包中的所有方法,當(dāng)方法拋出異常時(shí),打印"ex = 報(bào)錯(cuò)信息"。
2)@Pointcut的使用
@Pointcut是專門用來定義切點(diǎn)的,讓切點(diǎn)表達(dá)式可以復(fù)用。
你可能需要在切點(diǎn)執(zhí)行之前和切點(diǎn)報(bào)出異常時(shí)做些動(dòng)作(如:出錯(cuò)時(shí)記錄日志),可以這么做:
@Before("execution(* com.lqr..*(..))") public void before(JoinPoint point) { System.out.println("CSDN_LQR"); } @AfterThrowing(value = "execution(* com.lqr..*(..))", throwing = "ex") public void afterThrowing(Throwable ex) { System.out.println("記錄日志"); }
可以看到,表達(dá)式是一樣的,那要怎么重用這個(gè)表達(dá)式呢?這就需要用到@Pointcut注解了,@Pointcut注解是注解在一個(gè)空方法上的,如:
@Pointcut("execution(* com.lqr..*(..))") public void pointcut() {}
這時(shí),"pointcut()"就等價(jià)于"execution(* com.lqr..*(..))",那么上面的代碼就可以這么改了:
@Before("pointcut()") public void before(JoinPoint point) { System.out.println("CSDN_LQR"); } @AfterThrowing(value = "pointcut()", throwing = "ex") public void afterThrowing(Throwable ex) { System.out.println("記錄日志"); }
四、實(shí)戰(zhàn)
經(jīng)過上面的學(xué)習(xí),下面是時(shí)候?qū)崙?zhàn)一下了,這里我們來一個(gè)簡單的例子。
1、切點(diǎn)
這是界面上一個(gè)按鈕的點(diǎn)擊事件,就是一個(gè)簡單的方法而已,我們拿它來試刀。
public void test(View view) { System.out.println("Hello, I am CSDN_LQR"); }
2、切面類
要織入一段代碼到目標(biāo)類方法的前前后后,必須要有一個(gè)切面類,下面就是切面類的代碼:
@Aspect public class TestAnnoAspect { @Pointcut("execution(* com.lqr.androidaopdemo.MainActivity.test(..))") 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"); } @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()); } }
3、各通知的執(zhí)行結(jié)果
先來試試看,這幾個(gè)注解的執(zhí)行結(jié)果如何。
不對啊,按鈕的點(diǎn)擊事件中有打印"Hello, I am CSDN_LQR"的,這里沒有,怎么肥事?
這里因?yàn)锧Around環(huán)繞通知會(huì)攔截原方法內(nèi)容的執(zhí)行,我們需要手動(dòng)放行才可以。代碼修改如下:
@Around("pointcut()") public void around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("@Around"); joinPoint.proceed();// 目標(biāo)方法執(zhí)行完畢 }
也不對啊,少了一個(gè)@AfterThrowing通知。這個(gè)通知只有在切點(diǎn)拋出異常時(shí)才會(huì)執(zhí)行,我們可以讓代碼出現(xiàn)一個(gè)簡單的運(yùn)行時(shí)異常:
public void test(View view) { System.out.println("Hello, I am CSDN_LQR"); int a = 1 / 0; }
這下@AfterThrowing通知確實(shí)被調(diào)用了,而且也打印出了錯(cuò)誤信息(divide by zero)。但@AfterReturning通知反而不執(zhí)行了,原因很簡單,都拋出異常了,切點(diǎn)肯定是不能返回結(jié)果的。也就是說:@AfterThrowing通知與@AfterReturning通知是沖突的,在同個(gè)切點(diǎn)上不可能同時(shí)出現(xiàn)。
4、方法耗時(shí)計(jì)算的實(shí)現(xiàn)
因?yàn)锧Around是環(huán)繞通知,可以在切點(diǎn)的前后分別執(zhí)行一些操作,AspectJ為了能肯定操作是在切點(diǎn)前還是在切點(diǎn)后,所以在@Around通知中需要手動(dòng)執(zhí)行joinPoint.proceed()來確定切點(diǎn)已經(jīng)執(zhí)行,故在joinPoint.proceed()之前的代碼會(huì)在切點(diǎn)執(zhí)行前執(zhí)行,在joinPoint.proceed()之后的代碼會(huì)切點(diǎn)執(zhí)行后執(zhí)行。于是,方法耗時(shí)計(jì)算的實(shí)現(xiàn)就是這么簡單:
@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("耗時(shí):" + dx + "ms"); }
5、JoinPoint的作用
發(fā)現(xiàn)沒有,上面所有的通知都會(huì)至少攜帶一個(gè)JointPoint參數(shù),這個(gè)參數(shù)包含了切點(diǎn)的所有信息,下面就結(jié)合按鈕的點(diǎn)擊事件方法test()來解釋joinPoint能獲取到的方法信息有哪些:
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String name = signature.getName(); // 方法名:test Method method = signature.getMethod(); // 方法:public void com.lqr.androidaopdemo.MainActivity.test(android.view.View) Class returnType = signature.getReturnType(); // 返回值類型:void Class declaringType = signature.getDeclaringType(); // 方法所在類名:MainActivity String[] parameterNames = signature.getParameterNames(); // 參數(shù)名:view Class[] parameterTypes = signature.getParameterTypes(); // 參數(shù)類型:View
6、注解切點(diǎn)
前面的切點(diǎn)表達(dá)式結(jié)構(gòu)是這樣的:
execution(<修飾符模式>? <返回類型模式> <方法名模式>(<參數(shù)模式>) <異常模式>?)
但實(shí)際上,上面的切點(diǎn)表達(dá)式結(jié)構(gòu)并不完整,應(yīng)該是這樣的:
execution(<@注解類型模式>? <修飾符模式>? <返回類型模式> <方法名模式>(<參數(shù)模式>) <異常模式>?)
這就意味著,切點(diǎn)可以用注解來標(biāo)記了。
1)自定義注解
如果用注解來標(biāo)記切點(diǎn),一般會(huì)使用自定義注解,方便我們拓展。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TestAnnoTrace { String value(); int type(); }
@Target(ElementType.METHOD):表示該注解只能注解在方法上。如果想類和方法都可以用,那可以這么寫:@Target({ElementType.METHOD,ElementType.TYPE}),依此類推。
@Retention(RetentionPolicy.RUNTIME):表示該注解在程序運(yùn)行時(shí)是可見的(還有SOURCE、CLASS分別指定注解對于那個(gè)級別是可見的,一般都是用RUNTIME)。
其中的value和type是自己拓展的屬性,方便存儲(chǔ)一些額外的信息。
2)使用自定義注解標(biāo)記切點(diǎn)
這個(gè)自定義注解只能注解在方法上(構(gòu)造方法除外,構(gòu)造方法也叫構(gòu)造器,需要使用ElementType.CONSTRUCTOR),像平常使用其它注解一樣使用它即可:
@TestAnnoTrace(value = "lqr_test", type = 1) public void test(View view) { System.out.println("Hello, I am CSDN_LQR"); }
3)注解的切點(diǎn)表達(dá)式
既然用注解來標(biāo)記切點(diǎn),那么切點(diǎn)表達(dá)式肯定是有所不同的,要這么寫:
@Pointcut("execution(@com.lqr.androidaopdemo.TestAnnoTrace * *(..))") public void pointcut() {}
切點(diǎn)表達(dá)式使用注解,一定是@+注解全路徑,如:@com.lqr.androidaopdemo.TestAnnoTrace。
親測可用 ,不貼圖了。
4)獲取注解屬性值
上面在編寫自定義注解時(shí)就聲明了兩個(gè)屬性,分別是value和type,而且在使用該注解時(shí)也都為之賦值了,那怎么在通知中獲取這兩個(gè)屬性值呢?還記得JoinPoint這個(gè)參數(shù)吧,它就可以獲取到注解中的屬性值,如下所示:
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 通過Method對象得到切點(diǎn)上的注解 TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class); String value = annotation.value(); int type = annotation.type();
以上就是Android中怎么實(shí)現(xiàn)面向切面編程,小編相信有部分知識點(diǎn)可能是我們?nèi)粘9ぷ鲿?huì)見到或用到的。希望你能通過這篇文章學(xué)到更多知識。更多詳情敬請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。