AspectJ

AspectJ

面向切面编程(AOP,Aspect-oriented programming) 在 Android 中最流行的实践工具就是 AspectJ。

这篇文章就是来介绍 AspectJ 以及在 Android 中使用。


AspectJ

AspectJ 是一种几乎和 Java 完全一样的语言,而且完全兼容 Java(AspectJ 应该就是一种扩展Java)。

使用 AspectJ 有两种方法:

  • 纯 AspectJ 的语言。这语言一点也不难,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
  • 纯 Java 语言开发,然后使用 AspectJ 注解,简称 @AspectJ

不论哪种方法,最后都需要 AspectJ 的编译工具 ajc 来编译。由于 AspectJ 实际上脱胎于 Java,所以 ajc 工具也能编译 java 源码。

AspectJ官方网站

AspectJ文档


AspectJ 简单概念

要使用 AspectJ ,必须要先弄懂几个概念。


Join Points(连接点)

Join PointsAspectJ 中最关键的一个概念,它表示程序中可能作为代码注入目标的特定的点和入口。

比如说,一个方法的调用或者方法的执行是一个 Join Points ,设置或者读取一个变量也是一个Join Points

理论上说,一个程序中很多地方都可以被看做是 Join Points,但是 AspectJ 中,只有下面所示的几种执行点被认为是 Join Points

Join Points 说明 示例
method call 函数调用 比如调用Log.e(),这是一处Join Points
method execution 函数执行 比如Log.e()的执行内部,是一处Join Points
constructor call 构造方法调用 和 method call 类似
constructor execution 构造函数执行 和method execution类似
field get 获取某个变量 比如读取 DemoActivity.debug 成员
field set 设置某个变量 比如设置DemoActivity.debug成员
pre-initialization Object在构造方法中做得一些工作。
initialization Object在构造方法中做得工作
static initialization 类初始化 比如类的static{}
handler 异常处理 比如try catch(xxx)中,对应catch内的执行
advice execution 这个后面说

简单直白点说, Join Points 就是一个程序中的关键方法(包括构造方法)和代码段(staticblock)。

你想把新的代码插在程序的哪个地方,是插在构造方法中,还是插在某个方法调用前,或者是插在某个方法中,这个地方就是 Join Points ,当然,不是所有地方都能给你插的,只有支持的地方,才叫 Join Points


Advice(通知)

Advice 有三种方式 Before、After、Around ,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。


例如:

1
2
3
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
}

这里会分成几个部分,我们依次来看:

  • @Before :这个就是Advice,表示具体插入的方式。

  • execution :处理Join Point的类型,例如call、execution

  • (* android.app.Activity.on\(..))** :这个是最重要的表达式,代表源码中需要 hook 的地方。

    第一个位置表示返回值,* 表示返回值为任意类型,如果用注解,这里也需要填写@注解全名

    第二个位置表示具体位置,可是包名路径,其中可以包含 来进行通配,几个 没区别。

    第三个位置()代表这个方法的参数,你可以指定类型,例如android.os.Bundle,或者(..)这样来代表任意类型、任意个数的参数。

    同时,这里可以通过&&、||、!来进行条件组合。

  • public void onActivityMethodBefore :实际切入的代码。

BeforeAfter 其实还是很好理解的,也就是在 Pointcuts 之前和之后,插入代码, Around 可以完全替代目标方法执行的代码,他包含了 BeforeAfter 的全部功能,代码如下:

1
2
3
4
5
6
7
@Around("execution(* com.xys.aspectjxdemo.MainActivity.testAOP())")
public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String key = proceedingJoinPoint.getSignature().toString();
Log.d(TAG, "onActivityMethodAroundFirst: " + key);
proceedingJoinPoint.proceed();
Log.d(TAG, "onActivityMethodAroundSecond: " + key);
}

其中,proceedingJoinPoint.proceed() 代表执行原始的方法,在这之前、之后,都可以进行各种逻辑处理,也可以不执行原始方法,进行完全替换。


Pointcut(切入点)

告诉代码注入工具,在何处注入一段特定代码的表达式。


Aspect(切面)

Pointcut 和 Advice 的组合看做切面。例如,我们在应用中通过定义一个 pointcut 和给定恰当的advice,添加一个日志切面。


Weaving(织入)

注入代码(advices)到目标位置(joint points)的过程。


一张图简要总结了一下上述这些概念。

AspectJ



AspectJ 的使用

Android中配置 Aspectj 是特别麻烦的,我也试过最原始的配置方式,花了好长时间才搞定,非常麻烦。

