查看原文
其他

AspectJ在Android中的应用

狐友技术团队 搜狐技术产品 2021-01-15

☝点击上方蓝字,关注我们!

01

 AspectJ简介 

AspectJ是Java语言AOP(面向切面编程)的一种实现方案。AspectJ有两种实现方式,一种是使用Java语言和注解,然后通过AspectJ提供的编织器,编织代码到目标class文件。一种是直接使用AspectJ语言编写,然后使用ajc编译器用来编译aj文件,生成java标准的class文件。AspectJ语言的语法和Java一样,只是比Java语言多了一些关键字,但由于AndroidStudio并没有提供AspectJ语法的支持,所以在Android开发中使用AspectJ只能使用注解的方式来实现。


02

 一些基本概念 

下面通过注解的实现方式来解释一些基本的概念:

1. Aspect(切面)

一个切面是一个独立的功能实现,一个程序可以定义多个切面,定义切面需要新建一个类并加上@Aspect注解。例如:SampleAspect就是一个切面 ,这个切面实现了打印所有Activity中OnCreate方法耗时的功能。

1@Aspect
2public class SampleAspect {
3    @Pointcut("execution(void android.support.v4.app.FragmentActivity+.onCreate(..))")
4    public void activityOnCreate() {
5
6    }
7    @Around("activityOnCreate()")
8    public Object activityOnCreateTime(ProceedingJoinPoint joinPoint) {
9        Object object = null;
10        long startTime = System.currentTimeMillis();
11        try {
12            object = joinPoint.proceed();
13        } catch (Throwable throwable) {
14            throwable.printStackTrace();
15        }
16        Log.d("chao","activityOnCreateTime:"+(System.currentTimeMillis() - startTime));
17        return object;
18    }
19}

<< 向右滑动查看完整代码 >>

2. JointPoint(链接点)

链接点代表了程序中可以切入代码的位置,包括函数的调用和执行,类的初始化,异常处理等,链接点就是利用AspectJ可以侵入修改的地方。例如Activity中onCreate方法的调用和执行,Activity的初始化,构造方法的执行等,可以在这些JointPoint(链接点)切入自己想要的代码。


3. PointCut(切点)

切点是具体的链接点,切点定义了需要织入代码的链接点,切点的定义有专门的语法。例如:下面这个就代表一个切点,这个切点表示了所有Activity以及其子类的onCreate方法的执行。

1@Pointcut("execution(void android.support.v4.app.FragmentActivity+.onCreate(..))")
2public void activityOnCreate() {
3
4}

<< 向右滑动查看完整代码 >>

4. Advice(通知)

通知代表对切点的监听,可以在这里编写需要织入的代码。通知包括:@Before方法执行前,@After方法执行后,@Around方法执行前后。例如:下面分别表示了切点activityOnCreate的执行前、执行后、执行前后的监听,其中@Around需要自己处理方法的执行,并且必须放在@Before和@After之前。

1@Around("activityOnCreate()")
2  public Object activityOnCreateTime(ProceedingJoinPoint joinPoint) {
3      Object object = null;
4      long startTime = System.currentTimeMillis();
5      try {
6          object = joinPoint.proceed();
7      } catch (Throwable throwable) {
8          throwable.printStackTrace();
9      }
10      Log.d("chao","activityOnCreateTime:"+(System.currentTimeMillis() - startTime));
11      return object;
12
13  }
14
15  @Before("activityOnCreate()")
16  public void onCreateBefore(JoinPoint joinPoint) {
17      Log.d("chao""onCreateBefore" + joinPoint.getSignature().getDeclaringType() + ":" + joinPoint.getSignature().getDeclaringTypeName());
18  }
19  @After("activityOnCreate()")
20  public void onCreateAfter(JoinPoint joinPoint) {
21      Log.d("chao""onCreateAfter" + joinPoint.getSignature().getDeclaringType() + ":" + joinPoint.getSignature().getDeclaringTypeName());
22  }

<< 向右滑动查看完整代码 >>


03

 匹配表达式 

上面讲到了定义切点时需要用到专门的语法:匹配表达式。匹配表达式分为以下几种类型:

1. 方法类型

1[!] [@Annotation] [public,protected,private] [static] [final] 返回值类型 [类名.]方法名(参数类型列表) [throws 异常类型]

<< 向右滑动查看完整代码 >>

定义方法切点时需要用到方法类型的匹配表达式,其中execution()代表了方法的执行,[]中的内容为可选。 

如果是构造方法只需要把方法名替换为new。例如:

1@Pointcut("execution(@com.sohu.aspectj.AspectLog public static final boolean
2com.sohu.aspectj.MainActivity.getIsAnnotation(java.lang.String,boolean)throws 
3java.lang.Exception)"
)
4public void activityIsFirst() {
5
6}
7构造方法的切入点
8@Pointcut("execution(public com.sohu.aspectj.KotlinClassFile.new(java.lang.String,boolean))")
9public void kotlinClassInit() {
10
11}

