最近学习到了AOP这种架构思想,感觉很巧妙很实用,能应用到很多开发场景,在此就以常见的登录及用户行为统计功能来实践一下。
对于AOP的概念大概是这样的(百度百科):
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
以登录为例,在我们的App中判断登录状态有这极高的出场率,我猜大家一般都是这样写的:
if(Helper.isLogin()){
....
}else {
//去登录
....
}
只是把他抽成了一个公用的方法,然后就是铺天盖地的调用,估计光个人中心就要写个十几二十次,类似的代码写多了,我们的工程就成了牛皮癣重症患者。接下来我们就看看AOP思想是怎么低耦高效的来做这样的事儿的呢。
首先我们来搭建一下环境,我们用到了Aspectj这个框架,基本用法可参考这篇博客
项目级build
buildscript { //编译时用Aspect专门的编译器,不在使用传统的javac
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
//版本界限:AS-3.0.1 + gradle4.4-all(需要配置r17的NDK环境)
//或者:As-3.2.1 + gradle4.6-all(正常使用,无警告)
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
module级build
apply plugin: 'com.android.application'
//版本界限:AS-3.0.1 + gradle4.4-all(需要配置r17的NDK环境)
//或者:As-3.2.1 + gradle4.6-all(正常使用,无警告)
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.example.aopdemo"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'org.aspectj:aspectjrt:1.8.13'
}
// 版本界限:As-3.0.1 + gradle4.4-all (需要配置r17的NDK环境)
// 或者:As-3.2.1 + gradle4.6-all (正常使用,无警告)
//固定配置
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;
}
}
}
}
比较值得注意的有两点
1.androidstudio和gradle的版本问题
AS-3.0.1 + gradle4.4-all(需要配置r17的NDK环境)
AS-3.2.1 + gradle4.6-all (正常使用,无警告)
AS-3.4.0 + gradle5.1.1-all(会有警告信息)
2. 编译时用Aspect专门的编译器,不在使用传统的javac(无法在编译期插入代码),这个也不用我们关心是Aspect做好的,它无缝支持java。
环境准备好开撸
新建了3个Activity,
MainActivity,包含了4个按钮(登录,评论,点赞,转发)及click方法
public class MainActivity extends AppCompatActivity {
private final String TAG = "AOPDEMO";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@ClickBehavior("登录")
public void login(View view) {
Log.e(TAG,"模拟登录请求...验证通过,登陆成功");
}
//用户行为统计
@ClickBehavior("评论")
@LoginCheck
public void remark(View view) {
Log.e(TAG,"评论 --> 评论、点赞、转发界面");
startActivity(new Intent(this,ContentActivity.class));
}
//用户行为统计
@ClickBehavior("点赞")
@LoginCheck
public void zan(View view) {
Log.e(TAG,"点赞 --> 评论、点赞、转发界面");
startActivity(new Intent(this,ContentActivity.class));
}
//用户行为统计
@ClickBehavior("转发")
@LoginCheck
public void transmit(View view) {
Log.e(TAG,"转发 --> 评论、点赞、转发界面");
startActivity(new Intent(this,ContentActivity.class));
}
}
其中ClickBehavior和LoginCheck是自定义的注解,分别是用户行为统计(点击行为)和登录检查
ClickBehavior
//用户行为统计
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClickBehavior {
String value();
}
LoginCheck
//用户行为统计
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCheck {
}
大家可能有疑问了,就写了两个注解就能实现功能了?之前引入的Aspectj又有什么用呢?ok最关键的一步骚操作即将到达战场
ClickBehaviorAspect(类名可随意)
@Aspect //定义切片类
public class ClickBehaviorAspect {
private final String TAG = "AOPDEMO";
//1.应用中用到了哪些注解,放入当前的切入点进行处理(找到需要处理的切入点)
// execution, 以方法执行时作为切点,触发Aspect类
// @:应因为ClickBehavior是一个接口,如果参数是一个类则不加@
// * *(..))":可以处理ClickBehavior这个类的所有方法,也可具体指定某个方法
// method() : 方法名可随意
@Pointcut("execution(@com.example.aopdemo.annotation.ClickBehavior * *(..))")
public void method() {
}
//2.对这些切入点如何处理
@Around("method()")
public Object joinPoint(ProceedingJoinPoint joinPoint) throws Throwable{
//获取签名方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//获取方法所属类名
String className = methodSignature.getDeclaringType().getSimpleName();
//获取方法名
String methodName = methodSignature.getName();
//获取方法的注解值(需要统计的用户行为)
String funName = methodSignature.getMethod().getAnnotation(ClickBehavior.class).value();
//统计方法的执行时间,统计用户点击某功能的行为(真实项目:存储到本地,每过x天上传到服务器)
long begin= System.currentTimeMillis();
Log.e(TAG,"ClickBehavior Method start >>>");
//MainActivity中切面的方法,如:remark(View view),即在运行remark函数的函数提前会先 //运行以上内容,在remark函数运行完后运行 Object result = joinPoint.proceed();
//之后的内容,就是通过这种方式在预编译时插入相同代码的
Object result = joinPoint.proceed();
long duration= System.currentTimeMillis() - begin;
Log.e(TAG,"ClickBehavior Method end >>>");
Log.e(TAG,String.format("统计了:%s功能,在%s类的%s方法,用时%s ms",funName,className,methodName,duration));
return result;
}
}
LoginCheckAspect
@Aspect //定义切片类
public class LoginCheckAspect {
private final String TAG = "AOPDEMO";
//1.应用中用到了哪些注解,放入当前的切入点进行处理(找到需要处理的切入点)
// execution, 以方法执行时作为切点,触发Aspect类
// @:应为ClickBehavior是一个接口
// * *(..))":可以处理ClickBehavior这个类的所有方法
// method() : 方法名可随意
@Pointcut("execution(@com.example.aopdemo.annotation.LoginCheck * *(..))")
public void method() {
}
//2.对这些切入点如何处理
@Around("method()")
public Object joinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
Context context = (Context) joinPoint.getThis();
//在此改true或false来设定登录状态
if (true) { //正式项目从sharedPreferences中读取
Log.e(TAG, "检测到已登录");
return joinPoint.proceed();
} else {
Log.e(TAG, "检测到未登录");
Toast.makeText(context, "请先登录!", Toast.LENGTH_LONG).show();
context.startActivity(new Intent(context, LoginActivity.class));
return null; //方法不执行(切入点)
}
}
}
点击按钮时,可对应查看logcat效果