欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

android AOP实现之AspectJ

程序员文章站 2022-07-12 14:10:35
...

AOP

1.1 背景

OOP(面向对象编程)的精髓是把功能或问题模块化,每个模块都有自己的职责,理想状态是只处理自己职责之内的事务。但在实际中,理想的职责单一往往携带了一些其他的、“脏”的逻辑处理。举个最简单而又常见的例子:现在想为模块A加上日志功能,要求模块运行时候能输出日志。在不知道AOP的情况下,一般的处理都是:先设计一个日志输出模块,这个模块提供日志输出API,比如Android中的Log类。然后,其他模块需要输出日志的时候调用Log类的几个函数,比如e(TAG,…),w(TAG,…),d(TAG,…),i(TAG,…)等。范围再延伸一些,在其他模块中(模块A,模块B…)同样需要记录日志的功能。而这些日志的处理对于原模块来讲就是“脏”的逻辑,如何把日志这部分逻辑从原模块剔除出去集中处理?
AOP (面向切面编程)就是为了解决OOP过程中的横向尴尬。

1.2 AOP定义与使用场景

AOP:(Aspect-Oriented Programming)是一种面向切面的编程范式,可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的技术。AOP是OOP的延续,是函数式编程的一种衍生范型,将代码切入到类的指定方法、指定位置上的编程思想。主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果,提高模块化。

1.3 AOP使用场景

使用场景:在OOP过程中的一些横向的、跨越多个模块的任务,通过横切能达到集中处理的任务。比如日志记录、任务执行时间统计、安全控制、权限控制、性能统计、异常处理等等。

1.4 AOP实现调研

直接上链接吧,我的觉得总结的还不错。
Android AOP预研:http://blog.csdn.net/crazy__chen/article/details/52013920


AOP实现之AspectJ

AspectJ是一个面向切面的框架,一个对Java的通用面向切面的扩展。AspectJ定义了AOP语法,包含一个专门的编译器(ajc: 用于AspectJ和Java语言的字节码编译)用来生成遵守Java字节编码规范的Class文件。

在android 中使用AspectJ有两种方法:1.完全使用AspectJ的语言(需要学习规范);2.使用纯Java语言开发,然后使用AspectJ注解,简称@AspectJ。
(注:下文内容着重介绍第二种方法。)

AspectJ官方文档介绍:http://www.eclipse.org/aspectj/docs.php
AspectJ编程指南:http://www.eclipse.org/aspectj/doc/released/progguide/index.html
Aspect语言语义:http://www.eclipse.org/aspectj/doc/released/progguide/semantics.html#semantics-intro

1.AspectJ概念点

在使用AspectJ之前先了解几个概念:Join points(链接点),Pointcuts(切入点),Advice(通知)。

1.1 连接点(Join Points)

连接点是程序执行过程中明确的一点,虽然切面定义了横切的类型,但AspectJ系统不允许完全任意的横切。相反,切面定义了在程序执行过程中切入原则性点的类型,这些原则性的点被称为连接点。连接点可以是方法或构造函数调用和执行,异常处理,字段分配和访问等。AspectJ提供了多种连接点:

Join Points 解释
Method call 当一个方法被调用时,不包括非静态方法的super调用。
Method execution 当实际方法的代码体执行时。
Constructor call 构建一个对象并调用该对象的初始构造函数(即,不用于“super”或“this”构造函数调用)。被构造的对象在构造函数调用连接点返回,所以它的返回类型被认为是对象的类型,并且可以在返回通知后访问对象本身。
Constructor execution 当实际构造函数的代码体执行时,在”this”或者“super”构造函数调用之后执行。正在构造的对象是当前正在执行的对象,因此可以使用此切入点来访问。调用超级构造函数的构造函数的构造函数执行连接点还包含封装类的任何非静态初始化方法。没有值从构造函数执行连接点返回,所以它的返回类型被认为是无效的。
Static initializer execution 当一个类的静态初始化器执行时。没有值从静态初始化器执行连接点返回,所以它的返回类型被认为是无效的。
Object pre-initialization 在特定类的对象初始化代码运行之前。这包含从第一个被调用的构造函数开始到其父构造函数开始之间的时间。因此,这些连接点的执行包含评估this()和super()构造函数调用的参数的连接点。没有值从对象预初始化连接点返回,所以它的返回类型被认为是无效的。
Object initialization 当一个特定类的对象初始化代码运行时。这包含了返回其父构造函数和返回其第一个构造函数之间的时间。它包含所有用于创建对象的动态初始化器和构造函数。正在构造的对象是当前正在执行的对象,因此可以使用此切入点来访问。没有值从构造函数执行连接点返回,所以它的返回类型被认为是无效的。
Field reference 引用非常量字段时。 [请注意,对常量字段(绑定到常量字符串对象或原始值的静态最终字段)的引用不是连接点,因为Java要求将它们内联。]
Field set 当一个字段被分配给。 字段集连接点被认为有一个参数,该字段被设置为的值。 没有值从字段集合连接点返回,所以它的返回类型被认为是无效的。 [请注意,初始化常量字段(初始化程序为常量字符串对象或原始值的静态最终字段)不是连接点,因为Java要求将其引用内联。]
Handler execution 当一个异常处理程序执行。 处理程序执行连接点被认为有一个参数,正在处理异常。 没有值从字段集合连接点返回,所以它的返回类型被认为是无效的。
Advice execution 当一条通知的代码体执行时。

