码迷,mamicode.com
首页 > 移动开发 > 详细

(4.6.23.1)Android之面向切面编程:AOP 与 Aspect简介

时间:2017-04-20 21:37:47      阅读:874      评论:0      收藏:0      [点我收藏+]

标签:功能   lips   java编译   jsb   过程   lease   location   它的   统计   

一、OOP的困境

1.1 OOP

ObjectOriented Programming,面向对象编程

在OOP的世界中,问题或者功能都被划分到一个一个的模块里边。每个模块专心干自己的事情,模块之间通过设计好的接口交互.

是一种方法论,一种编程的思想。

1.2 从“打印日志”来看AOP 和 OOP

OOP的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有问题都能完美得划分到模块中

举个最简单而又常见的例子:
现在想为每个模块加上日志功能,要求模块运行时候能输出日志
  • OOP的处理方式

    1. 先设计一个日志输出模块,这个模块提供日志输出API,比如Android中的Log类;
    2. 然后,其他模块需要输出日志的时候调用Log类的几个函数,比如e(TAG,…),w(TAG,…),d(TAG,…),i(TAG,…)等。
/***
*代码很简单。但是从这个小例子中,你也会发现要是这个程序比较复杂的话,到处都加*Log,或者在某些特殊函数加权限检查的代码,真的是一件挺繁琐的事情。
*/
public class AopDemoActivity extends Activity {
   private static final String TAG = "AopDemoActivity";
//onCreate,onStart,onRestart,onPause,onResume,onStop,onDestory返回前,都输出一行日志
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.layout_main);
       Log.e(TAG,"onCreate");
    }
   protected void onStart() {
       super.onStart();
        Log.e(TAG, "onStart");
    }
   protected void onRestart() {
       super.onRestart();
        Log.e(TAG, "onRestart");
    }
    protectedvoid onResume() {
       super.onResume();
        Log.e(TAG, "onResume");
?  // checkPhoneState会检查app是否申明了android.permission.READ_PHONE_STATE权限
        checkPhoneState();
    }
   protected void onPause() {
       super.onPause();
        Log.e(TAG, "onPause");
    }
   protected void onStop() {
       super.onStop();
        Log.e(TAG, "onStop");
    }
   protected void onDestroy() {
       super.onDestroy();
        Log.e(TAG, "onDestroy");
    }
   private void checkPhoneState(){
       if(checkPermission("android.permission.READ_PHONE_STATE")== false){
           Log.e(TAG,"have no permission to read phone state");
           return;
        }
       Log.e(TAG,"Read Phone State succeed");
       return;
    }
   private boolean checkPermission(String permissionName){
       try{
           PackageManager pm = getPackageManager();
          //调用PackageMangaer的checkPermission函数,检查自己是否申明使用某权限
           int nret = pm.checkPermission(permissionName,getPackageName());
           return nret == PackageManager.PERMISSION_GRANTED;
        }......
    }
}

在没有接触AOP之前,能想到的解决方案基本就是上面的方法。但是,从OOP角度看,除了日志模块本身,其他模块的承载的业务功能都不包含“打印日志”,而仅仅需要这样一个调用。这个日志输出功能,从整体来看,都是一个面上的。而这个面的范围,就不局限在单个模块里了,而是横跨多个模块。

在没有AOP之前,各个模块要打印日志,就是自己处理。通过分散在各处的函数调用从而实现了“日志打印功能”,这有点类似于分权管理,那么AOP就是对应集权管理方式。

  • AOP的处理方式

第一,我们要认识到OOP世界中,有些功能是横跨并嵌入众多模块里的,比如打印日志,比如统计某个模块中某些函数的执行时间等。这些功能在各个模块里分散得很厉害,可能到处都能见到。

第二,AOP的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。比如我们可以设计两个Aspects,一个是管理某个软件中所有模块的日志输出的功能,另外一个是管理该软件中一些特殊函数调用的权限检查

