AOP之AspectJ在android中的解读

Posted 小钟视野

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AOP之AspectJ在android中的解读相关的知识,希望对你有一定的参考价值。

一 前言

     在没有接触AOP切面编程时,总觉得它是一门特神奇的,特遥不可及的技术,直到公司做无埋,用hook所有监听器的直男方式,遇到无底洞的大坑之后,才痛定思痛执着了解AOP切面编程。
    对于AOP切面编程的意义,最主要是找到切入点,接下来了解AspectJ框架的一些基本核心概念。

    既然是一个框架,那么就要遵循它的规则

二 核心概念

    <1> Join Point 又名Jpoint

        JPoint跟java基本无异,只不过一些关键字可能有差别,定义的切入点都跟方法很像
        是整个Aspectj的中心,也就是切入点。如何查找正确的切入点?
        答案就是你想要做什么,比如无埋的操作,你要监听所有Onclick(v)那么onClick(v)就是一个jPoint
        被当做切入点的一般有属性、方法(包括构造方法)
         AspectJ库的Jpoint:

    <2>Pointcuts : 

        其实也是Jpoint,只不过是Jpoint的一个强大的子集,可以自定义自己想要的JPoint

          <一>:理解各种Signature:可以理解为切入点的具体条件过滤筛选,匹配上才执行切入点,也就是匹配                                                指定JPoint对应的函数(包括构造函数)
                1.MethodSignature:正常java方法条件筛选.继承于CodeSignature
                       一个MethodSignature的格式为:
                       @注解 访问权限 返回值的类型 包名.函数名(参数) 如:

                        (1) @注解和访问权限(也就是java的修饰符public/private/protect,以及static/final)                                                                 这两个属于可选项。

                            如果不设置它们,则默认都会选择也就是忽略访问权限和注解。
                           以访问权限为例,如果没有设置访问权限作为条件,那么public,private,protect及static、                                                   final的函数都会进行搜索。
                        (2) 返回值类型:就是java函数的返回值类型。如果不限定类型的话,就用*通配符表示
                       (3) 包名.函数名:用于查找匹配的java函数。可以使用通配符,包括*和..以及+号。其中*号用于匹                                                       配除.号之外的任意字符,而..则表示任意子package,+号表示子类。
                        (4) 函数的参数:参数匹配比较简单,主要是java的参数类型,比如:
                             (int, char):表示参数只有两个,并且第一个参数类型是int,第二个参数类型是char

                            (String, ..):表示至少有一个参数。并且第一个参数类型是String,后面参数类型不限。                                                                                在参数匹配中,..代表任意参数个数和类型

                            (Object ...):表示不定个数的参数,且类型都是Object,这里的...不是通配符,而是Java中代                                                                        表不定参数的意思
                        例子:
                        java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
                        Test*:可以表示TestBase,也可以表示TestDervied
                        java..*:表示java任意子类
                        java..*Model+:表示Java任意package中名字以Model结尾的子类,比如TabelModel,                                                                                    TreeModel 等

                2.ConstructorSignature:java构造方法条件筛选:与MethodSignature的类似,但构造方法没有                                                                                     返回类型

                    格式例子:
                    public *..TestDerived.new(..): 区别于MethodSignature的是多了一个new,这是构造方法条件                                                                       筛选必须的,放在最后
                        (1)public:选择public访问权限
                        (2)*..代表任意包名
                        (3)TestDerived.new:代表TestDerived的构造函数
                        (4)(..):代表参数个数和类型都是任意

                3.TypeSinature:java类条件筛选,针对的是整个类

                4.FieldSignature:对于java属性的条件筛选

                   标准格式:
                        @注解 访问权限 类型 类名.成员变量名
                        (1)@注解和访问权限是可选的
                        (2)类型:成员变量类型,*代表任意类型
                        (3)类名.成员变量名:成员变量名可以是*,代表任意成员变量
                       例子:int test..TestBase.base  省略的访问权限,访问test包开头,类名为TestBase且属性为base的                                                         属性

         <二>执行切入点:

            比如是调用方法是执行还是执行方法时执行、设置属性时执行还是获取属性时执行等等

             1.execute(CodeSignature):执行方法体时执行这个切入点
             2.call(CodeSignature):调用方法时执行这个切入点
             3.set(FieldSignature):为属性设置值时执行这个切入点:如 field = "test";
             4.get(FieldSignature):获取属性值时执行这个切入点:如 i = field;
             5.staticinitializaton:静态代码块执行时执行这个切入点 如:static
             6.inittialization:执行构造方法初始化是执行这个切入点 如:new xxx()
             7.handler(NullPointerException):表示catch到NullPointerException的JPoint。
             8.within(TypePattern):TypePattern标示package或者类。表示某个类内。TypePatter可以使用通配符
               结合注解可以表示:被注解的类,符合切入点
             9.target()、this() 注意:this()和target()匹配的时候不能使用通配符。

               等等如下图            

         通过执行切入点可以看出,<一>和<二>除了7、8、9都是要搭配使用的。

         Jpoint的pointCuts如下图:


    <3>.Advice:

           advice就是一种Hook。可以设置在切入点JPoint之前还是之后,执行我们需要添加的代码。

          Advice类型如下:

             1.before(Jpoint):在Jpoint之前执行
             2.after(Jpoint):在jpoint之后执行
            3.around(JPoint):在jpoint中使用proceed()来替代原来方法,如果调用proceed(),则调用的是原始方                                              法,否则原始方法不被调用。

           Advice类型如下:


