看AspectJ在Android中的强势插入

JulHlw 7年前
   <h2>什么是AOP</h2>    <p>AOP是Aspect Oriented Programming的缩写,即『面向切面编程』。它和我们平时接触到的OOP都是编程的不同思想,OOP,即『面向对象编程』,它提倡的是将功能模块化,对象化,而AOP的思想,则不太一样,它提倡的是针对同一类问题的统一处理,当然,我们在实际编程过程中,不可能单纯的安装AOP或者OOP的思想来编程,很多时候,可能会混合多种编程思想,大家也不必要纠结该使用哪种思想,取百家之长,才是正道。</p>    <p>那么AOP这种编程思想有什么用呢,一般来说,主要用于不想侵入原有代码的场景中,例如SDK需要无侵入的在宿主中插入一些代码,做日志埋点、性能监控、动态权限控制、甚至是代码调试等等。</p>    <h2>AspectJ</h2>    <p>AspectJ实际上是对AOP编程思想的一个实践,当然,除了AspectJ以外,还有很多其它的AOP实现,例如ASMDex,但目前最好、最方便的,依然是AspectJ。</p>    <h3>在Android项目中使用AspectJ</h3>    <p>AOP的用处非常广,从Spring到Android,各个地方都有使用,特别是在后端,Spring中已经使用的非常方便了,而且功能非常强大,但是在Android中,AspectJ的实现是略阉割的版本,并不是所有功能都支持,但对于一般的客户端开发来说,已经完全足够用了。</p>    <p>在Android上集成AspectJ实际上是比较复杂的,不是一句话就能compile,但是,鄙司已经给大家把这个问题解决了,大家现在直接使用这个SDK就可以很方便的在Android Studio中使用AspectJ了。</p>    <h3>接入说明</h3>    <p>首先,需要在项目根目录的build.gradle中增加依赖:</p>    <pre>  <code class="language-java">classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'</code></pre>    <p>完整代码如下:</p>    <pre>  <code class="language-java">buildscript {      repositories {          jcenter()      }      dependencies {          classpath 'com.android.tools.build:gradle:2.3.0-beta2'          classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'            // NOTE: Do not place your application dependencies here; they belong          // in the individual module build.gradle files      }  }</code></pre>    <p>然后再主项目或者库的build.gradle中增加AspectJ的依赖:</p>    <pre>  <code class="language-java">compile 'org.aspectj:aspectjrt:1.8.9'</code></pre>    <p>同时在build.gradle中加入AspectJX模块:</p>    <pre>  <code class="language-java">apply plugin: 'android-aspectjx'</code></pre>    <p>这样就把整个Android Studio中的AspectJ的环境配置完毕了,如果在编译的时候,遇到一些『can’t determine superclass of missing type xxxxx』这样的错误,请参考项目README中关于excludeJarFilter的使用。</p>    <pre>  <code class="language-java">aspectjx {      //includes the libs that you want to weave      includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library'        //excludes the libs that you don't want to weave      excludeJarFilter 'universal-image-loader'  }</code></pre>    <h3>AspectJ入门</h3>    <p>我们通过一段简单的代码来了解下基本的使用方法和功能,新建一个AspectTest类文件,代码如下:</p>    <pre>  <code class="language-java">@Aspect  public class AspectTest {        private static final String TAG = "xuyisheng";        @Before("execution(* android.app.Activity.on**(..))")      public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {          String key = joinPoint.getSignature().toString();          Log.d(TAG, "onActivityMethodBefore: " + key);      }  }</code></pre>    <p>在类的最开始,我们使用@Aspect注解来定义这样一个AspectJ文件,编译器在编译的时候,就会自动去解析,并不需要主动去调用AspectJ类里面的代码。</p>    <p>我的原始代码很简单:</p>    <pre>  <code class="language-java">public class MainActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);      }  }</code></pre>    <p>通过这种方式编译后,我们来看下生成的代码是怎样的。AspectJ的原理实际上是在编译的时候,根据一定的规则解析,然后插入一些代码,通过aspectjx生成的代码,会在Build目录下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/35c534b051eecfa305d0d6bb3c68c3f0.png"></p>    <p>通过反编译工具查看下生成内容:</p>    <p><img src="https://simg.open-open.com/show/6b55c3f658bc95abea587e1f0ca7b809.png"></p>    <p>我们可以发现,在onCreate的最前面,插入了一行AspectJ的代码。这个就是AspectJ的主要功能,抛开AOP的思想来说,我们想做的,实际上就是『在不侵入原有代码的基础上,增加新的代码』。</p>    <h3>AspectJ之Join Points</h3>    <p>Join Points,简称JPoints,是AspectJ的核心思想之一,它就像一把刀,把程序的整个执行过程切成了一段段不同的部分。例如,构造方法调用、调用方法、方法执行、异常等等,这些都是Join Points,实际上,也就是你想把新的代码插在程序的哪个地方,是插在构造方法中,还是插在某个方法调用前,或者是插在某个方法中,这个地方就是Join Points,当然,不是所有地方都能给你插的,只有能插的地方,才叫Join Points。</p>    <h3>AspectJ之Pointcuts</h3>    <p>Join Points和Pointcuts的区别实际上很难说,我也不敢说我理解的一定对,但这些都是概念上的内容,并不影响我们去使用。</p>    <p>Pointcuts,在我理解,实际上就是在Join Points中通过一定条件选择出我们所需要的Join Points,所以说,Pointcuts,也就是带条件的Join Points,作为我们需要的代码切入点。</p>    <h3>AspectJ之Advice</h3>    <p>又来一个Advice,Advice其实是最好理解的,也就是我们具体插入的代码,以及如何插入这些代码。我们最开始举的那个例子,里面就是使用的最简单的Advice——Before。类似的还有After、Around,我们后面来讲讲他们的区别。</p>    <h3>AspectJ之切点语法</h3>    <p>我们以前面的Demo来看下最简单的AspectJ语法:</p>    <pre>  <code class="language-java">@Before("execution(* android.app.Activity.on**(..))")  public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {  }</code></pre>    <p>这里会分成几个部分,我们依次来看:</p>    <ul>     <li>@Before:Advice,也就是具体的插入点</li>     <li>execution:处理Join Point的类型,例如call、execution</li>     <li>(* android.app.Activity.on**(..)):这个是最重要的表达式,第一个『*』表示返回值,『*』表示返回值为任意类型,后面这个就是典型的包名路径,其中可以包含『*』来进行通配,几个『*』没区别。同时,这里可以通过『&&、||、!』来进行条件组合。()代表这个方法的参数,你可以指定类型,例如android.os.Bundle,或者(..)这样来代表任意类型、任意个数的参数。</li>     <li>public void onActivityMethodBefore:实际切入的代码。</li>    </ul>    <p>这里还有一些匹配规则,可以作为示例来进行讲解:</p>    <table>     <thead>      <tr>       <th>表达式</th>       <th>含义</th>      </tr>     </thead>     <tbody>      <tr>       <td>java.lang.String</td>       <td>匹配String类型</td>      </tr>      <tr>       <td>java.*.String</td>       <td>匹配java包下的任何“一级子包”下的String类型,如匹配java.lang.String,但不匹配java.lang.ss.String</td>      </tr>      <tr>       <td>java..*</td>       <td>匹配java包及任何子包下的任何类型,如匹配java.lang.String、java.lang.annotation.Annotation</td>      </tr>      <tr>       <td>java.lang.*ing</td>       <td>匹配任何java.lang包下的以ing结尾的类型</td>      </tr>      <tr>       <td>java.lang.Number+</td>       <td>匹配java.lang包下的任何Number的自类型,如匹配java.lang.Integer,也匹配java.math.BigInteger</td>      </tr>     </tbody>    </table>    <table>     <thead>      <tr>       <th>参数</th>       <th>含义</th>      </tr>     </thead>     <tbody>      <tr>       <td>()</td>       <td>表示方法没有任何参数</td>      </tr>      <tr>       <td>(..)</td>       <td>表示匹配接受任意个参数的方法</td>      </tr>      <tr>       <td>(..,java.lang.String)</td>       <td>表示匹配接受java.lang.String类型的参数结束,且其前边可以接受有任意个参数的方法</td>      </tr>      <tr>       <td>(java.lang.String,..)</td>       <td>表示匹配接受java.lang.String类型的参数开始,且其后边可以接受任意个参数的方法</td>      </tr>      <tr>       <td>(*,java.lang.String)</td>       <td>表示匹配接受java.lang.String类型的参数结束,且其前边接受有一个任意类型参数的方法</td>      </tr>     </tbody>    </table>    <h2>AspectJ实例</h2>    <h3>Before、After</h3>    <p>这两个Advice应该是使用的最多的,所以,我们先来看下这两个Advice的实例,首先看下Before和After。</p>    <pre>  <code class="language-java">@Before("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))")  public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {      String key = joinPoint.getSignature().toString();      Log.d(TAG, "onActivityMethodBefore: " + key);  }    @After("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))")  public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable {      String key = joinPoint.getSignature().toString();      Log.d(TAG, "onActivityMethodAfter: " + key);  }</code></pre>    <p>经过上面的语法解释,现在看这个应该很好理解了,我们来看下编译后的类:</p>    <p><img src="https://simg.open-open.com/show/b943f2efdae5d031d2cb24df12ed8aac.png"></p>    <p>我们可以看见,在原始代码的基础上,增加了Before和After的代码,Log也能被正确的插入并打印出来。</p>    <h3>Around</h3>    <p>Before和After其实还是很好理解的,也就是在Pointcuts之前和之后,插入代码,那么Around呢,从字面含义上来讲,也就是在方法前后各插入代码,是的,他包含了Before和After的全部功能,代码如下:</p>    <pre>  <code class="language-java">@Around("execution(* com.xys.aspectjxdemo.MainActivity.testAOP())")  public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {      String key = proceedingJoinPoint.getSignature().toString();      Log.d(TAG, "onActivityMethodAroundFirst: " + key);      proceedingJoinPoint.proceed();      Log.d(TAG, "onActivityMethodAroundSecond: " + key);  }</code></pre>    <p>其中,proceedingJoinPoint.proceed()代表执行原始的方法,在这之前、之后,都可以进行各种逻辑处理。</p>    <p>原始代码:</p>    <pre>  <code class="language-java">public class MainActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          testAOP();      }        public void testAOP() {          Log.d("xuyisheng", "testAOP");      }  }</code></pre>    <p>我们先来看下编译后的代码:</p>    <p><img src="https://simg.open-open.com/show/6cf143bfd774c63cdb498b4dfa39d9ab.png"></p>    <p>我们可以发现,Around确实实现了Before和After的功能,但是要注意的是,Around和After是不能同时作用在同一个方法上的,会产生重复切入的问题。</p>    <h3>自定义Pointcuts</h3>    <p>自定义Pointcuts可以让我们更加精确的切入一个或多个指定的切入点。</p>    <p>首先,我们需要自定义一个注解类,例如——DebugTool.java:</p>    <pre>  <code class="language-java">/**   * 自定义AOP注解   * <p>   * Created by xuyisheng on 17/1/12.   */    @Retention(RetentionPolicy.CLASS)  @Target({ElementType.CONSTRUCTOR, ElementType.METHOD})  public @interface DebugTool {  }</code></pre>    <p>然后在需要插入代码的地方使用这个注解:</p>    <pre>  <code class="language-java">public class MainActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          testAOP();      }        @DebugTool      public void testAOP() {          Log.d("xuyisheng", "testAOP");      }  }</code></pre>    <p>最后,我们来创建自己的切入文件。</p>    <pre>  <code class="language-java">@Pointcut("execution(@com.xys.aspectjxdemo.DebugTool * *(..))")  public void DebugToolMethod() {  }    @Before("DebugToolMethod()")  public void onDebugToolMethodBefore(JoinPoint joinPoint) throws Throwable {      String key = joinPoint.getSignature().toString();      Log.d(TAG, "onDebugToolMethodBefore: " + key);  }</code></pre>    <p>先定义Pointcut,并申明要监控的方法名,最后,在Before或者其它Advice里面添加切入代码,即可完成切入。</p>    <p>编译好的代码如下:</p>    <p><img src="https://simg.open-open.com/show/583b58ae5341cfc62a6d6b67b146048f.png"></p>    <p>通过这种方式,我们可以非常方便的监控指定的Pointcut,从而增加监控的粒度。</p>    <h3>call和execution</h3>    <p>在AspectJ的切入点表达式中,我们前面都是使用的execution,实际上,还有一种类型——call,那么这两种语法有什么区别呢,我们来试验下就知道了。</p>    <p>被切代码依然很简单:</p>    <pre>  <code class="language-java">public class MainActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          testAOP();      }        public void testAOP() {          Log.d("xuyisheng", "testAOP");      }  }</code></pre>    <p>先来看execution,代码如下:</p>    <pre>  <code class="language-java">@Before("execution(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")  public void methodAOPTest(JoinPoint joinPoint) throws Throwable {      String key = joinPoint.getSignature().toString();      Log.d(TAG, "methodAOPTest: " + key);  }</code></pre>    <p>编译之后的代码如下所示:</p>    <p><img src="https://simg.open-open.com/show/7783f1e38074491e41a9537b2804b45e.png"></p>    <p>再来看下call,代码如下:</p>    <pre>  <code class="language-java">@Before("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")  public void methodAOPTest(JoinPoint joinPoint) throws Throwable {      String key = joinPoint.getSignature().toString();      Log.d(TAG, "methodAOPTest: " + key);  }</code></pre>    <p>编译之后的代码如下所示:</p>    <p><img src="https://simg.open-open.com/show/ccdce119e72941a22b9eb7595b7d65bf.png"></p>    <p>其实对照起来看就一目了然了,execution是在被切入的方法中,call是在调用被切入的方法前或者后。</p>    <p>对于Call来说:</p>    <pre>  <code class="language-java">Call(Before)  Pointcut{      Pointcut Method  }  Call(After)</code></pre>    <p>对于Execution来说:</p>    <pre>  <code class="language-java">Pointcut{    execution(Before)      Pointcut Method    execution(After)  }</code></pre>    <h3>切入点过滤与withincode</h3>    <p>除了前面提到的call和execution,比较常用的还有一个withincode。这个语法通常来进行一些切入点条件的过滤,作更加精确的切入控制。我们可以参考下面这个例子:</p>    <pre>  <code class="language-java">public class MainActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          testAOP1();          testAOP2();      }        public void testAOP() {          Log.d("xuyisheng", "testAOP");      }        public void testAOP1() {          testAOP();      }        public void testAOP2() {          testAOP();      }  }</code></pre>    <p>testAOP1()和testAOP2()都调用了testAOP()方法,但是,现在想在testAOP2()方法调用testAOP()方法的时候,才切入代码,那么这个时候,就需要使用到Pointcut和withincode组合的方式,来精确定位切入点。</p>    <pre>  <code class="language-java">// 在testAOP2()方法内  @Pointcut("withincode(* com.xys.aspectjxdemo.MainActivity.testAOP2(..))")  public void invokeAOP2() {  }    // 调用testAOP()方法的时候  @Pointcut("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")  public void invokeAOP() {  }    // 同时满足前面的条件,即在testAOP2()方法内调用testAOP()方法的时候才切入  @Pointcut("invokeAOP() && invokeAOP2()")  public void invokeAOPOnlyInAOP2() {  }    @Before("invokeAOPOnlyInAOP2()")  public void beforeInvokeAOPOnlyInAOP2(JoinPoint joinPoint) {      String key = joinPoint.getSignature().toString();      Log.d(TAG, "onDebugToolMethodBefore: " + key);  }</code></pre>    <p>我们再来看下编译后的代码:</p>    <p><img src="https://simg.open-open.com/show/17179df07858cba4eed0cbca599a6ae4.png"></p>    <p>我们可以看见,只有在testAOP2()方法中被插入了代码,这就做到了精确条件的插入。</p>    <h3>异常处理AfterThrowing</h3>    <p>AfterThrowing是一个比较少见的Advice,他用于处理程序中 <strong>未处理的异常</strong> ,记住,这点很重要,是 <strong>未处理的异常</strong> ,具体原因,我们等会看反编译出来的代码就知道了。我们随手写一个异常,代码如下:</p>    <pre>  <code class="language-java">public void testAOP() {      View view = null;      view.animate();  }</code></pre>    <p>然后使用AfterThrowing来进行AOP代码的编写:</p>    <pre>  <code class="language-java">@AfterThrowing(pointcut = "execution(* com.xys.aspectjxdemo.*.*(..))", throwing = "exception")  public void catchExceptionMethod(Exception exception) {      String message = exception.toString();      Log.d(TAG, "catchExceptionMethod: " + message);  }</code></pre>    <p>这段代码很简单,同样是使用我们前面类似的表达式,但是这里是为了处理异常,所以,使用了*.*来进行通配,在异常中,我们执行一行日志,编译好的代码如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/63035f7cafeb890fed88055d6285570b.png"></p>    <p>我们可以看见com.xys.aspectjxdemo包下的所有方法都被加上了try catch,同时,在catch中,被插入了我们切入的代码,但是最后,他依然会throw e,也就是说,这个异常已经会被抛出去,崩溃依旧是会发生的。同时,如果你的原始代码中已经try catch了,那么同样也无法处理,具体原因,我们看一个反编译的代码:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2c4c9f9f2c2e74c8cac31b00e1aa84bc.png"></p>    <p>可以看见,实际上,原始代码的catch中,又被套了一层try catch,所以,e.printStackTrace()被try catch,也就不会再有异常发生了,也就无法切入了。</p>    <h2> </h2>    <p> </p>    <p>来自:http://blog.csdn.net/eclipsexys/article/details/54425414</p>    <p> </p>