好在江户的工作组已经简化的这件事情。他们在 Github 上开源了 gradle_plugin_android_aspectjx 这个库,非常棒,这里就直接使用这个库,省下了很多繁琐的步骤。


接入说明

  1. 在项目的根目录的build.gradle文件中添加依赖,修改后文件如下
1
2
3
4
5
6
7
8
9
10
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
  1. 在编写 AspectJ 的项目或者库中,在其 build.gradle 文件中添加 AspectJ 的依赖
1
compile 'org.aspectj:aspectjrt:1.8.9'
  1. 在使用 AspectJ 的项目中加入 AspectJX 的插件:
1
apply plugin: 'android-aspectjx'

aspectjx 默认会遍历项目编译后所有的 .class 文件和依赖的第三方库,去查找符合织入条件的切点,为了提升编译效率,可以加入过滤条件指定遍历某些库或者不遍历某些库。

includeJarFilterexcludeJarFilter 可以支持 groupId 过滤,artifactId 过滤,或者依赖路径匹配过滤

1
2
3
4
5
6
aspectjx {
//织入遍历符合条件的库
includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library'
//排除包含‘universal-image-loader’的库
excludeJarFilter 'universal-image-loader'
}

更多详细用法,直接查看 Github 文档


使用

我们首先应用一个最简单的案例。

创建一个 AspectTest 的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Aspect  //必须添加@Aspect 注解
public class AspectTest {
final String TAG = AspectTest.class.getSimpleName();

@Before("execution(* *..MainActivity+.on**(..))")
public void method(JoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = joinPoint.getThis().getClass().getSimpleName();

Log.e(TAG, "class:" + className);
Log.e(TAG, "method:" + methodSignature.getName());
}
}

MainActivty的代码如下:

1
2
3
4
5
6
7
8
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}

这样完成了一个简单的应用。


AspectJ 一般会配合注解来使用,在特定注解的位置处理代码。

再来一个案例,通过注解来打印方法的耗时:

  1. 定义注解

    1
    2
    3
    4
    @Retention(RetentionPolicy.CLASS) //注意,不能是 RetentionPolicy.SOURCE
    @Target({ElementType.METHOD,ElementType.CONSTRUCTOR})
    public @interface Dove {
    }

    注意,配合 AspectJ 时,注解的生命周期一定要定义成 CLASS 或 RUNTIME ,如果定义成 SOURCE,根据其原理可知,它找不到这个注解的。

  2. 创建 AspectJ 的处理类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    @Aspect  //必须添加@Aspect 注解
    public class AspectDovePlugin {

    @Pointcut("execution(@com.deemons.dove.Dove * *(..))")//方法切入点
    public void methodAnnotated() {
    }

    @Pointcut("execution(@com.deemons.dove.Dove *.new(..))")//构造器切入点
    public void constructorAnnotated() {
    }

    @Around("methodAnnotated()||constructorAnnotated()")
    public Object method(ProceedingJoinPoint joinPoint) throws Throwable {
    //AspectJ 的一些 API,用于获取信息。
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    String canonicalName = methodSignature.getMethod().getDeclaringClass().getCanonicalName();
    String methodName = methodSignature.getName();
    long startTime = System.nanoTime();
    Object result = joinPoint.proceed();//执行原方法

    //拼接方法调用的参数,以及 方法执行的时间。
    StringBuilder keyBuilder = new StringBuilder();
    keyBuilder.append(canonicalName);
    keyBuilder.append(String.format(" : %s (", methodName));
    for (Object obj : joinPoint.getArgs()) {
    if (obj != null) {
    String format =
    String.format(Locale.getDefault(), "%s = %s", obj.getClass().getSimpleName(),
    obj.toString());
    keyBuilder.append(format);
    } else {
    keyBuilder.append("null");
    }
    }
    long during = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
    keyBuilder.append(String.format(Locale.getDefault(), ") --->:[%d ms]", during));
    Log.d("Dove", (keyBuilder.toString()));// 打印时间差
    return result;
    }
    }
  3. 使用注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MainActivity extends AppCompatActivity {

    @Dove
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    test("test param",88);
    }

    @Dove
    private void test(String s,int i) {
    Log.d("","do test()");
    }
    }

    打印结果如下:

    1
    2
    test (String = test paramInteger = 88)   --->:[0 ms]
    onCreate (null) --->:[37 ms]

    这样就完成了方法耗时以及调用参数的打印工作。



参考