ps :
AspectJ需要编写aj文件,然后把AOP代码放到aj文件中。但是在Android开发中,我建议不要使用aj文件。因为aj文件只有AspectJ编译器才认识,而Android编译器不认识这种文件。所以当更新了aj文件后,编译器认为源码没有发生变化,所以不会编译它。
当然,这种问题在其他不认识aj文件的java编译环境中也存在。所以,AspectJ提供了一种基于注解的方法来把AOP实现到一个普通的Java文件中。这样我们就把AOP当做一个普通的Java文件来编写、编译就好。

/**
*
*
*/


package com.androidaop.demo;
import android.util.Log;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.JoinPoint;

@Aspect   //必须使用@AspectJ标注,这样class DemoAspect就等同于 aspect DemoAspect了
public class DemoAspect {
    staticfinal String TAG = "DemoAspect";
/*
@Pointcut:pointcut也变成了一个注解,这个注解是针对一个函数的,比如此处的logForActivity()
其实它代表了这个pointcut的名字。如果是带参数的pointcut,则把参数类型和名字放到
代表pointcut名字的logForActivity中,然后在@Pointcut注解中使用参数名。
基本和以前一样,只是写起来比较奇特一点。后面我们会介绍带参数的例子
*/
@Pointcut("execution(* com.androidaop.demo.AopDemoActivity.onCreate(..)) ||"
        +"execution(* com.androidaop.demo.AopDemoActivity.onStart(..))")
public void logForActivity(){};  //注意,这个函数必须要有实现,否则Java编译器会报错

/*
@Before:这就是Before的advice,对于after,after -returning,和after-throwing。对于的注解格式为
@After,@AfterReturning,@AfterThrowing。Before后面跟的是pointcut名字,然后其代码块由一个函数来实现。比如此处的log。
*/
    @Before("logForActivity()")
    public void log(JoinPoint joinPoint){
       //对于使用Annotation的AspectJ而言,JoinPoint就不能直接在代码里得到多了,而需要通过
      //参数传递进来。
       Log.e(TAG, joinPoint.toShortString());
    }
}

提示:如果开发者已经切到AndroidStudio的话,AspectJ注解是可以被识别并能自动补齐。
上面的例子仅仅是列出了onCreate和onStart两个函数的日志,如果想在所有的onXXX这样的函数里加上log,该怎么改呢?
@Pointcut("execution(* *..AopDemoActivity.on*(..))")
public void logForActivity(){};

技术分享

二、适用AOP的场景

  • 日志
  • 持久化
  • 性能监控
  • 数据校验
  • 缓存
  • 其他更多

三、工具和库

有一些工具和库帮助我们使用 AOP:
- AspectJ: 一个 JavaTM 语言的面向切面编程的无缝扩展(适用Android)。

  • Javassist for Android: 用于字节码操作的知名 java 类库 Javassist 的 Android 平台移植版。

  • DexMaker: Dalvik 虚拟机上,在编译期或者运行时生成代码的 Java API。

  • ASMDEX: 一个类似 ASM 的字节码操作库,运行在Android平台,操作Dex字节码。

四、AspectJ简介

我们下面的例子选用 AspectJ,有以下原因:
- 功能强大
- 支持编译期和加载时代码注入
- 易于使用

AspectJ会在编译期间增加一个新的步骤处理AspectJ注解,并在对应的切入点中生成和注入必要的样板代码

使用AspectJ有两种方法:

  • 完全使用AspectJ的语言。这语言一点也不难,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
  • 或者使用纯Java语言开发,然后使用AspectJ注解,简称@AspectJ。
    Anyway,不论哪种方法,最后都需要AspectJ的编译工具ajc来编译。由于AspectJ实际上脱胎于Java,所以ajc工具也能编译java源码。

AspectJ现在托管于Eclipse项目中,官方网站是:

五、AOP概念介绍

5.1 Join Points: 何处支持注入

Join Points(以后简称JPoints)是AspectJ中最关键的一个概念。什么是JPoints呢?JPoints就是程序运行时的一些执行点。那么,一个程序中,哪些执行点是JPoints呢?比如:

  • 一个函数的调用可以是一个JPoint。比如Log.e()这个函数。e的执行可以是一个JPoint,而调用e的函数也可以认为是一个JPoint。
  • 设置一个变量,或者读取一个变量,也可以是一个JPoint。比如Demo类中有一个debug的boolean变量。设置它的地方或者读取它的地方都可以看做是JPoints
  • for循环可以看做是JPoint

