AOP开发——AspectJ的使用
文章对应的项目地址aop-tech,运行一下sample,结合代码和文章,你会收获更多。
熟悉程序开发的都知道OOP(Object Oriented Programming ,面向对象编程),把功能封装在一个类中,使用的时候创建该类的对象,调用对象的方法或者使用其属性即可,OOP具有可重用性、灵活性和扩展性。
尽管OOP具有很多好处,但是如果在软件开发领域只使用OOP,在某些情况下也会使程序变得复杂且难以维护。例如,我们需要统计程序中点击事件的执行情况,如果我们要自己找遍代码中的点击事件,这个工程量就太大了,而且维护起来也不方便。这个时候,使用AOP的方式就会使问题变得简单。
AOP(Aspect Oriented Programming,面向切面编程),把某一类问题集中在一个地方进行处理,比如处理程序中的点击事件、打印日志等。
关于OOP和AOP,我觉得邓凡平老师在深入理解Android之AOP中说的挺对的:
OOP和AOP都是方法论,表示的是我们从什么角度来看待问题。OOP的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有功能都能完美得划分到模块中。AOP的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。
那么在Android中有哪些使用到了AOP这种思想呢?
在Application中有个ActivityLifecycleCallbacks接口,这个接口提供了Activity生命周期相关的方法回调。当开发者调用了Application的public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback)
方法之后,就可以在ActivityLifecycleCallbacks的实现类中统一处理这些生命周期方法。这其实就是AOP思想的一种体现。
另外,我们今天的主角——AspectJ, 它是AOP编程思想的一个很火的实践。
AspectJ 介绍
AspectJ是一个面向切面编程的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。AspectJ还支持原生的Java,只需要加上AspectJ提供的注解即可。在Android开发中,一般就用它提供的注解和一些简单的语法就可以实现绝大部分功能上的需求了。
Join Points介绍 **
Join Points,简称JPoints,是AspectJ中最关键的一个概念,表示的是程序运行时的一些执行点**。理论上说,一个程序中很多地方都可以被看做是JPoint,但是AspectJ中,只有几种执行点被认为是JPoints,如构造方法调用、方法调用、方法执行、异常等等。JPoints实际上就是表示想把AspectJ的代码插入到程序哪个地方,是插入在方法中,还是插入在方法调用前后。需要说明的是:在AspectJ中,方法调用(call)和方法执行(execution)是不一样的,这个后面再做介绍。
Pointcuts介绍
一个程序会有很多的JPoints,即使是同一个函数,还分为call类型和execution类型的JPoint,但是并不是所有的JPoint都是我们需要关心的。比如我们可能只需要关心点击事件方法,那么如何从众多的JPoints中选择我们感兴趣的JPoint呢?这个时候可以用Pointcut:
@Around("execution(* android.view.View.OnClickListener.onClick(..))")
public void onClickMethodAround(ProceedingJoinPoint joinPoint) {}
上述代码的意思就是在OnClickListener.onClick()方法执行前后执行代码块中的逻辑。
所以在这里,我们可以简单的理解Pointcut的作用就是过滤JPoint。
Advice介绍
Advice简单来说就是表示AspectJ的hook点,在AspectJ中常用的是before、after、around等。before表示在JPoint执行之前,需要干的事情。after表示的是在JPoint执行之后,around表示的是在JPoint执行前后。
Aspect介绍
前面我们讲了AspectJ中使用过程中需要用到了一个概念,对于问题的处理需要统一放到一个地方去处理,这个地方就是Aspect,意为“切面”。在Java开发中主要是使用@Aspect注解来表示一个切面。
Android 中使用Gradle集成 AspectJ
在Android中集成AspectJ,主要思想就是hook Apk打包过程,使用AspectJ提供的工具来编译.class文件。这一点,JakeWharton 在其项目JakeWharton/hugo 中演示了如何在Gradle中添加AspectJ,这为后来的人指了一条光明的道路。
一般来说,自己手动接入AspectJ的话,按照下面的指示即可。
在项目根目录build.gradle下引入aspectjtools插件:
buildscript {
dependencies {
..
classpath 'org.aspectj:aspectjtools:1.8.10'
classpath 'org.aspectj:aspectjweaver:1.8.8'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
在运行app的module目录下的build.gradle中引入:
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在运行时也需要相关的Library支持,所以还需要在项目的dependencies中添加依赖:
dependencies {
...
compile 'org.aspectj:aspectjrt:1.8.10'
}
目前还有一些在Android中集成AspectJ的比较火的框架,如 HujiangTechnology / gradle_plugin_android_aspectjx。该框架支持kotlin,我对这个框架深入研究了一番,也按照它的思想写了一个简单的gradle plugin ,收获颇多,我自己的项目地址是 aop-tech,项目中演示了如何通过AOP的方式解决统一处理登录、绑定手机号、统计方法耗时、打印点击事件日志等的逻辑,有兴趣的可以去看看,欢迎交流。
AspectJ 命令常用参数介绍
1 -inpath: .class文件路径,可以是在jar文件中也可以是在文件目录中,路径应该包含那些AspectJ相关的文件,只有这些文件才会被AspectJ处理。输出文件会包含这些.class 。该路径就是一个单一参数,多个路径的话用分隔符隔开。
2 -classpath: 指定去哪找用户使用到的.class文件,路径可以是zip文件也可以是文件目录,该路径就是一个单一参数,多个路径的话用分隔符隔开。
3 -aspectPath: 需要被处理的切面路径,存在于jar文件或者文件目录中。在Andorid中使用的话一般指的是被@Aspect注解标示的class文件路径。需要注意的是编译版本需要与Java编译版本一致。classpath指定的路径应该包含所有的aspectpath指定的.class文件。不过默认情况下,inPath和aspectPath中的路径不一定非要放置在classPath中,因为编译器会自动处理把它们加入。路径格式与classpath和inpath样,都需要用分隔符隔开。
4 **-bootClasspath: ** 重载跟VM相关的bootClasspath,例如在Android中使用android-27的源码进行编译。路径格式与之前一样。
5 -d: 指定由AspectJ处理后的.class文件存放目录,如果不指定的话会放置在当前的工作目录中。
6 -outjar: 指定被AspectJ处理后的jar包存放的文件目录,
更多详情请查看官网 http://www.eclipse.org/aspectj/doc/released/devguide/ajc-ref.html
Sample—处理点击事件
例如,我们需要处理项目中的所有控件的点击事件,打印控件的名称,可以使用AspectJ来简单方便的处理。在之前已经在gradle中引入的AspectJ的基础上,我们新建一个Java文件,如下:
@Aspect
public class ClickAspect {
private static final String TAG = "ClickAspect";
// 第一个*所在的位置表示的是返回值,*表示的是任意的返回值,
// onClick()中的 .. 所在位置是方法参数的位置,.. 表示的是任意类型、任意个数的参数
// * 表示的是通配
@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")
public void clickMethod() {}
@Around("clickMethod()")
public void onClickMethodAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
View view = null;
for (Object arg : args) {
if (arg instanceof View) {
view = (View) arg;
}
}
//获取View 的 string id
String resEntryName = null;
String resName = null;
if (view != null) {
// resEntryName: btn_activity_2 resName: com.sososeen09.aop_tech:id/btn_activity_2
resEntryName = view.getContext().getResources().getResourceEntryName(view.getId());
resName = view.getContext().getResources().getResourceName(view.getId());
}
joinPoint.proceed();
Log.d(TAG, "after onclick: " + "resEntryName: " + resEntryName + " resName: " + resName);
}
}
运行项目,点击一个控件(设置了点击事件)之后,可以看到日志输出:
./com.sososeen09.aop_tech D/ClickAspect: after onclick: resEntryName: btn_activity_3 resName: com.sososeen09.aop_tech:id/btn_activity_3
切入点的语法
以上面的例子来讲解:
- @Around:是advice,也就是具体的插入点。@Around该方法的逻辑会包含切入点前后,如果用到该注解,记得自己需要控制切入点的执行逻辑,调用
joinPoint.proceed()
。如果使用@Before注解,表示的是在切入点之前执行,@After表示在切入点之后执行,此时不需要调用joinPoint.proceed()
。 - execution:处理JPoint的类型,例如call、execution。对于
execution(* android.view.View.OnClickListener.onClick(..))
,第一个*
所处的位置表示的是返回值,*
是通配符,表示的是任意类型。android.view.View.OnClickListener.onClick(..)
表示的执行OnClickListener的onClick()方法。onClick(..)
中的..
表示任意类型、任意个数的参数。 - onClickMethodAround:表示的实际切入代码。这个方法名可以自己随意定义。
在上面的例子中实际上我是自定义了一个PointCut,名字是clickMethod()
。这个名称随意,只要在advice中指定好该名称就可以了。
@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")
public void clickMethod() {}
如果不想自定义,可以直接这样:
@Around("execution(* android.view.View.OnClickListener.onClick(..))")
public void onClickMethodAround(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
call和execution
我们之前讲的切入点语法都是execution,那么如果使用call有什么区别呢?
我们再使用一个例子,创建一个切面用来打印方法的执行时间,并且只处理带有注解的参数。
TimeSpend 注册如下,value表示的是方法的功能
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TimeSpend {
String value() default "";
}
使用execution打印方法执行时间的切面如下:
@Aspect
public class MethodSpendTimeAspect {
private static final String TAG = "MethodSpendTimeAspect";
@Pointcut("execution(@com.sososeen09.aop_tech.aspect.TimeSpend * *(..))")
public void methodTime() {}
@Around("methodTime()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
String funName = methodSignature.getMethod().getAnnotation(TimeSpend.class).value();
//统计时间
long begin = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - begin;
Log.e(TAG, String.format("功能:%s,%s类的%s方法执行了,用时%d ms", funName, className, methodName, duration));
return result;
}
}
原始Java文件如下:
public class LoginActivity extends AppCompatActivity {
...
@TimeSpend("登录")
private void attemptLogin() {
StatusHolder.sHasLogin = true;
Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
finish();
}
}
编译之后的.class文件:
public class LoginActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
...
super.onCreate(savedInstanceState);
mEmailSignInButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
LoginActivity.this.attemptLogin();
}
});
}
@TimeSpend("登录")
private void attemptLogin() {
JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
attemptLogin_aroundBody1$advice(this, var1, MethodSpendTimeAspect.aspectOf(), (ProceedingJoinPoint)var1);
}
static {
ajc$preClinit();
}
}
如果把execution该为call,在看一下编译后的 .class 文件 :
public class LoginActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
mEmailSignInButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
LoginActivity.access$000(com.sososeen09.aop_tech.LoginActivity.this);
}
});
}
@TimeSpend("登录")
private void attemptLogin() {
StatusHolder.sHasLogin = true;
Toast.makeText(this, "登录成功", 0).show();
this.finish();
}
static {
ajc$preClinit();
}
static void access$000(LoginActivity x0) {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, null, x0);
attemptLogin_aroundBody1$advice(x0, makeJP, MethodSpendTimeAspect.aspectOf(), (ProceedingJoinPoint) makeJP);
}
}
看到区别了吧,execution表示JPoint是执行方法的地方,AspectJ会对被执行方法做处理。而call表示JPoint是调用方法的地方,AspectJ会对调用处做处理。
总结
本文介绍了AOP的一些概念性的知识,简单介绍了AspectJ在Android开发中的基本使用方式。限于篇幅和水平,难以对AspectJ做一个全面的介绍,建议对AOP和AspectJ有兴趣的读者可以阅读下面的相关项目和文章,也欢迎交流。