AspectJ Join Points 官网介绍通道

1.2 切入点(Pointcuts)

官方给出的介绍是:A pointcut is a program element that picks out join points and exposes data from the execution context of those join points。那么我对 pointcuts 的理解就是过滤规则+结果集,符合AespectJ规则的连接点会有很多,我们需要使用过滤规则来滤出我们关心的所有链接点,这里的过滤规则即切入点。

原始Pointcuts 解释 对应Join Points 示例
call (MethodPattern) 捕获签名与MethodPattern匹配的方法调用连接点 Method call call(* android.app.Activity.on**(..))
execution (MethodPattern) 捕获签名与MethodPattern匹配的方法执行连接点 Method execution execution(* android.support.v4.app.Fragment.on**(..))
call (ConstructorPattern) 捕获签名与ConstructorPattern匹配的构造函数调用连接点。 Constructor call call(*.new(int, int))
execution (ConstructorPattern) 捕获签名与ConstructorPattern匹配的构造函数执行连接点。 Constructor execution 类似方法执行
get (FieldPattern) 捕获签名与FieldPattern匹配的字段引用连接点。 [请注意,对常量字段(绑定到常量字符串对象或原始值的静态最终字段)的引用不是连接点,因为Java要求将它们内联。] Field reference
set (FieldPattern) 捕获签名与FieldPattern匹配的字段集连接点。 [请注意,初始化常量字段(初始化程序为常量字符串对象或原始值的静态最终字段)不是连接点,因为Java要求将其引用内联。] Field set
initialization(ConstructorPattern) 捕获签名与ConstructorPattern匹配的对象初始化连接点。 Object initialization
preinitialization(ConstructorPattern) 捕获签名与ConstructorPattern匹配的对象预初始化连接点。 Object pre-initialization
staticinitialization(TypePattern) 捕获签名与TypePattern匹配的静态初始化执行连接点。 Static initializer execution
handler(TypePattern) 捕获签名与TypePattern匹配的异常处理连接点。 Handler execution handler(ArrayOutOfBoundsException)
组合Pointcuts 解释 示例
within(TypePattern) 捕获其执行的代码是在TypePattern匹配的类型中定义的连接点(注:这里表示的是包名或者类名,也就是说与之匹配的包/类下所有符合规则的连接点) within(com.skx.tomike.fragment.business.*) within(com.skx.tomike.fragment.business.CatalogFragment)
withincode(ConstructorPattern l MethodPattern) 捕获其执行代码是在签名与MethodPattern匹配的方法中定义的连接点。 withincode(void m())
cflow(Pointcut) 捕获在控制流中符合切入点的所有连接点,包括本身 cflow(call(void Test.main()))
this(Type or Id) 捕获当前正在执行的Type对象实例(绑定到此的对象)或标识符Id的类型(必须在封闭通知或切入点定义中绑定)的连接点。 不会匹配来自静态上下文的任何连接点。 this(SomeType)
target(Type or Id) 捕获目标对象(应用了调用或字段操作的对象)是Type的实例或标识符Id的类型(必须在封闭通知或切入点定义中绑定)的连接点,。 不匹配任何调用,获取或静态成员集。 target(com.skx.tomike.activity.function.AopTestActivity)
args(Type or Id, …)
if(BooleanExpression) 捕获布尔表达式计算结果为true的连接点
! Pointcut 捕获Pointcut未选取的连接点。 !this(Point)
Pointcut0 && Pointcut1 捕获满足与条件的连接点 target(Point) && call(int *())
Pointcut0 ll Pointcut1 捕获满足或条件的连接点 call(void setX(int)) ll call(void setY(int))
( Pointcut )

代码中定义切入点


    @Pointcut("within(com.skx.tomike.fragment.business.CatalogFragment) && execution(*  android.support.v4.app.Fragment.on**(..))")
    public void pointcutTest() {
    }

解析:

  1. @Pointcut 用来声明这是一个切入点
  2. pointcutTest 是我们定义的切入点的名称,具体的命名规则看你了。
  3. within 和 execution 都是切入点的定义规则 。第一个规则表示com.skx.tomike.fragment.business.CatalogFragment 这个包下定义的所有连接点;第二个规则表示Fragment下所有匹配on**规则的方法。&& 是关系符,表示与。类似的还有 || 表示或、!表示非。
  4. “ * android.support.v4.app.Fragment.on**(..)” 前面的 * 号是通配符表示匹配任何返回值;后面的 * * 用来匹配名称的;(..) 用来匹配形参的,.. 表示匹配任意类型。

