aop面向切面编程之AspectJ的简单应用

面向切面

AOP(Aspect Oriented Programming)即:面向切面编程,通过预编译(pre-compiled)方式和运行期动态代理实现程序功能统一维护的一种技术。而对于AspectJ而言其实就是选取代码中的一个某个共有执行点选取出来,并在一些特定的条件下织入我们的代码来完成编译插桩,以便完成特定逻辑,就比如登录状态检查、日志代码的织入和埋点等等,实际上AspectJ是在class->dex时对字节码织入了代码,对于修改字节码的还有ASM、Javassist等,而AspectJ是会在我们的代码中织入它封装好的代码,可能会对性能有轻微的影响,如果对性能要求高的话,建议使用ASM直接修改字节码而不会织入其他多余的代码,这些我们这里不多说,感兴趣的可以网上有很多文章介绍,关于AspectJ的介绍我在性能优化之启动优化(一)这篇有大体的介绍。关于操作字节码的库有很多感兴趣的可以去了解一下。

1、Join Point

JoinPoints(连接点),程序中可能作为代码注入(织入)目标的特定的点,一般在代码中可能会存在很多Join Point(连接点)。在AspectJ中可以作为JoinPoints的地方包括:

Join Point 说明
Method call 方法被调用,即在方法调用处织入代码
Method execution 方法执行处,即在方法体重中织入代码
Constructor call 构造函数被调用
Constructor execution 构造函数执行
Field get 读取属性
Field set 写入属性
Pre-initialization 与构造函数有关,很少用到
Initialization 与构造函数有关,很少用到
Static initialization static 块初始化
Handler 异常处理
Advice execution 所有 Advice 执行

PointCut(切入点)

Pointcut 切入点,即一组Join Point(连接点)的集合,PointCuts就是Join Point的集合,只是说PointCut是具有条件的 Join Point ,在程序中可能存在很多Join Point,那么我们就需要通过Pointcut去筛选出我们感兴趣的Join Point来做处理。

Join Point Pointcuts 语法
Method call call(MethodPattern)
Method execution execution(MethodPattern)
Constructor call call(ConstructorPattern)
Constructor execution execution(ConstructorPattern)
Field get get(FieldPattern)
Field set set(FieldPattern)
Pre-initialization initialization(ConstructorPattern)
Initialization preinitialization(ConstructorPattern)
Static initialization staticinitialization(TypePattern)
Handler handler(TypePattern)
Advice execution adviceexcution()

Pattern就是正则的意思,即MethodPattern表示匹配方法的正则,上面除了 Join Point 对应的切点,Pointcuts 还有其他选择方法:

  • within(TypePattern) TypePattern 表示某个包或类中包含JPoint,符合 TypePattern 的代码中的 Join Point,表示在com.xxx.XxxActivity类型中的Join Point:

    @Pointcut("!within(com.aop.XxxActivity)")
     public void baseCondition() {}
    
  • this(Type) Join Point 所属的 this 对象是否 instanceOf Type的类型,即就是说被织入代码的Type是否Type类型的实例;

    @Pointcut("!this(com.xxx.xxx.aop.*) && !this(com.android.xxx.activity.*)")
    public void baseCondition() {}
    
  • target(Type) Join Point 所在的对象(例如 call 或 execution 操作符应用的对象)是否 instanceOf Type的类型,表示对com.xxx.MyHttpClient类型的对象;

    @Pointcut("!target(com.xxx.MyHttpClient)")
     public void baseCondition() {}
    
  • args(Type , ...) 方法或构造函数参数的类型,如;arges(long,..),对Join Point的参数进行条件筛选,下面的是表示对参数是request 的Join Point。

    @Pointcut("call(org.apache.http.HttpResponse org.apache.http.client.HttpClient.execute(org.apache.http.client.methods.HttpUriRequest)) && (target(httpClient) && (args(request) && baseCondition()))")
      public void httpClientExecuteOne(HttpClient httpClient, HttpUriRequest request) {}
    

target 与 this区别?

public class PersonAspect{
    @Pointcut("call(* com.xx.Person.eat(..))")
      public void callMethod() {
          Log.e("tag", "callMethod->");
    }

 @Around("callMethod()")
public void beforeMethodCall(JoinPoint joinPoint) {
    Log.e("tag", "getTarget->" + joinPoint.getTarget());
    Log.e("tag", "getThis->" + joinPoint.getThis());
}
}


public class MainActivity extends Activity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Person person= new Person();
    person.eat();
  }
}

getTarget-> com.xx.Person@68b1c9g
getThis->com.xx..MainActivity@48d38bd

  • target 与 this 很容易混淆,target指的是切入点的所有者,而this指代的是被织入代码所属类的实例对象。
  • 如果把call 换成 execution 结果是这样的:

