Android Studio 想接入 AspectJ ? 看这篇就对了!从0到1 , 包会!
OOP ( Object Oriented Programming ) 面向对象编程思想
AOP ( Aspect Oriented Programming ) 面向切面编程思想
如下图,每一个模块封装了其特有的功能属性,各尽其责,便于其它使用者的调用和复用。
如下图,在特定的切面进行代码 「织入」(注意用词,是织入不是hook…),添加共同逻辑( 日志,埋点等) 且不会影响原有模块的业务功能和架构。
OOP 的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有问题都能完美得划分到模块中。举个最简单而又常见的例子:现在想为每个模块加上日志功能,要求模块运行时候能输出日志并统计当前方法耗时。
这个问题放在 OOP 思想中解决的办法通常是设计日志模块,并且在需要统计的地方一一手动加入日志的 API,并且如果日志的 API 改动,那将牵一发而动全身。
这个时候 AOP 的用途便体现出来了。
AOP 主要用途有:日志记录,行为统计,安全控制,事务处理,异常处理,系统统一的认证、权限管理等。可以使用 AOP 思想将这些代码从业务逻辑代码中划分出来,通过对这些行为的分离,可以将它们独立到非主导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
上图中 AspectJ 是 Android 实现 AOP 编程思想的具体工具
AspectJ 的使用核心就是它的编译器,它就做了一件事,将 AspectJ 的代码在编译期插入目标程序当中,运行时跟在其它地方没什么两样,因此要使用它最关键的就是使用它的编译器去编译代码 (AspectJ compile) 。
ajc 会构建目标程序与 AspectJ 代码的联系,在编译期将 AspectJ 代码插入被切出的 PointCut 中,达到 AOP 的目的。
也就是在 .java
文件编译为.class
文件的时候,对刚编译完的.class
字节码文件做手脚,对相应切入点的代码进行功能代码「织入」。
下段内容摘自张绍文极客时间-编译插桩的三种方法.
AspectJ是 Java 中流行的 AOP(aspect-oriented programming)编程扩展框架,网上很多文章说它处理的是 Java 文件,其实并不正确,它内部也是通过字节码处理技术实现的代码注入。
从使用上来看,作为字节码处理元老,AspectJ 的框架的确有自己的一些优势。
上面说道 : AOP是一个编程思想和概念,本身并没有设定具体语言的实现,这实际上提供了非常广阔的发展的空间。
AspectJ 是 AOP 的一个很悠久的实现,它能够和 Java 配合起来使用。( 很稳 , 支付宝app第三方开源也有用到)
先来了解几个 AspectJ 的 基本和主要的 关键词 :
筛选条件 | 说明 | 示例 |
---|---|---|
within(TypePattern) | 筛选执行的包名路径 | within(com.sample.aop.*),在aop包名内的JPoint. |
withincode(Method) | 筛选执行的方法. | withinCode(* A.aopMethod(…)),在A类的aopMethod涉及的JPoint |
target(类全限定名) | target一般用在call的情况,匹配任意标注了的目标类(指明拦截的方法属于那个类) | target(A)就会搜索到由A类调用testMethod的地方 |
this(类全限定名) | 与target雷同,区分点在于:this指方法是在哪个类中被调用的 | B类中调用A.testMethod,指定的类为B |
args() | 对入参进行条件匹配 | args(int,. . ),表示第一个参数是int,后面参数个数和类型不限 |
… | 其它高级用法 |
Log.e()
这个函数 , e()
可以看作是个 JPoint ,而且调用e()
的函数也可以认为是一个 JPoint ) , 也就是所有可以注入代码的地方。可以说如果 AspectJ 规定中没有这样的 JPoint,那么我们是无法利用AspectJ 来实现功能需求.
织入时机 | 说明 | 示例 |
---|---|---|
call | 函数调用 | 比如调用Log.e() , 这是一处JPoint |
execution | 函数调执行 | execution是某个函数执行的内部 |
例如 A 类中,调用 Pointcut.Method() ,
call 截取的是 在A类中调用该处函数的地方.
execution 截取的则是 Pointcut 内 Method() 执行的方法…
Call(Before)
Pointcut{
Pointcut Method
}
Call(After)
Pointcut{
execution(Before)
Pointcut Method
execution(After)
}
实体类 , get/setName方法
下面这个例子通过 AOP 修改getName()
返回参数,在调用setName()
方法加上打印日志
public class AopDemo {
public static class innerB {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
声明使用 Aspect 的类, 加上@Aspect 注解即可,类里定义了切入点和通知,即组成了切面.
@Pointcut(" call(* getName() ) ")
@Aspect
public class DemoAspect {
@Pointcut("call(* setName(String))")
public void demo2() {}
@Around("demo2()")
public Object arounddemo1(ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
Object proceed = joinPoint.proceed();
if (target instanceof AopDemo.innerB) {
Log.e("log", "call setName");
// ((AopDemo.innerB) target).setName("haha"); // 可以在方法执行之后搞点事情.
}
// joinPoint.proceed()代表执行原始的方法,在这之前之后都可以进行各种逻辑处理。
return proceed;
}
@Pointcut("call(* getName())")
public void demo1() {}
@Around("demo1()")
public String arounddemo1() {
return "hoho";
}
}
方法调用出 , 例如在MainActivity里调用实体类的get/setName方法
// MainActivity里调用方法
AopDemo.innerB innerB = new AopDemo.innerB();
innerB.setName("ok");
Toast.makeText(this, "str==" + innerB.getName(), Toast.LENGTH_SHORT).show();
innerB.setName(“ok”); 代码执行后便会加上 Log.e(“log”, “call setName”);
innerB.getName(); 返回的不是"ok",而是我们代码织入的"hoho"
// MainActivity.class AspectJ 代码织入前
innerB innerB = new innerB();
innerB.setName("ok");
Toast.makeText(this, "str==" + innerB.getName(), 0).show();
// MainActivity.class AspectJ 代码织入后
innerB innerB = new innerB();
String var7 = "ok";
JoinPoint var9 = Factory.makeJP(ajc$tjp_0, this, innerB, var7);
var10000 = DemoAspect.aspectOf();
Object[] var10 = new Object[]{this, innerB, var7, var9};
var10000.arounddemo1((new MainActivity$AjcClosure1(var10)).linkClosureAndJoinPoint(4112));
Toast.makeText(this, "str==" + DemoAspect.aspectOf().arounddemo1(), 0).show();
如果将 Pointcut 的 call 方法改为 execution , 修改的则是 innerB.class 文件.
也就是说 AspectJ 实现 AOP 编程思想的方法就是在 .java 文件编译为 .class 后,使用 ajc (AspectJ compile) 编译器 , 将需要 织入 的代码插到特定的 Pointcut 中
以上便是 AspectJ 基本使用方式,要挑战高级用法,前往 AspectJ 开发手册.
例如我们要在代码中进行权限检查,如果没有权限则不执行方法(或者执行权限调用方法,权限申请成功后再执行目标方法)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomPointCut {
String[] permissionName();
}
@CustomPointCut(permissionName = {"PHONE", "STATUS"})
public void customMethod() {
Toast.makeText(this, "customMethod call", Toast.LENGTH_SHORT).show();
}
@Aspect
public class DemoAspect {
// 具体使用的时候,CustomPointCut 要改为具体的全限定名.
@Pointcut("execution(@com.xxx.CustomPointCut * *..*.*(..))")
public void customMethod() {
}
@Around("customMethod()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 类名
String className = methodSignature.getDeclaringType().getSimpleName();
// 方法名
String methodName = methodSignature.getName();
// 功能名
CustomPointCut behaviorTrace = methodSignature.getMethod().getAnnotation(CustomPointCut.class);
String[] value = behaviorTrace.permissionName();
// value -- > phone,status
long start = System.currentTimeMillis();
// 也可以不执行joinPoint.proceed(),根据业务需求没有权限/登录不调用目标方法
Object result = joinPoint.proceed();// result 为目标方法调用后的返回值
long duration = System.currentTimeMillis() - start;//可以统计方法耗时.
return result;//返回值,可以任性修改.
}
}
这样只要代码中加入 @CustomPointCut 注解,便可以统一处理权限操作. 登录判断也可以用此方法来统一处理。代码精简,一步到位.
下面看一下 AspectJ 在编译时期做了哪些处理
// MainActivity.class
@CustomPointCut(
permissionName = {"PHONE", "STATUS"}
)
public void customMethod() {
JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
DemoAspect var10000 = DemoAspect.aspectOf();
Object[] var2 = new Object[]{this, var1};
var10000.aroundMethod((new MainActivity$AjcClosure1(var2)).linkClosureAndJoinPoint(69648));
}
建议新建一个aop相关模块 , 方便 aop 作为Demo 项目的调试,后期可以轻松依赖进自己的工程项目。
首先创建一个 lib-aop 模块,作为 library 方式引入到 Demo 项目中
关键有以下两点 :
aspectj 相关jar包 : maven仓库地址.
示例项目中所用 jar 包 : aspectjrt-1.8.13.jar. ( 放置 lib-aop 模块内 libs 文件夹下)
// aop 模块内 build.gradle
apply plugin: 'com.android.library'
apply from: '../lib-aop/aspectj-configure-lib.gradle' // ajc 编译所需gradle脚本
android {
compileSdkVersion 27
defaultConfig {
minSdkVersion 17
targetSdkVersion 27
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
//依赖方式 1. 使用本地jar包
api fileTree(include: ['*.jar'], dir: 'libs') // 作用范围一定得是 api !!!
//依赖方式 2. 配置 maven 地址
// api 'org.aspectj:aspectjrt:1.8.13'
}
建议 在 lib-aop 模块 内新建 aspectj-configure-lib.gradle 文件,脚本内容为以下代码
// aspectj-configure-lib.gradle
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.13'
classpath 'org.aspectj:aspectjweaver:1.8.13'
}
}
repositories {
mavenCentral()
}
android.libraryVariants.all { variant ->
if (variant.buildType.isDebuggable()) {
// return; //开放后debug模式不生效
}
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", 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
}
}
}
}
android.libraryVariants.all
,这只适用于 类型为 library 的 module 享用android.applicationVariants.all
,即可.在 Demo 工程中的 app 目录下,配置 build.gradle 脚本文件
// build.gradle
apply plugin: 'com.android.application'
// ajc 编译所需gradle脚本,application适用
apply from: '../lib-aop/aspectj-configure-app.gradle'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.demo.aop"
minSdkVersion 17
targetSdkVersion 27
versionCode 1
versionName "1.0"
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation project(":lib-aop")// 依赖 aop 模块,这里对作用范围没有限制.
}
上文说到 aspectj-configure-app.gradle 与 aspectj-configure-lib.gradle 的不同点只在于 第22行,
// aspectj-configure-app.gradle
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.13'
classpath 'org.aspectj:aspectjweaver:1.8.13'
}
}
repositories {
mavenCentral()
}
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (variant.buildType.isDebuggable()) {
// return; //开放后debug模式不生效
}
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 便配置完成了,另外,下面几点细节提醒 :
-dontwarn com.xxx.aop.**
-keep class com.xxx.aop.**{*;}
app : apply from : '../lib-aop/aspectj-configure-app.gradle'
library : apply from : '../lib-aop/aspectj-configure-lib.gradle'
并且依赖 lib-aop 模块(如果有公用base模块,作用范围可以用 api ,其它模块就可以不用再次添加依赖了。)
implementation project(":lib-aop")// 依赖 aop 模块
java.lang.NoClassDefFoundError: Failed resolution of: xxx/xxx/具体类
if (variant.buildType.isDebuggable()) {
return;
}
java.lang.RuntimeException: java.io.IOException: Failed to delete C:\Users\..\build\intermediates\intermediate-jars\debug\classes.jar
关闭任务管理器中 java.exe 进程 ,再次编译即可.
正在上传中…
先理清概念 : Android AOP面向切面编程AspectJ.
再深入了解 阿拉神农的 :深入理解Android之AOP. 大多数博客都参考该篇文章
最后android配置 aspectj : AndroidStudio 配置 AspectJ 环境实现AOP.