理论上说,一个程序中很多地方都可以被看做是JPoint,但是AspectJ中,只有如表1所示的几种执行点被认为是JPoints:

Join Points 说明 示例
method call 函数调用 比如调用Log.e(),这是一处JPoint
method execution 函数执行 比如Log.e()的执行内部,是一处JPoint。注意它和method call的区别。method call是调用某个函数的地方。而execution是某个函数执行的内部。
constructor cal 构造函数调用 和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 这个是AspectJ的内容,稍后再说

下面我们来看个例子以直观体会一把,打印出其中所有的join points。

package test;
public class Test{
   static public class TestBase{
        static{
          int x = 0;
        }
        int base = 0;
        public TestBase(int index){
            base = index;
        }
   }
     static  public class TestDerived extends TestBase{
        public int derived = 0;
        public TestDerived(){
            super(0);
            this.derived = 1000;

        }
        public void testMethod() {
            try{
                byte[] test = null;
                test[1] = 0x33;
            }catch(Exception ex){
            }
        }
        static int getFixedIndex(){
           return 1000;
        }
    }
     public static void main(String args[]){
        System.out.println("Test begin...");
        TestDerived derived = new TestDerived();
        derived.testMethod();
        derived.base = 1;
        System.out.println("Test end...");
    }
}

技术分享

  • 左图的第一个红框:

    • staticinitialization(test.Test.):表示当前是哪种类型的JPoint,括号中代表目标对象是谁(此处是指Test class的类初始化)。由于Test类没有指定static block,所以后面的at:Test.java:0 表示代码在第0行(其实就是没有找到源代码的意思)。
    • Test类初始化完后,就该执行main函数了。所以,下一个JPoint就是execution(voidtest.Test.main(String[]))。括号中表示此JPoint对应的是test.Test.main函数。at:Test.java:30表示这个JPoint在源代码的第30行。大家可以对比图2的源码,很准确!
    • main函数里首先是执行System.out.println。而这一行代码实际包括两个JPoint。一个是get(PrintStream java.lang.System.out),get表示Field get,它表示从System中获取out对象。另外一个是call(void java.io.PrintStream.println(String)),这是一个call类型的JPoint,表示执行out.println函数。
  • 左图第二个红框

    • 它表示TestBase的类的初始化,由于源码中为TestBase定义了static块,所以这个JPoint清晰指出了源码的位置是at:Test.java:5
  • 左图第三个红框

    • 它和对象的初始化有关。在源码中,我们只是构造了一个TestDerived对象。它会先触发TestDerived Preinitialization JPoint,然后触发基类TestBase的PreInitialization JPoint。注意红框中的before和after 。在TestDerived和TestBase所对应的PreInitialization before和after中都没有包含其他JPoint。所以,Pre-Initialization应该是构造函数中一个比较基础的Phase。这个阶段不包括类中成员变量定义时就赋值的操作,也不包括构造函数中对某些成员变量进行的赋值操作。
    • 而成员变量的初始化(包括成员变量定义时就赋值的操作,比如源码中的int base = 0,以及在构造函数中所做的赋值操作,比如源码中的this.derived = 1000)都被囊括到initialization阶段。请读者对应图三第二个红框到第三个红框(包括第3个红框的内容)看看是不是这样的
  • 第5个红框。它包括三个JPoint

    • testMethod的call类型JPoint
    • testMethod的execution类型JPonint
    • 以及对异常捕获的Handler类型JPoint

5.2 Pointcuts:定位想要注入的具体连接点

一个程序会有很多的JPoints,即使是同一个函数(比如testMethod这个函数),还分为call类型和execution类型的JPoint。显然,不是所有的JPoint,也不是所有类型的JPoint都是我们关注的。再次以AopDemo为例,我们只要求在Activity的几个生命周期函数中打印日志,只有这几个生命周期函数才是我们业务需要的JPoint,而其他的什么JPoint我不需要关注。

