一、背景:
在項目開發中,對 App 客戶端重構后,發現用於統計用戶行為的友盟統計代碼和用戶行為日志記錄代碼分散在各業務模塊中,比如在某個模塊,要想實現對用戶的行為一和行為二進行統計,因此按照OOP面向對象編程思想,就需要把友盟統計的代碼以強依賴的形式寫入相應的模塊中,這樣會造成項目業務邏輯混亂,並且不利於對外提供SDK。因此,通過研究發現,在Android項目中,可以使用AOP面向切面編程思想,把項目中所有的友盟統計代碼,從各個業務模塊提取出來,統一放到一個模塊里面,這樣就可以避免我們提供的SDK中包含用戶不需要的友盟SDK及其相關代碼。
二、基本概念:
面向切面編程(AOP,Aspect-oriented programming):是一種可以通過預編譯方式和運行期動態代理實現在不修改源代碼的情況下給程序動態統一添加功能的技術。AOP是OOP的延續,是軟件開發中的一個熱點,是函數式編程的一種衍生范型,將代碼切入到類的指定方法、指定位置上的編程思想。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
AOP、OOP在字面上雖然非常類似,但卻是面向不同領域的兩種設計思想。OOP(面向對象編程)針對業務處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元划分,而AOP則是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。這兩種設計思想在目標上有着本質的差異。舉個簡單的例子,對於“雇員”這樣一個業務實體進行封裝,自然是OOP/OOD的任務,我們可以為其建立一個“Employee”類,並將“雇員”相關的屬性和行為封裝其中,若用AOP設計思想對“雇員”進行封裝將無從談起,同樣,對於“權限檢查”這一動作片斷進行划分,則是AOP的目標領域,若通過OOD/OOP對一個動作進行封裝,則有點不倫不類。
AOP編程的主要用途有:日志記錄,行為統計,安全控制,事務處理,異常處理,系統統一的認證、權限管理等。可以使用AOP技術將這些代碼從業務邏輯代碼中划分出來,通過對這些行為的分離,可以將它們獨立到非指導業務邏輯的方法中,進而改變這些行為的時候不影響業務邏輯的代碼。
三、具體的實現Aspectj:
AOP是一個概念,一個規范,本身並沒有設定具體語言的實現,這實際上提供了非常廣闊的發展的空間。AspectJ是AOP的一個很悠久的實現,它能夠和 Java 配合起來使用。
AspectJ的使用核心就是它的編譯器,它就做了一件事,將AspectJ的代碼在編譯期插入目標程序當中,運行時跟在其它地方沒什么兩樣,因此要使用它最關鍵的就是使用它的編譯器去編譯代碼ajc。ajc會構建目標程序與AspectJ代碼的聯系,在編譯期將AspectJ代碼插入被切出的PointCut中,達到AOP的目的。
AspectJ中幾個必須要了解的關鍵字概念:
Aspect:Aspect 聲明類似於 Java 中的類聲明,在Aspect中會包含着一些Pointcut以及相應的Advice。
JoinPoint(連接點):表示在程序中明確定義的點,例如,典型的方法調用,對類成員的訪問以及異常處理程序塊的執行等等,這些都是JoinPoints。連接點是應用程序提供給切面插入的地方在插入地建立AspectJ程序與源程序的連接。
PointCut(切點):表示一組JoinPoints,這些JoinPoint或是通過邏輯關系組合起來,或是通過通配、正則表達式等方式集中起來,它定義了相應的Advice 將要發生的地方。
Advice(通知):定義了在 PointCut里面定義的程序點具體要做的操作,它通過 before、after 和 around 來區別是在每個JoinPoint之前、之后還是代替執行的代碼。
一個連接點是程序流中指定的一點。切點收集特定的連接點集合和在這些點中的值。一個通知是當一個連接點到達時執行的代碼,這些都是AspectJ的動態部分。其實連接點就好比是程序中的一條一條的語句,而切點就是特定一條語句處設置的一個斷點,它收集了斷點處程序棧的信息,而通知就是在這個斷點前后想要加入的程序代碼。AspectJ中也有許多不同種類的類型間聲明,這就允許程序員修改程序的靜態結構、名稱、類的成員以及類之間的關系。AspectJ中的方面是橫切關注點的模塊單元。它們的行為與Java語言中的類很像,但是方面還封裝了切點、通知以及類型間聲明。
圖3.1 AspectJ的幾個關鍵點概念
圖3.1簡要的總結了一下上述這些概念在程序中的作用。
圖3.2 一般工程結構
正常情況下,我們會把一個簡單的示例應用拆分成兩個 modules,第一個包含我們的 Android App 代碼,第二個是一個 Android Library 工程,使用 AspectJ 織入代碼(代碼注入)。經過ajc編譯器編譯后,可以將兩個modules的代碼編譯在一起,使App可以正常運行。
四、集成AspectJ到項目中:
1、build.gradle配置:
由於aspectj編譯時需要用到ajc編譯器,為了使 Aspectj能在Android上運行,將aspect模塊的代碼注入app中,需要使用gradle插件完成編譯,所以我們需要在所有業務模塊Module中的build.gradle加入以下groovy構建語句:
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
import com.android.build.gradle.LibraryPlugin
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath ‘com.android.tools.build:gradle:2.3.2’
classpath ‘org.aspectj:aspectjtools:1.8.10’
classpath ‘org.aspectj:aspectjweaver:1.8.10’
}
}
android.libraryVariants.all { variant ->
LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.5",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath
,"-bootclasspath", plugin.getAndroidBuilder().getBootClasspath(true).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;
}
}
}
}
在app主模塊的build.gradle需要使用以下groovy構建語句:
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath ‘com.android.tools.build:gradle:2.3.2’
classpath ‘org.aspectj:aspectjtools:1.8.10’
classpath ‘org.aspectj:aspectjweaver:1.8.10’
}
}
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.5",
"-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;
}
}
}
}
App主模塊與其他庫工程中的groovy構建語句唯一的差別是獲取"-bootclasspath"的方法不同,在主模塊中是project.android.bootClasspath.join(File.pathSeparator),而在庫工程中則為plugin.getAndroidBuilder().getBootClasspath(true).join(File.pathSeparator)。
此外還需要在aspectj代碼模塊的build.gradle中添加對aspectj庫的依賴:
compile ‘org.aspectj:aspectjrt:1.8.10’
並且使所有業務模塊添加對aspectj模塊的依賴
compile project(':aspectj')
這樣整個Aspectj編譯環境就搭建好了。
由於不同版本的gradle在獲取編譯時獲取類的路徑等信息Api不同,所以以上groovy配置語句僅在Gradle Version高於3.3的版本上生效。
2、gradle配置優化:
如果我們的項目中個存在着較多個模塊module,若在每個模塊中都添加以上groovy構建語句,將會使得build.gradle變得比較復雜,不移維護, 所以考慮將以上groovy編譯語句封裝為一個gradle插件,我們只需要在各個業務模塊的build.gradle中只需添加一句
apply plugin: 'xxx.xxx'即可。
編寫gradle插件:
在項目中新建模塊aspectjbuild,在build.gradle構建腳本中的內容為:
//是一個groovy項目
apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
jcenter()
}
dependencies {
//添加依賴
compile gradleApi()
compile localGroovy()
classpath ‘com.android.tools.build:gradle:2.3.2’
classpath ‘org.aspectj:aspectjtools:1.8.10’
classpath ‘org.aspectj:aspectjweaver:1.8.10’
}
//聲明插件類名
group = 'com.youcompany.youproject.aspectjbuild.plugin'
//聲明插件版本
version = '1.0.0'
//上傳到本地倉庫task
uploadArchives {
repositories {
mavenDeployer {
repository(url: uri('../aspectjbuild/repo'))
}
}
}
在src/mian目錄下新建groovy目錄,並新建文件AspectjBuild.groovy,在AspectjBuild.groovy中的內容為:
package com.youcompany.youproject
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.compile.JavaCompile
import com.android.build.gradle.LibraryPlugin
import org.aspectj.bridge.IMessage
/**
* <p>aspectj編譯插件</p>
*
*/
public class AspectjBuild implements Plugin<Project> {
@Override
void apply(Project project) {
project.android.libraryVariants.all { variant ->
LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.5",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", plugin.getAndroidBuilder().getBootClasspath(true).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;
}
}
}
}
}
}
然后在src/main目錄下新建resources/META-INF.gradle-plugins目錄,在該目錄下新建aspectj.build.properties文件,該文件中的內容為:
implementation-class=com.youcompany.youproject.AspectjBuild
即指向剛才的groovy類,這個配置文件的文件名就是插件名字,使用該插件時為
apply plugin:’aspectj.build’
這樣整個gradle插件工程就搭建好了,然后我們通過執行gradle任務中的assemble完成對整個插件工程的編譯,生成插件,最后我們還需要執行uploadArchives任務,將插件上傳到本地倉庫,本地倉庫的地址在build.gradle中的配置為../aspectjbuild/repo。
轉摘請聲明來源【http://www.cnblogs.com/lzh-Linux/p/8243434.html】
當完成了assemble和uploadArchives任務后,我們就可以看到成功的生成了gradle插件。
在各個module中引用,我們需要在project的build.gradle中添加本地倉庫地址
maven { url uri('aspectjbuild/repo') },並依賴類
classpath 'com.youcompany.youproject.aspectjbuild.plugin:aspectjbuild:1.0.0',
完整的配置為:
buildscript {
repositories {
…
maven { url uri('aspectjbuild/repo') }
}
dependencies {
…
classpath 'com.youcompany.youproject.aspectjbuild.plugin:aspectjbuild:1.0.0'
}
}
最后,我們就可以通過 apply plugin: 'aspectj.build' 在各個Module的build.gradle中來代替之前那段很長的groovy構建語句了。
轉載請聲明來源【http://www.cnblogs.com/lzh-Linux/p/8243434.html】
五、在項目中的使用:
用於從各業務模塊中拆分出友盟推送、統計和控制日志記錄,將友盟推送和統計sdk從lib_common模塊移動到aspectj模塊,把相關代碼集中在aspectj模塊。
1、友盟推送:
根據友盟配置推送文檔知,需要在所有的Activity onCreate(..)方法中添加
PushAgent.getInstance(this).onAppStart();
在Application中的onCreate(..)添加初始化代碼
具體實現代碼:
替換原來在BaseActivity類中的onCreate方法:
定義匹配表達式: private static final String POINTCUT_ACTIVITY_ONCREATE =
"execution(* com.youcompany.youproject.common.base.BaseActivity.onCreate(..))";
定義切點方法: @Pointcut(POINTCUT_ACTIVITY_ONCREATE)
public void methodActivityCreate() {}
該切點表示在所有BaseActivity類包括子類,中的OnCreate(..)方法是切入點.
@After("methodActivityCreate()")
public void onActivityCreate(JoinPoint joinPoint) throws Throwable {
Context context = ((Context) joinPoint.getTarget());
PushAgent.getInstance(context).onAppStart();
}
}
@After表示當BaseActivity中的OnCreate(..)執行完成后執行通知定義的方法onActivityCreate(..)。.
替換原來在youprojectApplication中onCreate方法的初始化:
private static final String POINTCUT_APPLICATION_ONCREATE =
"execution(* com.youcompany.youproject.youprojectApplication.onCreate())";
@Pointcut(POINTCUT_APPLICATION_ONCREATE)
public void methodApplicationCreate() { }
@After("methodApplicationCreate()")
public void onApplicationCreate(JoinPoint joinPoint) {
final Context context = ((Context) joinPoint.getTarget());
PushAgent mPushAgent = PushAgent.getInstance(context);
//注冊推送服務,每次調用register方法都會回調該接口
mPushAgent.register(new IUmengRegisterCallback() {
@Override
public void onSuccess(String deviceToken) {
//注冊成功會返回device token
Intent intent = new Intent("com.youcompany.youproject.deviceToken");
intent.putExtra("deviceToken", deviceToken);
context.sendBroadcast(intent);
}
@Override
public void onFailure(String s, String s1) {
}
});
mPushAgent.setDebugMode(false);
//sdk開啟通知聲音
mPushAgent.setNotificationPlaySound(MsgConstant.NOTIFICATION_PLAY_SDK_ENABLE);
//自定義推送消息處理
mPushAgent.setPushIntentServiceClass(PushMessageService.class);
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Log.i(TAG, "友盟初始化--" + joinPoint.getThis().getClass().getName() + "--" + methodSignature.getName());
}
獲取消息推送開關
由於消息推送開關在lib_common中,而aspectj不依賴lib_common所以考慮使用反射的方法獲取:
/*
* 獲取消息推送開關
*/
private boolean isMessagePush() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> cacheClass = Class.forName("com.youcompany.youproject.common.data.InfoCache");
Object object = cacheClass.getMethod("getIns").invoke(null);
return (boolean) cacheClass.getMethod("isMessagePush").invoke(object);
}
因此還需要在混淆添加配置
# InfoCache類中的兩個方法用於獲取消息推送開關不能被混淆
-keep class com.youcompany.youproject.common.data.InfoCache
-keepclassmembers class com.youcompany.youproject.common.data.InfoCache {
public static ** getIns();
public boolean isMessagePush();
}
這樣就完成了通過aspectj實現友盟推送代碼的抽離,登錄友盟友盟后台,通過推送一條消息,測試可以成功的收到推送。
2、友盟統計:
在項目中中,友盟統計統計的行為有消息、圖像管理等等。
因此,我把統計各個模塊的使用代碼,分別定義在每個對應模塊的第一個Activity的OnCreate(..)方法中,根據不同的Activity類名,進行統計。
interface ClassConstants {
...
...
/*主界面*/
String Main = "com.youcompany.youproject.main.main.MainActivity";
/*消息列表*/
String Message = "com.youcompany.youproject.message.list.MessageCenterActivity";
/*圖像管理*/
String Picture = "com.youcompany.youproject.files.manager.FilesManagerActivity";
...
...
}
同時根據友盟統計文檔,需要在所有的Activity中的onResume()和onPause()方法中分別調用MobclickAgent.onResume(context); MobclickAgent.onPause(context);方法,因此還定義了對應的切點及相應的通知。
private static final String POINTCUT_ACTIVITY_ONRESUME =
"execution(* com.youcompany.youproject.common.base.BaseActivity.onResume())";
private static final String POINTCUT_ACTIVITY_ONPAUSE =
"execution(* com.youcompany.youproject.common.base.BaseActivity.onPause())";
@Pointcut(POINTCUT_ACTIVITY_ONRESUME)
public void methodActivityResume() {
}
@Pointcut(POINTCUT_ACTIVITY_ONPAUSE)
public void methodActivityPause() {
}
@After("methodActivityResume()")
public void onActi}vityResume(JoinPoint joinPoint) throws Throwable {
//獲取目標對象
Context context = ((Context) joinPoint.getTarget());
MobclickAgent.onResume(context);
}
@After("methodActivityPause()")
public void onActivityPause(JoinPoint joinPoint) throws Throwable {
//獲取目標對象
Context context = ((Context) joinPoint.getTarget());
MobclickAgent.onPause(context);
}
在每個Activity類中,無論是否需要onResume()和onPause方法,我們都需要在類中重寫這兩個方法,為aspectj編譯時插入語句提供位置。
3、app控制日志:
在上傳App控制日志時,需要用到用戶名或者監控點名字等,可以通過反射獲取類內成員變量值或通過JoinPoint獲取方法的參數值。
訪問目標方法最簡單的做法是定義增強處理方法時,將第一個參數定義為JoinPoint類型,當該增強處理方法被調用時,該JoinPoint參數就代表了織入增強處理的連接點。JoinPoint里包含了如下幾個常用的方法:
Object[] getArgs:返回目標方法的參數
Signature getSignature:返回目標方法的簽名
Object getTarget:返回被織入增強處理的目標對象
Object getThis:返回AOP框架為目標對象生成的代理對象。
由於項目中代碼采用的是MVP架構,所以我把上傳控制日志的代碼定義在了Model層,這樣只要進行了對應的網絡底層數據請求,就可以進行日志記錄。同時對於同一個操作,可能存在於多個模塊,例如,某個操作,存在於模塊一,同時也存在於模塊二,
因此,可以使用邏輯運算符組合切點:
private static final String POINTCUT_ALERT_CONTROL =
"execution(* com.youcompany.youproject.alert.data.source.RemoteAlertDataResource.controlZone(..))";
private static final String POINTCUT_MAP_ALERT_CONTROL =
"execution(* com.youcompany.youproject.map.data.source.RemoteMapDataSource.controlZone(int, java.lang.String))";
@Pointcut(POINTCUT_ALERT_CONTROL)
public void methodAlertControl() {
}
@Pointcut(POINTCUT_MAP_ALERT_CONTROL)
public void methodMapAlert() {
}
使用邏輯或進行組合切點匹配
@After("methodAlertControl() || methodMapAlert()")
public void onControlZone(JoinPoint joinPoint) throws Throwable {
int flag = (int) joinPoint.getArgs()[0];
String content = "";
String name = (String) joinPoint.getArgs()[1];
if (flag == Constants.AlertControl.FLAG_PANGLU) {
content = "[XXX]" + name;
addAppControlLog(7, 440504, content);
} else if (flag == Constants.AlertControl.FLAG_PANGLU_BACKUP) {
content = "[XXX]" + name;
addAppControlLog(7, 440505, content);
}
Log.i(TAG, content);
}
以上實現代碼,當我們在模塊一里進行操作,或在模塊二里進行操作,都可以進行上傳控制日志.
六、總結:
采用aop編程思想,使用aspectj在Android Studio開發環境下,通過配置gradle編譯腳本,成功在項目中項目中上實現了aop技術,將友盟推送、統計和app控制日志從各個業務模塊中抽離到一個單獨的模塊,為后續對外提供SDK做出了准備工作,在項目中取得了較好的效果。
最后請注意,如果你的工程存在多個模塊可能會導致aspectj在編譯時由於多線程沖突導致aop插入代碼失敗,解決方法是修改aspectj的源碼,在CompilerAdapter類中在對應的field上用ThreadLocal 包裹,使在參數實例在多線程下隔離,而不是用官方原版的jar包。