三.实例详解

1.导入AspectJ库

     compile 'org.aspectj:aspectjtools:1.8.6'
     compile 'org.aspectj:aspectjrt:1.8.6'

2.制作Aspectj编译插件

  本例使用本地插件,不上传仓库。使用as创建插件规则以及上传仓库,晚点讲解。这里有篇博客讲的很好,可以先了解

  https://juejin.im/entry/577bc26e165abd005530ead8

本地插件,结构如下:

也是类似于module


MyPlugin代码如下:

public class MyPlugin implements Plugin<Project> 

    void apply(Project project) 
        println "dddd******************d"

        def hasApp = project.plugins.withType(AppPlugin)
        def hasLib = project.plugins.withType(LibraryPlugin)
        if (!hasApp && !hasLib) 
            throw new IllegalStateException("'android' or 'android-library' plugin required.")
        

        final def log = project.logger
        final def variants
        if (hasApp) 
            variants = project.android.applicationVariants//当前module是app
         else 
            variants = project.android.libraryVariants//当前module是library
        

        project.dependencies 
            // TODO this should come transitively
            compile 'org.aspectj:aspectjrt:1.8.6'//当前module添加aspect库
        

        variants.all  variant ->

            JavaCompile javaCompile = variant.javaCompile
            //JavaCompile编译任务添加lastAction,当前module编译完之后会执行这个action,所以要在每个module中都要执行这个插件
            //因为project是代表当前module中的project。可以学习gradle,敬请期待
            javaCompile.doLast 
                String[] args = ["-showWeaveInfo",
                                 "-1.5",
                                 "-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);//命令执行入口
                
            
        

    

代码很简单,相信一眼就能看明白

这里一定要注意。module中使用到的AspectJ切入点一定要在当前build.gradle中引入这个插件,否则无法生效。

引入方式:apply plugin: 'com.hc.gradle'//执行插件com.hc.gradle.MyPlugin的apply方法,一定要执行编译AspectJ的插件,否则无法在编译期间编译当前module的AspectJ


3.写一个Hugo类,使用@AspectJ注解形式,使用AspectJ实现切面编程。

   重点来了,上代码

@Aspect
public class Hugo 
    public static String TAG = "Hugo";
    @Pointcut("within(@com.example.aoplib.DebugLog *)")//带有注解类DebugLog修饰的类的所有Jpoint
    public void withinAnnotatedClass() //注解DebugLog修饰的类,所有Jpoint(方法中不一定有DebugLog修饰)
        Log.d(TAG,"=====withinAnnotatedClass");//日志不会被打印
    

    @Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
    public void methodInsideAnnotatedType() //非java关键字synthetic修饰且带有注解DebugLog修饰的类的所有方法
    

    @Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")
    public void constructorInsideAnnotatedType() //执行构造方法且有注解DebugLog修饰
    

    @Pointcut("execution(@com.example.aoplib.DebugLog * *(..)) || methodInsideAnnotatedType()")
    public void method() //被DebugLog注解修饰的所有方法或者被DebugLog注解的类中所有的Jpoint(可根据PointCut的条件)
        Log.d(TAG,"=====method");//日志不会被打印
    

    @Pointcut("execution(@com.example.aoplib.DebugLog *.new(..)) || constructorInsideAnnotatedType()")
    public void constructor() //被DebugLog注解修饰的所有构造方法或者被DebugLog注解的类中所有的Jpoint(可根据PointCut的条件)
        Log.d(TAG,"=====constructor");
    

    @Around("method() || constructor()")
    public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable 
        enterMethod(joinPoint);

        long startNanos = System.nanoTime();
        Object result = "不执行原始方法,原始方法的log不会被打印";
        if ("123".equals("234")) 
            //调用原始方法并将结果返回和字符串拼接,也就是说可以篡改返回值
            //如果不调用此方法,则原始方法就不会被触发
            result = joinPoint.proceed()+"结果已被篡改";
        
//        result = joinPoint.proceed()+"结果已被篡改";
        long stopNanos = System.nanoTime();
        long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);

        exitMethod(joinPoint, result, lengthMillis);

        return result;
    

    private static void enterMethod(JoinPoint joinPoint) 

        //切入点获取切入类型
        CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
        //获取切入点的所在的类:方法所在的类
        Class<?> cls = codeSignature.getDeclaringType();
        //方法名
        String methodName = codeSignature.getName();
        //方法参数名
        String[] parameterNames = codeSignature.getParameterNames();
        //参数值
        Object[] parameterValues = joinPoint.getArgs();

        StringBuilder builder = new StringBuilder("enterMethod \\u21E2 ");
        builder.append(methodName).append('(');
        for (int i = 0; i < parameterValues.length; i++) 
            if (i > 0) 
                builder.append(", ");
            
            builder.append(parameterNames[i]).append('=');
            builder.append(Strings.toString(parameterValues[i]));
        
        builder.append(')');

        if (Looper.myLooper() != Looper.getMainLooper()) 
            builder.append(" [Thread:\\"").append(Thread.currentThread().getName()).append("\\"]");
        

        Log.d(asTag(cls), builder.toString());

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) 
            final String section = builder.toString().substring(2);
            Trace.beginSection(section);
        
    

    private static void exitMethod(JoinPoint joinPoint, Object result, long lengthMillis) 

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) 
            Trace.endSection();
        

        Signature signature = joinPoint.getSignature();
        //方法所在的类
        Class<?> cls = signature.getDeclaringType();
        //方法名
        String methodName = signature.getName();
        //方法返回类型
        boolean hasReturnType = signature instanceof MethodSignature
                && ((MethodSignature) signature).getReturnType() != void.class;

        StringBuilder builder = new StringBuilder("exitMethod \\u21E0 ")
                .append(methodName)
                .append(" [")
                .append(lengthMillis)
                .append("ms]");

        if (hasReturnType) 
            builder.append(" = ");
            builder.append(Strings.toString(result));
        

        Log.d(asTag(cls), builder.toString());
    

    private static String asTag(Class<?> cls) 
        if (cls.isAnonymousClass()) 
            return asTag(cls.getEnclosingClass());//若是匿名内部类则直接查找外部类
        
        return cls.getSimpleName();//外部类名
    