getTarget-> com.xx.Person@68b1c9g
getThis->com.xx.Person@68b1c9g

call和execution的区别

未命名文件.png

就是说execution是在切点处出织入代码,比如织入函数,那么execution织入的是函数体中,而call织入的是函数调用处。

Before 、After和Around区别

@Aspect
public class TestAspect {
private static boolean runAround = true;

public static void main(String[] args) {
    new TestAspect().hello();
    runAround = false;
    new TestAspect().hello();
}

public void hello() {
    System.err.println("in hello");
}

@After("execution(void aspects.TestAspect.hello())")
public void afterHello(JoinPoint joinPoint) {
    System.err.println("after " + joinPoint);
}

@Around("execution(void aspects.TestAspect.hello())")
public void aroundHello(ProceedingJoinPoint joinPoint) throws Throwable {
    System.err.println("in around before " + joinPoint);
    if (runAround) {
        joinPoint.proceed();
    }
    System.err.println("in around after " + joinPoint);
}

@Before("execution(void aspects.TestAspect.hello())")
public void beforeHello(JoinPoint joinPoint) {
    System.err.println("before " + joinPoint);
}
}

输出日志:

    in around before execution(void aspects.TestAspect.hello())
    before execution(void aspects.TestAspect.hello())
    in hello
    after execution(void aspects.TestAspect.hello())
    in around after execution(void aspects.TestAspect.hello())
    in around before execution(void aspects.TestAspect.hello())
    in around after execution(void aspects.TestAspect.hello())

当你使用around 时,如果你不调用joinPoint.proceed()该方法,那么被织入的切点函数或其他切点不会被调用,并且Befor和After是不可以使用ProceedingJoinPoint作为参数,只能使用JoinPoint 作为参数。around 只能使用execution不能使用call。

Pointcuts 的语法中涉及到一些 Pattern,下面是这些 Pattern 的规则,[]里的内容是可选的:

Pattern 规则
MethodPattern [!] [@Annotation] [public,protected,private] [static] [final] 返回值类型 [类名.]方法名(参数类型列表) [throws 异常类型]
ConstructorPattern [!] [@Annotation] [public,protected,private] [final] [类名.]new(参数类型列表) [throws 异常类型]
FieldPattern [!] [@Annotation] [public,protected,private] [static] [final] 属性类型 [类名.]属性名
TypePattern 其他 Pattern 涉及到的类型规则也是一样,可以使用 '!'、''、'..'、'+','!' 表示取反,'' 匹配除 . 外的所有字符串,'*' 单独使用事表示匹配任意类型,'..' 匹配任意字符串,'..' 单独使用时表示匹配任意长度任意类型,'+' 匹配其自身及子类,还有一个 '...'表示不定个数。

Advice

Advice 说明
@Before 在执行 Join Point 之前
@After 在执行 Join Point 之后,包括正常的 return 和 throw 异常
@AfterReturning Join Point 为方法调用且正常 return 时,不指定返回类型时匹配所有类型
@AfterThrowing Join Point 为方法调用且抛出异常时,不指定异常类型时匹配所有类型
@Around 替代 Join Point 的代码,如果要执行原来代码的话,要使用 ProceedingJoinPoint.proceed()

关于AspectJ,本人推荐使用沪江gradle_plugin_android_aspectjx的插件,在Aspectj的基础上,还支持Kotlin。

进入主题先来分析我们应该如何完成这个需求:

  • 首先登录状态在整个工程中有很多的地方会使用到,那么我们是否可以把代码抽取为接口?
  • 那么我们写得这个只是检查登录的状态吗?当然不是,我们还可能检查网络状态。
  • 那么上面的需求是不是就完了呢?不是的,在我们的程序中可能存在很多切点(Point Cut),我们不可能每个都做检查,这样不现实,所以我们需要打标志,哪些是需要我们AspectJ做处理织入代码的Point Cut,所以我们可以使用注解(Annotation)。

定义接口:

interface CheckStatus {
/**
 * 检查状态
 *
 * @return true表示检查通过,false表示检查不通过
 */
fun doCheck(context: Context?): Boolean
}

我们定义了CheckStatus 接口,具体实现由子类完成,反正我只需要知道你给我返回的状态是否可用就行。

为了让AspectJ能能够精确切点(Point Cut)的位置,我们还需要定义注解(Annotation),对切点(Point Cut)进行筛选,筛选出我们感兴趣的切点:

@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
annotation class Check(vararg val value: KClass<out CheckStatus>)