怎么从一堆一堆的JPoints中选择自己想要的JPoints呢?恩,这就是Pointcuts的功能。一句话,Pointcuts的目标是提供一种方法使得开发者能够选择自己感兴趣的JoinPoints。 类似于一种正则规则

5.1的例子中,怎么把Test.java中所有的Joinpoint选择出来呢?用到的pointcut格式为:
pointcuttestAll():within(Test)。

5.2.1 Pointcuts基本结构

  1. 选择那些调用println(而且不考虑println函数的参数是什么)的Joinpoint。
  2. 另外,调用者的类型不要是TestAspect的。 (TestAspect为当前类名)
//现在我想把5.1中的示例代码中,那些调用println的地方找到,该怎么弄?代码该这么写:
public pointcut  testAll(): call(public  *  *.println(..)) && !within(TestAspect) ;  

private static final String POINTCUT_METHOD =" call(public  *  *.println(..))";
@Pointcut(POINTCUT_METHOD )
public void testAll() {}
  • 第一个public:表示这个pointcut是public访问。这主要和aspect的继承关系有关,属于AspectJ的高级玩法,本文不考虑。
  • pointcut:关键词,表示这里定义的是一个pointcut。pointcut定义有点像函数定义。总之,在AspectJ中,你得定义一个pointcut。
  • testAll():pointcut的名字。在AspectJ中,定义Pointcut可分为有名和匿名两种办法。个人建议使用named方法。因为在后面,我们要使用一个pointcut的话,就可以直接使用它的名字就好。
  • testAll后面有一个冒号,这是pointcut定义名字后,必须加上。冒号后面是这个pointcut怎么选择Joinpoint的条件。
  • call(public * *.println(..))是一种选择条件
    • call:表示我们选择的Joinpoint类型为call类型
    • public :由于我们这里选择的JoinPoint类型为call类型,它对应的目标JPoint一定是某个函数。所以我们要找到这个/些函数。public 表示目标JPoint的访问类型(public/private/protect)
    • 第一个*表示返回值的类型是任意类型
    • 第二个*用来指明包名。此处不限定包名
    • 紧接其后的println是函数名。这表明我们选择的函数是任何包中定义的名字叫println的函数
    • (..)函数参数指明了目标函数的参数应该是什么样子的。比如这里使用了通配符..,代表任意个数的参数,任意类型的参数
  • &&:AspectJ 可以把几个条件组合起来,目前支持 &&,||,以及!这三个条件。这三个条件的意思不用我说了吧?和Java中的是一样的。
  • !within(TestAspectJ)
    • 前面的!表示不满足某个条件
    • within是另外一种类型选择方法,特别注意,这种类型和前面讲到的joinpoint的那几种类型不同

技术分享

5.2.2 Joinpoint类型的直接选择

5.2.1中示例定位了“调用println”的连接点,如果要改为函数的执行,也就是methodexecution的JPoint,那么pointcuts的写法就得包括execution(XXX)来限定。

5.2.2.1 Joinpoint类型一览

技术分享

5.2.2.2 定位正则

5.2.2.2.1 Method Signature
@注解 访问权限 返回值的类型 包名.函数名(参数)  
  • @注解 属于可选项
  • 访问权限(public/private/protect,以及static/final)属于可选项
    如果不设置它们,则默认都会选择。以访问权限为例,如果没有设置访问权限作为条件,那么public,private,protect及static、final的函数都会进行搜索。
  • 返回值类型就是普通的函数的返回值类型。如果不限定类型的话,就用*通配符表示
  • 包名.函数名用于查找匹配的函数。可以使用通配符,包括和..以及+号。其中号用于匹配除.号之外的任意字符,而..则表示任意子package,+号表示子类
    • java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
    • Test*:可以表示TestBase,也可以表示TestDervied
    • java..*:表示java任意子类
    • java..*Model+:表示Java任意package中名字以Model结尾的子类,比如TabelModel,TreeModel
  • 函数的参数,参数匹配比较简单,主要是参数类型..
    在参数匹配中,“..”代表任意参数个数和类型
    • (int, char):表示参数只有两个,并且第一个参数类型是int,第二个参数类型是char
    • (String, ..):表示至少有一个参数。并且第一个参数类型是String,后面参数类型不限
    • (Object …):表示不定个数的参数,且类型都是Object,这里的…不是通配符,而是Java中代表不定参数的意思
