文章目录
AOP 简介
Android Studio 想接入 AspectJ ? 看这篇就对了!从0到1 , 包会!
OOP ( Object Oriented Programming ) 面向对象编程思想
AOP ( Aspect Oriented Programming ) 面向切面编程思想
- OOP : 面向对象即针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元模块划分。也就是把各个独立的功能封装为个体或模块。
如下图,每一个模块封装了其特有的功能属性,各尽其责,便于其它使用者的调用和复用。
- AOP : 针对业务处理过程中特定的切面
如下图,在特定的切面进行代码 「织入」(注意用词,是织入不是hook…),添加共同逻辑( 日志,埋点等) 且不会影响原有模块的业务功能和架构。
OOP 的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有问题都能完美得划分到模块中。举个最简单而又常见的例子:现在想为每个模块加上日志功能,要求模块运行时候能输出日志并统计当前方法耗时。
这个问题放在 OOP 思想中解决的办法通常是设计日志模块,并且在需要统计的地方一一手动加入日志的 API,并且如果日志的 API 改动,那将牵一发而动全身。
这个时候 AOP 的用途便体现出来了。
AOP 主要用途有:日志记录,行为统计,安全控制,事务处理,异常处理,系统统一的认证、权限管理等。可以使用 AOP 思想将这些代码从业务逻辑代码中划分出来,通过对这些行为的分离,可以将它们独立到非主导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
Android AOP 实现原理
上图中 AspectJ 是 Android 实现 AOP 编程思想的具体工具
AspectJ 简介
AspectJ 的使用核心就是它的编译器,它就做了一件事,将 AspectJ 的代码在编译期插入目标程序当中,运行时跟在其它地方没什么两样,因此要使用它最关键的就是使用它的编译器去编译代码 (AspectJ compile) 。
ajc 会构建目标程序与 AspectJ 代码的联系,在编译期将 AspectJ 代码插入被切出的 PointCut 中,达到 AOP 的目的。
也就是在 .java
文件编译为.class
文件的时候,对刚编译完的.class
字节码文件做手脚,对相应切入点的代码进行功能代码「织入」。
下段内容摘自张绍文极客时间-编译插桩的三种方法.
AspectJ是 Java 中流行的 AOP(aspect-oriented programming)编程扩展框架,网上很多文章说它处理的是 Java 文件,其实并不正确,它内部也是通过字节码处理技术实现的代码注入。
从使用上来看,作为字节码处理元老,AspectJ 的框架的确有自己的一些优势。
- 成熟稳定 : 从字节码的格式和各种指令规则来看,字节码处理不是那么简单,如果处理出错,就会导致程序编译或者运行过程出问题。而 AspectJ 作为从 2001 年发展至今的框架,它已经很成熟,一般不用考虑插入的字节码正确性的问题。
- 使用简单 : AspectJ 功能强大而且使用非常简单,使用者完全不需要理解任何 Java 字节码相关的知识,就可以使用自如。它可以在方法(包括构造方法)被调用的位置、在方法体(包括构造方法)的内部、在读写变量的位置、在静态代码块内部、在异常处理的位置等前后,插入自定义的代码,或者直接将原位置的代码替换为自定义的代码。
Android AOP 基本实现方式
上面说道 : AOP是一个编程思想和概念,本身并没有设定具体语言的实现,这实际上提供了非常广阔的发展的空间。
AspectJ 是 AOP 的一个很悠久的实现,它能够和 Java 配合起来使用。( 很稳 , 支付宝app第三方开源也有用到)
先来了解几个 AspectJ 的 基本和主要的 关键词 :
- Aspect : Aspect 声明类似于 Java 中的 类声明 ,在Aspect中会包含着一些 Pointcut (切入点)以及相应的 Advice (通知) , 「Pointcut 和 Advice 的组合可以看做切面」
- Advice : ( 通知 ) , 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个JoinPoint (连接点)之前、之后还是完全替代目标方法 的代码。 --> when
- Pointcut : ( 切入点 ) , 告诉代码注入工具,在何处注入一段特定代码的表达式。 --> where
下面是 Pointcut 筛选和匹配条件.(为了更精确的找到要切入地方.)
筛选条件 | 说明 | 示例 |
---|---|---|
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,后面参数个数和类型不限 |
… | 其它高级用法 |
- JoinPoint : ( 连接点) , 表示 aspectj 支持代码织入的位置 ,例如典型的 方法函数调用 , 类成员的访问 以及对 异常处理程序块 的执行等等,这些都是 JPoint 。(如
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)
}
aop 初体验
实体类 , 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() ) ")
- call 表示方法函数调用即要截取的地方
- 第一个位置表示方法调用的返回值,*表示返回值为任意类型
- getName表示方法名,也可以使用该方法的全限定名,getName()的括号()代表这个方法的参数,可以指定类型,或者(…)(int,…)这样来代表任意类型和个数的参数。
- 同时在call()后面可以 加入&&、||、!来进行条件组合,匹配或过滤关键 JPoint
@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 到底做了什么骚操作
// 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 开发手册.
自定义PointCut
例如我们要在代码中进行权限检查,如果没有权限则不执行方法(或者执行权限调用方法,权限申请成功后再执行目标方法)
- 1.创建自定义注解行为
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomPointCut {
String[] permissionName();
}
- 2.方法调用,在MainActivity调用注解方法
@CustomPointCut(permissionName = {"PHONE", "STATUS"})
public void customMethod() {
Toast.makeText(this, "customMethod call", Toast.LENGTH_SHORT).show();
}
- 3.声明使用 Aspect 的类
@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));
}
Android AspectJ 接入实战
1.添加aop模块,配置依赖
建议新建一个aop相关模块 , 方便 aop 作为Demo 项目的调试,后期可以轻松依赖进自己的工程项目。
首先创建一个 lib-aop 模块,作为 library 方式引入到 Demo 项目中
关键有以下两点 :
- 依赖 ajc 编译脚本,我们将脚本内容写 aspectj-configure-lib.gradle 文件中,aop 模块引用该脚本
- 添加 aspectjrt 依赖,添加的方式有两种,如果是多人协作建议下载 jar 包配置到本地依赖.
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'
}
- 踩坑1 :aspectjrt 依赖的作用范围一定得是 api ,否则其它模块死活织入不了代码
2.配置 ajc 脚本
建议 在 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
}
}
}
}
- 踩坑2 :aspectj-configure-lib.gradle 只适用于lib 模块!
在 aspectj-configure-lib.gradle 的第 21行中配置到 :android.libraryVariants.all
,这只适用于 类型为 library 的 module 享用
application 的 module 只需要将这行配置改为android.applicationVariants.all
,即可.
3.配置 app 的脚本
在 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
}
}
}
}
4.细节提醒
如此一来,aspectj 便配置完成了,另外,下面几点细节提醒 :
- lib-aop 模块内需配置 proguard-rules.pro 混淆相关,将 lib-aop 内相关代码的包名添加进配置中,防止混淆.
-dontwarn com.xxx.aop.**
-keep class com.xxx.aop.**{*;}
- 其它模块如果需要在编译时期使用 aspectj 代码织入功能,需要加入gradle脚本 ajc 配置
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 模块
- 踩坑3 :lib-aop 不能打成 aar !
编写 aspect 相关的模块不能打成 aar ,需要模块项目引用,否则编译打包后会找不到类java.lang.NoClassDefFoundError: Failed resolution of: xxx/xxx/具体类
- 踩坑4 :J神的 hugo 插件 debug 时期 ajc 编译无效 , 因为在 ajc 编译脚本 gradle 中,如果是 Debuggable , return … ( 这一点估计坑了许多人. )
if (variant.buildType.isDebuggable()) {
return;
}
- 踩坑5 :AS编译时期 IOException ,或文件被占用.
java.lang.RuntimeException: java.io.IOException: Failed to delete C:\Users\..\build\intermediates\intermediate-jars\debug\classes.jar
关闭任务管理器中 java.exe 进程 ,再次编译即可.
- 踩坑6 :引入的第三方 library aar 或者是 .class 文件能够织入代码吗?
不能! 详见下图.
源码下载
正在上传中…
参考
先理清概念 : Android AOP面向切面编程AspectJ.
再深入了解 阿拉神农的 :深入理解Android之AOP. 大多数博客都参考该篇文章
最后android配置 aspectj : AndroidStudio 配置 AspectJ 环境实现AOP.