Android如何避免定製化需求影響到主邏輯-面向切面程式設計(AOP)
背景描述
在Android開發中,往往需要處理很多的定製化需求,程式碼中會充滿if...else...
這樣的分支程式碼。這樣的需求多了,會讓業務程式碼越來越難以維護。有沒有什麼辦法,以一種最小侵入性的形式承載這些定製化需求,既能始終保持主邏輯不變,又能實現定製化需求?
例:現Android專案中,有一個支付模組,主邏輯很簡單,一個pay
方法,傳入支付渠道和支付金額即可。但是需要滿足其他定製化需求:
- 定製化需求1,OPPO支付渠道要求單筆支付金額達到50元,需要先完成實名認證才能進行支付。
- 定製化需求2,支付操作,需要進行資料上報。
/** * 支付介面 * * @param payChannel 支付渠道 * @param money支付金額 */ private void pay(String payChannel,int money){ // 定製化需求1:OPPO渠道要求,單筆支付金額達到50元,需要先完成實名認證才能進行支付 if ("OPPO".equals(payChannel) && money >= 50){ Log.i(TAG, "pay: 跳轉到實名認證介面"); ... return; } // 定製化需求2:支付操作事件,進行資料上報 DataReport.uploadPayEvent(payChannel, money); Log.i(TAG, "pay: 進行標準的支付"); ... }
筆者所在的部門,是手遊公司的內部SDK開發部門,SDK為各款手遊提供了登入、支付、公共元件等功能,7年下來,原本簡單的主邏輯程式碼中,加入了近百個定製化需求,非常臃腫且維護成本越來越高。不堪重負,正在緊張地重構。那有沒有什麼辦法從技術上破解這個難題,讓重構後的SDK不再受其困擾?接下來將提到破解難題的利器,面向切面程式設計 。
簡介
面向切面程式設計(aspect-oriented programming,AOP),是一種程式設計範型,該泛型以一種稱為切面的語言構造為基礎,用來描述分散在物件、類或函式中的橫切關注點。簡單理解,一個Java method的執行,把它拆解成執行前
、執行時
、執行後
,對每個環節能有修改能力,則可以更靈活地控制這個Java method的執行邏輯。
本篇重點將其作為各種定製化需求的載體,同時也可以用作日誌埋點、登入狀態管理日誌記錄,效能統計,安全控制,事務處理,異常處理等場景。
AspectJ
AspectJ全稱為ofollow,noindex">Eclipse AspectJ ,是Eclipse開發的面向Java™程式語言的面向切面框架,它相容Java平臺,易於學習和使用。在JavaWeb基於Spring框架開發的專案中有廣泛使用,
在本文中,將選用AspectJ作為Java面向切面程式設計的開發庫,該庫可以整合進Android Studio專案中。
快速上手
基於背景描述中支付模組的2個定製化需求,通過AOP實現對主邏輯程式碼的最小侵入性。
配置
1. 在project的build.gradle中配置classpath
buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.2.0-rc03' // 配置classpath classpath 'org.aspectj:aspectjtools:1.8.6' } }
2. 在app的build.gradle中配置編織指令碼
apply plugin: 'com.android.application' // 編織指令碼 start import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main buildscript { repositories { mavenCentral() } dependencies { classpath 'org.aspectj:aspectjtools:1.8.9' classpath 'org.aspectj:aspectjweaver:1.8.9' } } repositories { mavenCentral() } 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; } } } } // 編織指令碼 end
3. 在其他用到AOP的module的build.gradle中配置編織指令碼
apply plugin: 'com.android.library' import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main android.libraryVariants.all { variant -> JavaCompile javaCompile = variant.javaCompile javaCompile.doLast { //下面的1.8是指我們相容的jdk的版本 String[] args = ["-showWeaveInfo", "-1.8", "-inpath", javaCompile.destinationDir.toString(), "-aspectpath", javaCompile.classpath.asPath, "-d", javaCompile.destinationDir.toString(), "-classpath", javaCompile.classpath.asPath, "-bootclasspath", android.bootClasspath.join(File.pathSeparator)] MessageHandler handler = new MessageHandler(true); new Main().run(args, handler) def log = project.logger 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: case IMessage.INFO: log.info message.message, message.thrown break; case IMessage.DEBUG: log.debug message.message, message.thrown break; } } } }
5. 引用庫
implementation 'org.aspectj:aspectjrt:1.8.9'
實現
1. 先定義特定的執行時註解
/** * OPPO特殊處理註解 * * @author Divin on 2018/11/23 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OppoPayAnnotation { }
/** * 支付事件資料上報註解 * * @author Divin on 2018/11/26 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface UploadPayEventAnnotation { }
2. 用註解修飾主邏輯方法
用多個註解修飾同一個方法時,會按順序從上到下進行執行。
/** * 支付介面 * * @param payChannel 支付渠道 * @param money支付金額 */ @OppoPayAnnotation @UploadPayEventAnnotation private void pay(String payChannel, int money) { Log.i(TAG, "pay: 進行標準的支付"); Toast.makeText(this, "進行標準的支付", Toast.LENGTH_LONG).show(); }
3. 編寫定製化需求切面程式碼
AspectJ框架,最簡單的是使用@Aspect類註解來修飾切面類,使用@Pointcut方法註解來找到切入點,"execution(@com.acronym.aoplib.pay.OppoPayAnnotation * *(..))"
表示找到所有類中使用OppoPayAnnotation修飾的方法。最後使用@Around方法註解來編寫切入程式碼,"executionPayAnnotation()"
即切入點。在pay方法中,每一行的用途已在在註釋中標明。
/** * 支付切入程式碼 * * @author Divin on 2018/11/23 */ @Aspect public class PayAnnotationAspectJ { private static final String TAG = "d5g-" + "PayAnnotationAspect"; /** * 找到切入點 */ @Pointcut("execution(@com.acronym.aoplib.pay.OppoPayAnnotation * *(..))") public void executionPayAnnotation() { } /** * 定製化需求1,OPPO支付渠道要求單筆支付金額達到50元,需要先完成實名認證才能進行支付 * * @param joinPoint * @return * @throws Throwable */ @Around("executionPayAnnotation()") public Object pay(ProceedingJoinPoint joinPoint) throws Throwable { Log.i(TAG, "pay: 定製化需求1,OPPO支付渠道要求單筆支付金額達到50元,需要先完成實名認證才能進行支付"); // 獲取被切入的方法簽名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); OppoPayAnnotation payAnnotation = signature.getMethod().getAnnotation(OppoPayAnnotation.class); if (payAnnotation != null) { // 解析出所有引數 Object[] args = joinPoint.getArgs(); String payChannel = (String) args[0]; int money = (int) args[1]; // OPPO渠道,支付金額達到50元 if ("OPPO".equals(payChannel) && money >= 50) { Log.i(TAG, "pay: 彈出實名認證介面"); // 攔截原方法的執行 return null; } } // 繼續原方法的執行 return joinPoint.proceed(); } }
/** * 支付事件的資料上報切入程式碼 * * @author Divin on 2018/11/23 */ @Aspect public class UploadPayEventAnnotationAspectJ { private static final String TAG = "d5g-" + "UploadPayEventAnnotationAspectJ"; /** * 找到切入點 */ @Pointcut("execution(@com.acronym.aoplib.upload.UploadPayEventAnnotation * *(..))") public void executionUploadPayEventAnnotation() { } /** * 定製化需求2,支付操作,需要進行資料上報 * * @param joinPoint * @return * @throws Throwable */ @Around("executionUploadPayEventAnnotation()") public Object uploadPayEvent(ProceedingJoinPoint joinPoint) throws Throwable { Log.i(TAG, "定製化需求2,支付操作,需要進行資料上報"); // 獲取被切入的方法簽名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); OppoPayAnnotation payAnnotation = signature.getMethod().getAnnotation(OppoPayAnnotation.class); if (payAnnotation != null) { // 解析出所有引數 Object[] args = joinPoint.getArgs(); String payChannel = (String) args[0]; int money = (int) args[1]; // 資料上報 DataReport.uploadPayEvent(payChannel, money); } // 繼續原方法的執行 return joinPoint.proceed(); } }
總結
通過AOP的形式管理定製化需求,可以讓主邏輯程式碼始終保持下面的效果,對於支撐多個業務的SDK裡,還是比較難得的。同時,這些AOP的類,可以單獨抽成jar包形式,按需進行引用,不載入不影響主邏輯執行。僅建議用作定製化需求的程式碼載體,不建議用作熱載入等場景。
很晚了,常用API、原理講解再補上。
/** * 支付介面 * * @param payChannel 支付渠道 * @param money支付金額 */ @OppoPayAnnotation @UploadPayEventAnnotation private void pay(String payChannel, int money) { Log.i(TAG, "pay: 進行標準的支付"); Toast.makeText(this, "進行標準的支付", Toast.LENGTH_LONG).show(); }