<< 向右滑动查看完整代码 >>

2. 运算符以及特殊符号

  • !非 && 与 || 或

1//所有Fragment类以及其子类testParams方法的调用
2@Pointcut("execution(public * testParams(..)) &&
3target(android.support.v4.app.Fragment+) && args(args)"
)
4public void fragmentTestParams(boolean args,Fragment fragment) {
5
6}

<< 向右滑动查看完整代码 >>

  • +表示自身以及子类

  • *表示任意类型

  • ..表示任意长度类型

1//AspectLog注解的在包com.sohu.aspectj开头的所有包下的方法
2@Pointcut("execution(@com.sohu.aspectj.AspectLog * com.sohu.aspectj..*(..))")
3public void fragmentaspectLog(Fragment fragment) {
4
5}

<< 向右滑动查看完整代码 >>

3. handler指定异常的处理

1//com.sohu.aspectj包开头下的所有Exception的异常
2@Pointcut("handler(java.lang.Exception)&&within(com.sohu.aspectj.*)")
3public void fragmentException() {
4
5}

<< 向右滑动查看完整代码 >>

4. 其它类型

  • target是指切点所在的类的对象,只能是对象,例如:不能是target(android.support.v4.app.Fragment+)或者target(android.support.v4.app.*),只能是target(fragment)

  • with是指指定类中的所有链接点,可以包括包名和类名,例如:within(android.support.v4.app.Fragment+)或者within(android.support.v4.app.*)

1@Pointcut("execution(public * testParams(..)) && target(fragment) && args(args)")
2public void fragmentTestParams(boolean args,Fragment fragment) {
3
4}
5
6//Fragment及其子类中的所有链接点
7@Pointcut("execution(public * testParams(..)) &&
8within(android.support.v4.app.Fragment+) && args(args)"
)
9public void fragmentWithTestParams(boolean args,Fragment fragment) {
10
11}

<< 向右滑动查看完整代码 >>

以上是一些常见的匹配表达式类型,更加高级的用法可以查看官方文档:[匹配表达式官方文档][1]

04

 自定义注解 

AspectJ可以自定义注解,来对需要注入代码的地方增加标志,下面通过一个打印方法信息的例子来说明一下自定义注解的使用方式:

1. 编写注解类

使用RUNTIME是因为切面中要使用注解的值,如果切面中不使用注解的值则可以使用CLASS类型

1@Target({ElementType.FIELD,ElementType.METHOD,ElementType.CONSTRUCTOR,ElementType.TYPE})
2@Retention(RetentionPolicy.RUNTIME)
3public @interface AspectLog {
4    String[] checkString();
5    int checkCode();
6}

<< 向右滑动查看完整代码 >>

2. 为需要切入的方法加上注解

1@AspectLog(checkCode = 3,checkString = {"a","b","b"})
2public void execute(String out) {
3    try {
4        Thread.sleep(500);
5    } catch (InterruptedException e) {
6        e.printStackTrace();
7    }
8}

<< 向右滑动查看完整代码 >>

3. 编写切面类处理注解并打印方法的一些信息

需要注意的是获取注解值有两种方法:一种是加上@annotation()标志后增加AspectLog参数,一种是通过ProceedingJoinPoint得到注解值。分别对应了logAndExecute方法和logAndExecute2方法。