5.2.2.2.2 Constructor signature

Constructorsignature和Method Signature类似,只不过构造函数没有返回值,而且函数名必须叫new

public *..TestDerived.new(..)
  • public:选择public访问权限
  • *..代表任意包名
  • TestDerived.new:代表TestDerived的构造函数
  • (..):代表参数个数和类型都是任意
5.2.2.2.3 Field Signature
@注解 访问权限 类型 类名.成员变量名 
set(int test..TestBase.base):表示设置TestBase.base变量时的JPoint   
  • @注解和访问权限是可选的
  • 类型:成员变量类型,*代表任意类型
  • 类名.成员变量名:成员变量名可以是*,代表任意成员变量
5.2.2.2.4 Type Signature
  • staticinitialization(test..TestBase):表示TestBase类的static block
  • handler(NullPointerException):表示catch到NullPointerException的JPoint。注意,图2的源码第23行截获的其实是Exception,其真实类型是NullPointerException。但是由于JPointer的查询匹配是静态的,即编译过程中进行的匹配,所以handler(NullPointerException)在运行时并不能真正被截获。只有改成handler(Exception),或者把源码第23行改成NullPointerException才行

5.2.3 Joinpoint类型的间接选择

除了根据前面提到的Signature信息来匹配JPoint外,AspectJ还提供其他一些选择方法来选择JPoint。比如某个类中的所有JPoint,每一个函数执行流程中所包含的JPoint。
特别强调,不论什么选择方法,最终都是为了找到目标的JPoint。
技术分享

注意:this()和target()匹配的时候不能使用通配符。

技术分享
注意,不是所有的AOP实现都支持本节所说的查询条件。比如Spring就不支持withincode查询条件。

5.3 advice: 注入代码的执行时间点

//testAll()是前面定义的pointcuts
//而before()定义了在这个pointcuts选中的JPoint执行前我们要干的事情。
before():testAll(){  
   System.out.println("before calling: " + thisJoinPoint);//打印这个JPoint的信息  
  System.out.println("      at:" + thisJoinPoint.getSourceLocation());//打印这个JPoint对应的源代码位置  
}  

恭喜,看到这个地方来,AspectJ的核心部分就掌握一大部分了。现在,我们知道如何通过pointcuts来选择合适的JPoint。那么,下一步工作就很明确了,选择这些JPoint后,我们肯定是需要干一些事情的。比如前面例子中的输出都有before,after之类的。这其实JPoint在执行前,执行后,都执行了一些我们设置的代码。在AspectJ中,这段代码叫advice。简单点说,advice就是一种Hook。

Tables Are Cool
before() before advice 表示在JPoint执行之前,需要干的事情
after() after advice 表示JPoint自己执行完了后,需要干的事情。
返回值类型 around() before和around是指JPoint执行前或执行后备触发,而around就替代了原JPoint around是替代了原JPoint,如果要执行原JPoint的话,需要调用proceed
after():returning(返回值类型)after():throwing(异常类型) returning和throwing后面都可以指定具体的类型,如果不指定的话则匹配的时候不限定类型 假设JPoint是一个函数调用的话,那么函数调用执行完有两种方式退出,一个是正常的return,另外一个是抛异常。注意,after()默认包括returning和throwing两种情况

注意,after和before没有返回值,但是around的目标是替代原JPoint的,所以它一般会有返回值,而且返回值的类型需要匹配被选中的JPoint。