代码还是很简单明了的,主要如下:

(1).使用@AspectJ注解方式表明这个类是一个AspectJ类,主要用于各种切入点

(2).切入点使用@pointCuts注解方式书写筛选条件,代码有注释应该一眼就能看明白

(3).Advice的@Around()结合pointCuts的方法,实现具体切入点

(4)使用@Around()注解的方法实现hook机制

 以上代码是实现应该比较好理解。结合前面的Jpoint讲解,应该是比较好理解。   

4.build之后,AspectJ已经实现插入,运行程序。

 如:

 原始代码:


build之后的class:


很明显,AspectJ把它给拦截,然后先执行Hugo.logAndExecute()方法,这个就是Hugo中@Around注解的方法。

AspectJDemo

扩展:结合注解的方式,AspectJ屡试不爽,比如权限框架、去除线上日志、无埋监听、handler方法错误日志统计等等

主要还是查看AspectJ的文档:

语法大全

官方文档

此文章及实例讲解借鉴博客如下:

https://blog.csdn.net/innost/article/details/49387395

以上是关于AOP之AspectJ在android中的解读的主要内容,如果未能解决你的问题,请参考以下文章

AOP之AspectJ在Android实现无侵入埋点实践

Android基于AOP的非侵入式监控之——AspectJ实战

Android AOP编程之AspectJ

Android AOP编程之AspectJ

Android AOP编程之AspectJ

Android 基于AOP监控之——AspectJ使用指南