1@Aspect
2public class SampleAspect {
3    @Pointcut("execution(@com.sohu.aspectj.AspectLog * *(..))&&@annotation(hyaspect)")
4    // @AspectLog 修饰的方法的执行
5    public void method(AspectLog hyaspect) {
6
7    }
8    // @AspectLog 修饰的方法的执行
9    @Pointcut("execution(@com.sohu.aspectj.AspectLog * *(..))")
10    public void method2() {
11
12    }
13    // @AspectLog 修饰的构造函数的执行
14    @Pointcut("execution(@com.sohu.aspectj.AspectLog *.new(..))&&@annotation(hyaspect)")
15    public void constructor(AspectLog hyaspect) {
16
17    }
18    // @AspectLog 修饰的构造函数的执行
19    @Pointcut("execution(@com.sohu.aspectj.AspectLog *.new(..))")
20    public void constructor2() {
21
22    }
23
24    @Around("method(hyaspect) || constructor(hyaspect)")
25    public Object logAndExecute(ProceedingJoinPoint joinPoint,AspectLog hyaspect) throws Throwable {
26        String[] checkString = hyaspect.checkString();//得到注解checkString的返回值
27        Log.d("chao""-----AspectLog:needCheck:" + hyaspect.needCheck()+" checkCode:"+hyaspect.checkCode()+" checkString:"+checkString[0]);
28        //输出结果:-----AspectLog:needCheck:false checkCode:3 checkString:a
29        Log.d("chao""-----className:" + joinPoint.getSignature().getDeclaringType());
30        //输出结果:-----className:class com.sohu.aspectj.MainActivity   
31        Log.d("chao""-----Methodname:" + joinPoint.getSignature().getName());
32        //输出结果: -----Methodname:execute
33        Log.d("chao""-----ParamsType:" + ((CodeSignature)(joinPoint.getSignature())).getParameterTypes()[0]);
34        //输出结果:-----ParamsType:class java.lang.String
35        Log.d("chao""-----ParamsValue:" + joinPoint.getArgs()[0]);
36        //输出结果:-----ParamsValue:myExecute
37        boolean hasReturnType = joinPoint.getSignature() instanceof MethodSignature
38                && ((MethodSignature) joinPoint.getSignature()).getReturnType() != void.class;
39        Log.d("chao""-----returnType:" + hasReturnType);
40        //输出结果:-----returnType:false
41        long startNanos = System.nanoTime();
42        Object result = joinPoint.proceed(); // 调用原来的方法
43        long stopNanos = System.nanoTime();
44        long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);
45        Log.d("chao""-----executeTime:" + lengthMillis);
46        //输出结果:-----executeTime:500
47        return result;
48    }
49    //另外一种写法
50    @Around("method2() || constructor2()")
51    public Object logAndExecute2(ProceedingJoinPoint joinPoint) throws Throwable {
52        MethodSignature methodSignature = ((MethodSignature) joinPoint.getSignature());
53        AspectLog hyaspect = methodSignature.getMethod().getAnnotation(AspectLog.class);
54        String[] checkString = hyaspect.checkString();
55        Log.d("chao""-----AspectLog2:needCheck:" + hyaspect.needCheck()+" checkCode:"+hyaspect.checkCode()+" checkString:"+checkString[0]);
56        Object result = joinPoint.proceed();
57        return result;
58    }
59}

<< 向右滑动查看完整代码 >>

05

 在Android中的引用 

在AndroidStudio中使用AspectJ需要分三步:

1. 引入第三方注解包

在需要编写切面的Module中引入'org.aspectj:aspectjrt:1.8.7'里面包含了注解等代码

1dependencies {
2    implementation 'org.aspectj:aspectjrt:1.8.7'//aspectj注解工具包
3}

<< 向右滑动查看完整代码 >>

2. 引入Gradle插件

使用AspectJ的功能必须通过插件来处理插入的代码。Gradle插件的作用是处理切面类,织入代码。目前针对Android来说并没有官方Gradle插件,需要自己编写或者使用第三方成熟的插件。下图所示是本地自定义的一个Gradle插件,使用这种本地Gradle插件的方式,则只需要在主module的build.gradle中引入插件即可,具体实现细节下个章节会讲到。


3. 编写切面类

编写切面类,编写切点和通知代码


06

 Gradle插件的实现 

Gradle插件实现如果不需要发布插件,则可以再编写本地插件。自定义插件的方式这里不再赘述,感兴趣的小伙伴可以参考官方文档[2]

1. 新建插件Module buildSrc

在其中build.gradle引用如下:

1apply plugin: 'groovy'
2
3repositories {
4    mavenLocal()
5    jcenter()
6    google()
7}
8
9dependencies {
10    implementation gradleApi()//gradle相关api
11    implementation localGroovy()//groovy支持api
12    implementation 'org.aspectj:aspectjtools:1.8.7'//aspectj工具
13    implementation 'com.android.tools.build:gradle:3.2.1'//gradle相关api
14    implementation 'org.aspectj:aspectjrt:1.8.7'//aspectj工具
15}
16

<< 向右滑动查看完整代码 >>

2. 继承Plugin<Project>

新建HyAspectJPlugin继承Plugin<Project>,在apply(project)方法中执行一些操作:首先获取java编译器中的一些java文件以及class文件目录,然后java编译器编译完成后,调用aspectj的编织工具,传入文件目录等参数,织入代码。织入的过程可以通过MessageHandler来输出log信息。

编织器需要的各个参数信息说明如代码中的注释,更详细的参数信息参考官网[3]