技术分享

  • 第一个红框是修改后的testMethod,在这个testMethod中,肯定会抛出一个空指针异常。
  • 第二个红框是我们配置的advice,除了before以外,还加了一个around。我们重点来看around,它的返回值是Object。虽然匹配的JPoint是testMethod,其定义的返回值是void。但是AspectJ考虑的很周到。在around里,可以设置返回值类型为Object来表示返回任意类型的返回值。AspectJ在真正返回参数的时候,会自动进行转换。比如,假设inttestMethod定义了int作为返回值类型,我们在around里可以返回一个Integer,AspectJ会自动转换成int作为返回值
  • 再看around中的//proceed()这句话。这代表调用真正的JPoint函数,即testMethod。由于这里我们屏蔽了proceed,所以testMethod真正的内容并未执行,故运行的时候空指针异常就不会抛出来。也就是说,我们完全截获了testMethod的运行,甚至可以任意修改它,让它执行别的函数都没有问题。

注意:从技术上说,around是完全可以替代before和after的。图7中第二个红框还把after给注释掉了。如果不注释掉,编译时候报错,[error]circular advice precedence: can’t determine precedence between two or morepieces of advice that apply to the same join point: method-execution(voidtest.Test$TestDerived.testMethod())(大家可以自己试试)。我猜测其中的原因是around和after冲突了。around本质上代表了目标JPoint,比如此处的testMethod。而after是testMethod之后执行。那么这个testMethod到底是around还是原testMethod呢?真是傻傻分不清楚!
(我觉得再加一些限制条件给after是可以避免这个问题的,但是没搞成功…)

5.4 参数传递和JPoint信息

5.4.1 参数传递

  • pointcuts修改:像定义函数一样定义pointcuts,然后在this,target或args中绑定参数名(注意,不再是参数类型,而是参数名)。
  • advice修改:也像定义函数一样定义advice,然后在冒号后面的pointcuts中绑定参数名(注意是参数名)
  • 在advice的代码中使用参数名。

前面介绍的advice都是没有参数信息的,而JPoint肯定是或多或少有参数的。而且advice既然是对JPoint的截获或者hook也好,肯定需要利用传入给JPoint的参数干点什么事情。比方所around advice,我可以对传入的参数进行检查,如果参数不合法,我就直接返回,根本就不需要调用proceed做处理。

往advice传参数比较简单,就是利用前面提到的this(),target(),args()等方法。另外,整个pointcuts和advice编写的语法也有一些区别。具体方法如下:

5.4.1.1 先在pointcuts定义时候指定参数类型和名字

pointcut testAll(Test.TestDerived derived,intx):call(*Test.TestDerived.testMethod(..))  
             && target(derived)&& args(x)  

注意上述pointcuts的写法,首先在testAll中定义参数类型和参数名。这一点和定义一个函数完全一样.
接着看target和args。此处的target和args括号中用得是参数名。而参数名则是在前面pointcuts中定义好的。这属于target和args的另外一种用法。
? 注意,增加参数并不会影响pointcuts对JPoint的匹配,上面的pointcuts选择和

pointcut testAll():call(*Test.TestDerived.testMethod(..)) && target(Test.TestDerived) &&args(int)是一样的  

5.4.1.2 修改advice

Object around(Test.TestDerived derived,int x):testAll(derived,x){  
     System.out.println("     arg1=" + derived);  
     System.out.println("     arg2=" + x);  
      return proceed(derived,x); //注意,proceed就必须把所有参数传进去。  
}  
  • advice的定义现在也和函数定义一样,把参数类型和参数名传进来。
  • 接着把参数名传给pointcuts,此处是testAll。注意,advice必须和使用的pointcuts在参数类型和名字上保持一致。
  • 然后在advice的代码中,你就可以引用参数了,比如derived和x,都可以打印出来。

5.4.2 JPoint信息

打印出了JPoint的信息,比如当前调用的是哪个函数,JPoint位于哪一行代码。这些都属于JPoint的信息。AspectJ为我们提供如下信息

参考资料

(4.6.23.1)Android之面向切面编程:AOP 与 Aspect简介

标签:功能   lips   java编译   jsb   过程   lease   location   它的   统计   

原文地址:http://blog.csdn.net/fei20121106/article/details/70269765

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!