思考两个问题:
1.如果我把第一个条件 “ within(com.skx.tomike.fragment.business.CatalogFragment) ”
替换成 “ within(com.skx.tomike.fragment.business.*) ” 会有什么变化?
2. 下面这个切入点如何理解?


    @Pointcut("call(@com.skx.tomike.aop.LogRecordAnnotation * *(..))")
    public void methodAnnotatedWithDebugTrace() {
    }

注意: AspectJ本身就是一种编译期的AOP,检查代码并匹配连接点于切入点的代价是昂贵的,所以,定义切入点的时候,尽量使用精确的匹配规则来降低匹配时间,减少不必要的资源浪费。那么,如何定义一个好的切入点呢?一个好的切入点应该包含以下几个方面:

  1. 选择特定类型的连接点:如:execution, get, set, call handler
  2. 确定连接点范围,如within ,withincode
  3. 匹配上下文信息,如:this,target,@annotation

除了在切入点上规范之外还可以通过排除不需要扫描的包来降低匹配时间
关于连接点(Join Point)和切入点(pointcuts)我找个了例子帮助理解,插排:连接点就是提供的所有可用的插孔,切入点是某一个设备链接到的某个特定的连接点。

AspectJ Pointcuts 官网介绍通道
AspectJ Join Points and Pointcuts 语法介绍

1.3 通知(Advice)

Advice定义了横切行为,它是根据切入点定义的,不同的通知类型决定了插入的代码片段与切入点所选择的每个连接点进行何种方式的交互。AspectJ支持三种类型的advice:

  • Before:其连接点之前执行
  • After:在连接点之后执行
  • Around:代替(或“围绕”)连接点运行

注意:after advice 详细分有三种情况,包括在连接点执行完成后,在抛出异常之后,或者在执行完一个异常之后。

  • after( Formals )
  • after( Formals ) returning [ ( Formal ) ]
  • after( Formals ) throwing [ ( Formal ) ]

    @After("execution(* android.app.Activity.on**(..))")
    public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable {
        Log.e(TAG, "after  - 在Activity 的on**()方法体之后执行");
    }

    // 注意: 这里的pointcutTest() 表示的切入点,其意义和直接在advice后面写匹配公式是一样的
    @Pointcut("execution(* android.app.Activity.on**(..))")
    public void pointcutTest() {
    }

    @Before("pointcutTest()")
    public void onActivityMethodWithin(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.e(TAG, "before - 在 " + key + " 方法体之前执行");
    }

两种写法:一种是由advice直接接入切入点的规则;一种是先用@Pointcut 定义好切入点方法,然后由advice接入定义好的切入点方法。这两种写法都没毛病,第二种方法我认为在条件比较多的情况下更实用些,因为切入点组合变化也更灵活些。

AspectJ Advice 官网介绍通道

2. Join points与Pointcuts和advice之前的关系

之前在看了一张图,很明确的表示了Join points与Pointcuts和advice之前的关系。
android AOP实现之AspectJ
pointcuts和advice 搭配组成了一个切面。之前我用插排来举例,现在我来分析下,程序里的连接点好比是插排里的插孔,有三孔插口、两孔插口、USB插口。什么是切入点,如果是一个USB的连接设备,那么切入点就是所有的USB插口。现在加一个规则,总共有3个USB插口,但是最右边的那个你不能用,那么现在的切入点是不是相当于:所有的USB插孔 && !最右边的USB插孔 ?

android AOP实现之AspectJ


自定义注解类型的Pointcuts

以log输出为例:
1.定义注解

package com.skx.tomike.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Log注解
 */
@Retention(RetentionPolicy.CLASS)//编译时注解
@Target({ElementType.METHOD})
public @interface LogRecordAnnotation {
    /**
     * @Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、
     * 类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。
     * 在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
     *
     * 作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

      取值(ElementType)有:
        1.CONSTRUCTOR:用于描述构造器
        2.FIELD:用于描述域
        3.LOCAL_VARIABLE:用于描述局部变量
        4.METHOD:用于描述方法
        5.PACKAGE:用于描述包
        6.PARAMETER:用于描述参数
        7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
     */
}

2.定义切面类、切面方法和要插入的代码

package com.skx.tomike.aop;

import android.util.Log;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;


/**
 * Created by shiguotao on 2018/3/22.
 */
@Aspect
public class LogAspect {

    private static final String TAG = "LogAspect";

    private static final String POINTCUT_METHOD = "call(@com.skx.tomike.aop.LogRecordAnnotation * *(..))";

    @Pointcut(POINTCUT_METHOD)
    public void logPointcuts() {
    }

    @Before("logPointcuts()")
    public void logPointcutsTest(ProceedingJoinPoint joinPoint) throws Throwable {
        // 这里是插入的代码
        Log.e(TAG, "aop 测试!");
    }

}

3.在需要log记录的地方使用注解


    @LogRecordAnnotation
    private void testAOP() {
        Log.e("testAOP", "11111111");
    }


参考链接

AOP之@AspectJ技术原理详解
深入理解Android之AOP
Android基于AOP的非侵入式监控之——AspectJ实战