1//Plugin<Project>是gradle3.2.1中的api
2class HyAspectJPlugin implements Plugin<Project> {
3    @Override
4    void apply(Project project) {
5        //AppExtension是gradle:3.2.1 中的api
6        project.extensions.getByType(AppExtension).getApplicationVariants().all { variants ->
7            JavaCompile javaCompiler = variants.javaCompiler
8            println("----------------inpath-------------" + javaCompiler.destinationDir.toString())
9            //输出内容为:/Users/allenzhang/AspectJ/app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes
10            println("----------------aspectpath-------------" + javaCompiler.classpath.asPath)
11            //输出内容为:/Users/allenzhang/.gradle/caches/transforms-1/files-1.1/design-28.0.0.aar/cd2e80a662cae66082356ce68f010bb8/jars/classes.jar
12            //:/Users/allenzhang/.gradle/caches/transforms-1/files-1.1/appcompat-v7-28.0.0.aar/c54d7f8ccd6a59577eea394c9c81344c/jars/classes.jar
13            println("----------------bootclasspath-------------" + javaCompiler.options.bootstrapClasspath.asPath)
14            //输出内容为:/Users/allenzhang/Library/Android/sdk/platforms/android-28/android.jar
15            //:/Users/allenzhang/Library/Android/sdk/build-tools/28.0.3/core-lambda-stubs.jar
16            // -1.7 设置规范1.7,匹配java1.7
17            // -showWeaveInfo,输出编织过程信息
18            // -inpath class文件目录或者jar包, 源字节码,需要处理的类
19            // -aspectpath  定义的切面类
20            // -d 存放编辑产生的class文件
21            // -classpath ,所有class文件,源class,java包,编织时需要用到的一些处理类
22            javaCompile.doLast 
{
23                String[] args = [
24                        "-showWeaveInfo",
25                        "-1.7",
26                        "-inpath", javaCompile.destinationDir.toString(),
27                        "-aspectpath", javaCompiler.classpath.asPath,
28                        "-d", javaCompile.destinationDir.toString(),
29                        "-classpath", javaCompiler.classpath.asPath,
30                        "-bootclasspath", javaCompile.options.bootstrapClasspath.asPath
31                ]
32                MessageHandler handler = new MessageHandler(true)
33                new Main().run(args, handler)
34                def log = project.logger
35                for (IMessage message : handler.getMessages(null, true)) 
{
36                    switch (message.getKind()) {
37                        case IMessage.ABORT:
38                        case IMessage.ERROR:
39                        case IMessage.FAIL:
40                            println("----------------ERROR-------------" + message.message)
41                            log.error message.message, message.thrown
42                            break
43                        case IMessage.WARNING:
44                        case IMessage.INFO:
45                            println("----------------WARNING-------------" + message.message)
46                            log.info message.message, message.thrown
47                            break
48                        case IMessage.DEBUG:
49                            println("----------------DEBUG-------------" + message.message)
50                            log.debug message.message, message.thrown
51                            break
52                    }
53                }
54            }
55        }
56    }
57}

<< 向右滑动查看完整代码 >>

3. 新建配置文件

如下图所示:新建resources/META-INF/gradle-plugins/com.sohu.aspectj.properties,定义插件的名称为com.sohu.aspectj。文件中内容为:implementation-class=com.sohu.aspectj.HyAspectj


07

 一个优秀的Gradle插件 

上面只是简单的插件编写方法,这种方法和kotlin冲突不能共存, 这是因为kotlin编译的class文件在另外的一个位置,还需要传入对应class文件夹的位置。下面介绍一款比较好的gradle插件[4]

相比上面简单的织入方式,有以下优点:

  • 支持InstantRun

  • 支持设置织入代码范围

  • 支持kotlin代码

  • 异步织入

如下图所示:插件原理是通过注册一个Transform,获取输入文件(这个输入文件包含了kotlin的class文件),过滤需要织入的文件,然后织入代码后输出到指定文件夹,感兴趣的小伙伴可以查看源码了解更详细的内容。

Transform的介绍可以参考Transform的使用[5]

以上介绍了AspectJ的相关概念以及用法,还介绍了Gradle插件的编写方式,AspectJ是Android面向切面编程的一种常用的实现方式,在打印日志、统计埋点、统一业务逻辑处理、性能统计等方面有广泛的应用。但匹配表达式的规则太多,容易混淆,以及没有官方Gradle插件的说明文档,这两点使AspectJ使用起来较为麻烦。在使用过程中只需要先掌握一些常见的用法,遇到不能满足需求的地方可以查阅官方文档寻找答案。


参考资料:

[1]https://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html

[2]https://guides.gradle.org/writing-gradle-plugins/

[3]https://www.eclipse.org/aspectj/doc/released/devguide/printable.html

[4]https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx

[5]https://github.com/SusionSuc/AdvancedAndroid/blob/master/gradle%E6%8F%92%E4%BB%B6%E4%B8%8E%E5%AD%97%E8%8A%82%E7%A0%81%E6%B3%A8%E5%85%A5/GradleTransformAPI%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8.md






狐友技术团队其他精彩文章

通过源码对SparkShuffle深入解析

Swift之Codable实战技巧

不了解GIF的加载原理?看我就够了!

安卓系统权限,你真的了解吗?





加入搜狐技术作者天团

千元稿费等你来!

戳这里!


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存