一、背景:
在项目开发中,对 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。
转摘请声明来源【】
当完成了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构建语句了。
五、在项目中的使用:
用于从各业务模块中拆分出友盟推送、统计和控制日志记录,将友盟推送和统计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 Preview = "com.youcompany.youproject.video.single.SinglePreviewActivity";
/*单屏回放*/
String Playback = "com.youcompany.youproject.video.single.SinglePlaybackActivity";
/*主界面*/
String Main = "com.youcompany.youproject.main.main.MainActivity";
/*消息列表*/
String Message = "com.youcompany.youproject.message.list.MessageCenterActivity";
/*图像管理*/
String Picture = "com.youcompany.youproject.files.manager.FilesManagerActivity";
/*视频*/
String Video = "com.youcompany.youproject.video.multi.MultiPlayerActivity";
/*入侵报警*/
String Alert = "com.youcompany.youproject.alert.main.AlertHomeActivity";
/*门禁*/
String Door = "com.youcompany.youproject.door.main.DoorHomeActivity";
/*电子地图*/
String StaticMap = "com.youcompany.youproject.map.staticmap.StaticMapActivity";
String GisMap = "com.youcompany.youproject.map.gis.GisActivity";
/*访客*/
String Visitor = "com.youcompany.youproject.visitor.scan.VisitorScanActivity";
/*车辆查询*/
String CarQuery = "com.youcompany.youproject.car.query.CarQueryActivity";
}
同时根据友盟统计文档,需要在所有的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 = "[防区旁路]" + name;
addAppControlLog(7, 440504, content);
} else if (flag == Constants.AlertControl.FLAG_PANGLU_BACKUP) {
content = "[防区恢复]" + name;
addAppControlLog(7, 440505, content);
}
Log.i(TAG, content);
}
以上实现代码,当我们在入侵报警模块里进行防区操作,或在电子地图里进行防区操作,都可以进行上传控制日志.
六、总结:
采用aop编程思想,使用aspectj在Android
Studio开发环境下,通过配置gradle编译脚本,成功在组件化项目中上实现了aop技术,将友盟推送、统计和app控制日志从各个业务模块中抽离到一个单独的模块,为后续对外提供SDK做出了准备工作,在项目中取得了较好的效果。