推荐链接:https://blog.csdn.net/Innost/article/details/49387395
1.前言
关于OOP和AOP的区别介绍,本文就不再详细阐述了,一种是面向对象编程,一种是面向切面编程。从编码思想上有些不太一样。下面我们直接从实战操作中来学习Android中的AOP
2.需求
2.1 需要给每个方法加上日志打印
我们先写一段代码 作为基础代码,我们在此之上 加上我们的需求
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
}
}
现在我们的需求就是需要在这个类上(实际项目需求不止这一个类,几十几百个),每个方法加上日志打印,打印出每个方法的调用顺序,那么我们传统做法是怎么样呢? 大家想到的肯定就是在这一个类每个地方加上日志打印类似下面这样。
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onResume() {
Log.d(TAG, "onResume: ");
super.onResume();
}
@Override
protected void onStart() {
Log.d(TAG, "onStart: ");
super.onStart();
}
@Override
protected void onPause() {
Log.d(TAG, "onPause: ");
super.onPause();
}
@Override
protected void onStop() {
Log.d(TAG, "onStop: ");
super.onStop();
}
}
当然这样也可以达到相同的目的,但是一个项目成百上千个方法,难道一个一个的这样去加吗?有没有更好的方式呢? 哈哈,这里就可以用aop的方式来解决拉,什么是AOP呢,它是怎么解决的呢,不要着急,接下来我们先不要想什么是AOP,OOP的,这些咱们都先不谈,我们先学习一个新东西。AspectJ
3.AspectJ快速入门
3.1 AspectJ介绍,
来自百度百科:
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
AspectJ官网地址: https://www.eclipse.org/aspectj/
这里我也就不讲一些你们不愿意看,我也不愿意看的一些东西了,东西太多了,只有慢慢看才能理解,既然是快速入门,我们就直接来点干货,
but,但是,在干之前建议大家还是要多看看开头推荐的那篇文章。其中有很多基础知识讲得挺好的。这里我也在重复讲一遍,也加深下自己的记忆。
首先我们要知道几个关键的知识点,我们会用到的。
1.Point
2.PointCut
3.Advice
3.2 Point
1.什么是Point呢?
答:Point,故名思意,就是一个点,AOP是面向切面编程, 也可以说是面向方法编程,那么这个点其实就是切入方法的点.
3.3 PointCut
1.什么是PointCut?
其实和Point差不多,这里的切入点就需要我们用AspectJ内部定义的关键字来声明不同的切入类型,比如是调用方法的时候,还是执行方法的时候,还是在静态方法执行的时候等等,接下来我们会讲到不要着急。
3.4 Advice
1.什么是Advice?
这里和Hook差不多的意思,这里大家只需要记住就是类似于hook要做的一些事情,内部也是由AspectJ为我们提供了三个关键注解,来帮助我们如何在什么时候切入。
接下来看看官方提供表格参考,等下实例会结合这个表格参考使用。
4.需求实现
好的关于AspectJ 大家只需要记住上面三个就行了,接下来看看使用AspectJ是如何实现的。
这就是我们通过AspectJ来实现为每个方法添加一条打印日志的记录的主要代码,当然我们还需要关联上我们的项目程序,光这个一段代码还没有我们项目建立关系,这里的建立关系,是通过gradle任务来执行的,下面我们看看。
如果你是App主项目则可以使用下面的代码
android.applicationVariants.all { variant ->
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)]
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;
}
}
}
如果你是放到model项目中可以使用下面的代码,(实际上就是替换applicationVariants 和 libraryVariants )
android.libraryVariants.all { variant ->
LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
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", plugin.project.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;
}
}
}
}
依赖(应该懂吧,各自加在什么地方)
classpath 'org.aspectj:aspectjtools:1.8.1'
api 'org.aspectj:aspectjrt:1.8.1'
以上代码为模板代码,无须修改。这段代码的作用就是在我们Gradle执行任务的时候,会使用aspectj内部的编译器,把我们的class字节码文件处理一遍,处理功能就是我们上面编写的添加日志的那段代码,根据我们给出的规则,来修改我们的字节码文件,达到切面的目的。
接下来我们先看看实际结果是怎么样。
可以看到这里的代码我们并没有加入任何代码,日志打印出了我们使用AspectJ中添加的代码,使用了我们的逻辑,接下来我们来分析上面的 代码是何含义。
这里有几个AspectJ的知识点我们需要继续学习。
5.AspectJ的知识点
1.AspectJ的知识点
5.1 @Aspect
作用于类上,声明这个类是一个AspectJ的类,在代码编译的时候会通过该注解找到这个类
5.2 @Pointcut(表达式)
作用于方法之上,表示这个方法是一个切入点,这个方法可以不用实现,但是必须要有这样一个方法。因为会在Advice中用到。
重要的是我们来看看里面的表达式,才是要将的重点核心,表达式规则
point [@注解] [访问权限] 返回值的类型 类全路径名(包名+类名).函数名(参数)
解释:
[] 里面的内容是可选项,意思是可以不用填写,
point 最后再讲,
@注解 表示可以匹配自定义注解相关的,
访问权限 就是java中的访问权限,如public ,private等,可以不填写 默认支持所有类型
返回值的类型 可以填写具体的返回值类型,比如String, int 或者支持任意返回类型 *
类全路径名(包名+类名) 这里也可以使用*代替,表示任意包,也可以指定具体的包名,中间可以用通配符*代替
函数名 方法名,也可以使用*代替 表示所有方法,也可以指定固定的方法
参数 参数如果填写 .. 表示任意参数,如果填写(String,..) 表示至少有一个且第一个是String类型的参数,后面是任意参数类型
上面的解释已经解析完了表达式的含义,这里还有一个要说的就是通配符,AspectJ 中的通配符
然后,别忘了上面还有一个point没有讲解,这个表示其实就是我们将要切入点的类型,在上面已经提到过一次了,再看看
这里已经列出了切入类型 我们先学习2个,一个 call 一个 execution ,可以看到官方给出的说明,一个是方法调用的时候,一个方法执行的时候。
5.3 回顾分析
到这里,我们再来看看这段代码
我们看到这里有一个表达式
@Pointcut("execution(* *.*(..))")
这里的含义是什么意思呢,首先是execution 表示方法执行的时候,然后是一个号,通过上面的表达规则,我们可以知道这第一个表示返回值,前面都省略了,然后是一个*.*(…) 这里的含义当然任意包下的任意方法,然后方法里面的参数任意。 到了这里就知道我们要注入的代码的切入点在哪里了把,任意类的任意方法,接下来还有一个
Advice没有讲,前面也提到了 Advice 就是hook代码的地方,这里填写我们的代码,但是Aspect里面没有Advice的注解,而是 @Before @After @Around,三个注解来表示
5.4 Advice
@Before 代码执行在方法之前
@After 代码注入在方法之后
@Around 可覆盖原来的方法
5.5 代码切入
@Aspect
public class MethodAOP {
@Pointcut("execution(* *.*(..))")
public void logForActivity() {
}
@Before("logForActivity()")
public void myLogPrint(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Log.e("===>", "当前调用的方法为:" +joinPoint.toString() +" , "+ signature.getMethod().getName());
}
}
上面我们可以看到 是@Before的关键字,表示执行在方法之前,里面有一个字符串是logForActivity这样的字符串 这里需要传入的是用@Pointcut声明的方法,可以传入多个,用逗号分隔
最后我们的实现方法里面打印了如下log
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Log.e("===>", "当前调用的方法为:" +joinPoint.toString() +" , "+ signature.getMethod().getName());
接着我们来看看编译后的class字节码文件,看看是不是如我们程序所写的一样,代码会注入到执行的方法的前面
现在可以看看,这是编译后的class文件,看看刚刚我们写的需要切面注入的代码是不是注入到了调用方法之前呢,答案一看就知道了吧,也可以看到,最后还是调用我们的myLogPrint 这个方法,执行的就是我们通过AspectJ 里面的那个对应的方法。
那么我们再来试试call 这个execution的区别,刚刚讲了,call表示你要调用这个方法的时候打印 ,比如有A,B2个方法,A方法,里面调用了B方法,那么这个代码插入就会在A方法里面,因为是Call操作,如果是execution那么这个就会执行在B方法中,来看看代码吧
@Aspect
public class MethodAOP {
@Pointcut("call(* cn.xiaxiayige.aspectdemo.*.*(..))")
public void logForActivity() {
}
@Before("logForActivity()")
public void myLogPrint(JoinPoint joinPoint) {
Log.e("===>", "当前调用的方法为:");
}
}
然后修改下我们的测试代码
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn=findViewById(R.id.btn);
}
private void AMethod() {
//insert code
BMethod();
}
private void BMethod() {
}
}
上面我用注释标出了预计我们的代码会插入的位置,那么我们编译一下 看看结果
可以看到图上,有三次地方插入了我们的代码,先看下最后一处,正式我们预计的那样,会插入到A方法中,B方法中没有代码插入,但是看看前面2个插入的地方,为什么这里也会有代码插入呢,可以看到我已经用绿色框框出了一些地方,这几个地方就是调用方法的地方,估计大家看一遍以后有个疑问就是,不是还有一个super.onCreate方法吗,上面怎么没有插入代码呢? 因为在上面的时候 我的Point是
call(* cn.xiaxiayige.aspectdemo.*.*(..))
指定了固定的包名下面的任意类,任意方法,super.onCreate调用的方法就不再是我们这个包名下面的方法了,看看其余三个,都是有this开头调用的方法,说明是调用的当前类的方法,当前类肯定是我指定在我包名下面的方法了呀,所以,这里就能够解释为什么super.onCreate方法前面没有代码插入。
接下来我们看看@Before @After @Around 这三个方法的注解使用,其中@Before 前面已经讲过了, 就是在方法执行的前面插入,那么@After肯定就是在方法执行之后,再执行我们插入的代码了, 主要是我们要讲一下@Around 这个的使用,@Around 这个注解表示可以覆盖原来的方法,但是也可以执行原来的方法,当然这里就可以执行我们自己的一些逻辑啦,比如一些条件判断。接下来我们看看效果 先修改一下测试代码
我们的切入点是指定的包名下面以Method结尾的方法,如果是A方法的话,执行完我们的代码之后继续执行原来程序代码,B方法则完全覆盖成我们的代码
@Aspect
public class MethodAOP {
@Pointcut("execution(* cn.xiaxiayige.aspectdemo.*.*Method(..))")
public void logForActivity() {
}
@Around("logForActivity()")
public void myLogPrint(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getMethod().getName();
Log.e("===>", "当前调用的方法为:" + joinPoint.toString() + " , " + name);
if ("AMethod".equals(name)) {
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
}
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn=findViewById(R.id.btn);
AMethod();
}
private void AMethod() {
Log.d(TAG, "AMethod");
}
private void BMethod() {
Log.d(TAG, "BMethod");
}
}
来看看 我们编译后的代码是不是我们预期的那样,A方法添加插入代码,然后继续执行,B方法覆盖掉原来的代码。
可以看到,这里已经按照我们的切入点规则,Method结尾的部分都已经注入了我们的方法,最后看看程序执行后的打印
上面就是程序执行的结果了,先打印了我们想要切入的代码,然后执行了程序原有的A方法 B方法并没有背执行,因为我们在程序中之判断了A方法可以被执行,忽略了B方法。
6.总结
从开始到现在,我们已经能够完成需求一了。就是能够再每个方法里加上自己的代码,打印处每个方法的方法名称。是不是很简单呢,就是三步骤。
1.首先要确定你要切面的点,切入的逻辑是什么,使用call 或者execution或者其他的(前面表格中那些)都是可以被切入的部分。
2.然后定义一个方法,使用@PointCut注解在方法之上,填写你的Point规则,
3.然后在使用@Before ,@After @Around 来确定你想要插入代码到具体的位置执行。
只需要通过以上三个步骤即可完成以上我们的需求,当然,更高级的用法还等着大家一起去探索。
推荐学习链接;
可以结合起来一起看
https://juejin.im/post/58ad3944b123db00672cdeeb
https://www.eclipse.org/aspectj/doc/released/progguide/quick.html