切面思想之集中式登录架构设计
如何利用切面的思想实现集中式登录?AspectJ
AspectJ 介绍
AspectJ是一个面向切面编程的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。AspectJ还支持原生的Java,只需要加上AspectJ提供的注解即可。在Android开发中,一般就用它提供的注解和一些简单的语法就可以实现绝大部分功能上的需求了。
Pointcut(切入点)
告诉代码注入工具,在何处注入一段特定代码的表达式。例如,在哪些 joint points 应用一个特定的 Advice。切入点可以选择唯一一个,比如执行某一个方法,也可以有多个选择,比如,标记了一个定义成@DebguTrace 的自定义注解的所有方法。
Advice(通知)
注入到class文件中的代码。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。 除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。
Joint point(连接点)
程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。
Android 中使用Gradle集成 AspectJ
在Android中集成AspectJ,主要思想就是hook Apk打包过程,使用AspectJ提供的工具来编译.class文件。
1、配置我们项目根目录中的build.gradle
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
// 版本界限: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'
}
注意:
版本界限:As-3.0.1 + gradle4.4-all (需要配置r17的NDK环境)
或者:As-3.2.1 + gradle4.6-all (正常使用,无警告)
或者:As-3.4.0 + gradle5.1.1-all (有过时的API警告)
2、配置app目录中的build.gradle
apply plugin: 'com.android.application'
//添加的第一处需要添加的代码 编译时用Aspect专门的编译器,不再使用传统的javac
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
...
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'
testImplementation 'junit:junit:4.12'
//添加的第二处需要添加的代码 引入包
implementation 'org.aspectj:aspectjrt:1.8.13'
}
//添加的第三处处需要添加的代码 主要做支持
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;
}
}
}
}
AspectJ的使用
登录中我们需要做两点
- 判断是否登录
- 做用户行为统计
如下代码是自定义注解,用于用户行为统计和用户登录检测。如果想要学习自定义注解,大家可以看一下这篇博客Android 自定义注解(Annotation)
package com.aop.login.annotation;
// 用户点击痕迹(行为统计)
@Target(ElementType.METHOD) // 目标作用在方法之上
@Retention(RetentionPolicy.RUNTIME)//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
public @interface ClickBehavior {
String value();
}
package com.aop.login.annotation;
// 用户登录检测
@Target(ElementType.METHOD) // 目标作用在方法之上
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCheck {
}
@Aspect // 定义切面类
public class LoginCheckAspect {
private final static String TAG = "TAG";
// 1、应用中用到了哪些注解,放到当前的切入点进行处理(找到需要处理的切入点)
// execution,以方法执行时作为切点,触发Aspect类
// * *(..)) 可以处理ClickBehavior这个类所有的方法
@Pointcut("execution(@com.aop.login.annotation.LoginCheck * *(..))")
public void methodPointCut() {
}
// 2、对切入点如何处理
@Around("methodPointCut()")
public Object jointPotin(ProceedingJoinPoint joinPoint) throws Throwable {
Context context = (Context) joinPoint.getThis();
if (true) { // 从SharedPreferences中读取
Log.e(TAG, "检测到已登录!");
return joinPoint.proceed();
} else {
Log.e(TAG, "检测到未登录!");
Toast.makeText(context, "请先登录!", Toast.LENGTH_SHORT).show();
context.startActivity(new Intent(context, LoginActivity.class));
return null; // 不再执行方法(切入点)
}
}
}
@Aspect // 定义切面类
public class ClickBehaviorAspect {
private final static String TAG = "TAG";
// 1、应用中用到了哪些注解,放到当前的切入点进行处理(找到需要处理的切入点)
// execution,以方法执行时作为切点,触发Aspect类
// * *(..)) 可以处理ClickBehavior这个类所有的方法
@Pointcut("execution(@com.aop.login.annotation.ClickBehavior * *(..))")
public void methodPointCut() {}
// 2、对切入点如何处理
@Around("methodPointCut()")
public Object jointPotin(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 >>> ");
Object result = joinPoint.proceed(); // MainActivity中切面的方法
long duration = System.currentTimeMillis() - begin;
Log.e(TAG, "ClickBehavior Method End >>> ");
Log.e(TAG, String.format("统计了:%s功能,在%s类的%s方法,用时%d ms",
funName, className, methodName, duration));
return result;
}
}
MainActivity.java
public class MainActivity extends Activity {
private final static String TAG = "TAG";
@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 area(View view) {
Log.e(TAG, "开始跳转到 -> 我的专区 Activity");
startActivity(new Intent(this, AreaActivity.class));
}
// 我的优惠卷点击事件(用户行为统计)
@ClickBehavior("我的优惠券")
@LoginCheck
public void coupon(View view) {
Log.e(TAG, "开始跳转到 -> 我的优惠券 Activity");
startActivity(new Intent(this, CouponActivity.class));
}
// 我的积分点击事件(用户行为统计)
@ClickBehavior("我的积分")
@LoginCheck
public void score(View view) {
Log.e(TAG, "开始跳转到 -> 我的积分 Activity");
startActivity(new Intent(this, ScoreActivity.class));
}
}