可能有人会有疑问,注解为什么是可变参数,那是因为检查状态可能会同时检查多个状态,如同时检查登录和网络,一般只有这些条件都满足了才会通过检查,当然条件的优先级,也是和我们传入的class顺序相关,接下来就进入我们的切面相关的代码了。

@Aspect
@SuppressWarnings("unused")
class CheckStatusAspect {
// 使用WeakHashMap缓存起来,防止每次调用方法都要反射
private val mCacheStatusClass by lazy {
    WeakHashMap<KClass<out CheckStatus>, CheckStatus>()
}

//定义切面的规则
//1.就在原来应用中哪些注释的地方放到当前切面进行处理,筛选切点
//execution(注解名   注解用的地方)  ,其他类型的参数使用ars
//方法名自己定义
@Pointcut("execution(@com.youbesun.perform.aop.Check * *(..))")
fun checkStatus() {
}

//2.对进入切面的内容如何处理
//advice
//@Before()  在切入点之前运行
//@After()   在切入点之后运行
//@Around()  在切入点前后都运行
//方法名自己定义
@Around("checkStatus()")
@Throws(Throwable::class)
@SuppressWarnings("unused")
fun aroundJointPoint(joinPoint: ProceedingJoinPoint) {
    //初始化context
    val context = when (val obj = joinPoint.getThis()) {
        is Context -> obj
        is androidx.fragment.app.Fragment -> obj.activity
        is android.app.Fragment -> obj.activity
        else -> {
            LogHelper.e("AOP IS Around Joint Point checkStatus Error !")
            joinPoint.proceed()
            return
        }
    }
    //最后判断这个方法中的注解,是否都满足条件
    if (createInstance(joinPoint, context)) joinPoint.proceed()
}

private fun createInstance(
    joinPoint: ProceedingJoinPoint,
    context: Context?
): Boolean {
    //是否满足条件
    var isCheckSuccess = false

    //获取方法信息
    val methodSignature = joinPoint.signature as MethodSignature
    val statusKClass = methodSignature.method.getAnnotation(Check::class.java).value

    /*可能需要检查多个条件,如:登录和网络,只有全部成立才会通过*/
    statusKClass.forEach { it ->
        var statusCheck = mCacheStatusClass[it]
        if (statusCheck == null) {
            tryCatch({
                //反射创建实现类实例
                statusCheck = it.constructors.asSequence().firstOrNull()?.call()
                // 优化点,使用缓存避免同一个实例重复反射,造成性能损耗
                if (statusCheck != null) mCacheStatusClass[it] = statusCheck
            }, {
                // 创建实例有问题,上交bugly
                StabilityHelper.postCatcherException(
                    RuntimeException("CheckStatusAspect: ${it.message}", it)
                )
            })
        }

        isCheckSuccess = statusCheck?.doCheck(context) ?: false

        //优化点,如果有存在条件false立即停止
        if (!isCheckSuccess) return isCheckSuccess
    }

    return isCheckSuccess
    }
}

代码中的每一步都有清晰的注释,这里就不多说的,定义注解是为了减少切点的筛选,这里唯一需要注意的是,我么定义的接口和它的实现类不能被混淆,因为这里用到了反射,使用就非常的简单,代码如下:

/**
  * @Describe:登录状态检查
  */
class CheckLoginStatus : CheckStatus {
override fun doCheck(context: Context?): Boolean {
    val isLogin = Account.isLogin()
    if (!isLogin) context?.let { it.startActivity(it.intentFor<LoginActivity>()) }
    return isLogin
  }
}

然后在指定的方法上使用注解:

  @Check(CheckLoginStatus::class)
  override fun onClick(v: View) {}

这段代码就是检查登录状态的,当然你可以在实现一个检查网络的,比如:

  @Check(CheckNetWorkStatus::class,CheckLoginStatus::class)
  override fun onClick(v: View) {}

这段代码会优先检查网络状态,如果网络可用接着在检查登录状态,但是如果第一个条件网络状态没有通过,那么直接就不会下一步检查。到这里AspectJ就结束了,下一篇将会使用ASM替换AspectJ,使用gardle transform + asm对代码进行织入。。最后对于面向切面编程说两句,aop对目前在日志统计、埋点等等使用的非常多,可以做很多事,最近在看前微信大佬张绍文的Android开发高手课,发现原来我学的是假的Android。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,547评论 4 374
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,787评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,175评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,752评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,169评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,056评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,195评论 2 321
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,980评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,724评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,872评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,344评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,667评论 3 264
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,379评论 3 245
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,202评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,992评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,189评论 2 286
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,987评论 2 279

推荐阅读更多精彩内容