AOP之AspectJ
前言:这几天一直在学习aop切面编程,以前一直也有听过aop但是实际用的还是比较少,不是很清楚,这周版本刚上线,有点空闲时间,所以就把之前想学习的aop技术重点花时间学习了下,所以才有这篇博客总结下学习的情况,其实现在的博客大都是这个抄写那个,那个抄写这个的,其实我认为不管怎么抄写,只要自己掌握了,就没关系。
AOP技术有很多,目前比较出名流行的技术框架大概是这几类:1.APT 2.ASM 3.AspectJ,经过这几天的学习,我还是认为AspectJ是最简单最快的实现方式,所以接下来,我会对AspectJ重点总结下这几天的学习。
开始接入AspectJ步骤:
1.首先,需要在项目根目录的build.gradle中增加依赖:
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'
2.然后再主项目或者库的build.gradle中增加AspectJ的依赖和插件依赖:
apply plugin: 'android-aspectjx'
compile 'org.aspectj:aspectjrt:1.8.10'
3.excludeJarFilter解释:用该属性,可以过滤减少AspectJ扫描库文件,用法:
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'
}
AspectJ语法
Join Point
Join Point是AspectJ前言核心的地方,它就像一把刀,把程序的整个执行过程切成了一段段不同的部分。例如,构造方法调用、调用方法、方法执行、异常等等,这些都是Join Points,实际上,也就是你想把新的代码插在程序的哪个地方,是插在构造方法中,还是插在某个方法调用前,或者是插在某个方法中,这个地方就是Join Points,当然,不是所有地方都能给你插的,只有能插的地方,才叫Join Points。插入点的列举:
JoinPoint | 说明 |
---|---|
Method call | 方法被调用 |
Content Cell | Content Cell |
Method execution | 方法执行 |
Constructor call | 构造函数被调用 |
Constructor execution | 构造函数执行 |
Field get | 读取属性 |
Field set | 写入属性 |
Pre-initialization | 与构造函数有关,很少用到 |
Initialization | 与构造函数有关,很少用到 |
Static initialization | static 块初始化 |
Handler | 异常处理 |
Advice execution | 所有 Advice 执行 |
以上都是AspectJ插入代码的点。
Pointcuts
Pointcuts 是具体的切入点,可以确定具体织入代码的地方,基本的 Pointcuts 是和 Join Point 相对应的,在我理解,实际上就是在Join Points中通过一定条件选择出我们所需要的Join Points,所以说,Pointcuts,也就是带条件的Join Points,作为我们需要的代码切入点,两者相对应的关系点:
Pointcuts | 说明 |
---|---|
Join Point | Pointcuts syntax |
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 | advice excution() |
Advice
Advice是在插入点的地方植入代码,在 AspectJ 中有五种类型:Before、After、AfterReturning、AfterThrowing、Around。
Advice | 说明 |
---|---|
@Before | 在执行 Join Point 之前 |
@After | 在执行 Join Point 之后,包括正常的 return 和 throw 异常 |
@AfterReturning | Join Point 为方法调用且正常 return 时,不指定返回类型时匹配所有类型 |
@AfterThrowing | Join Point 为方法调用且抛出异常时,不指定异常类型时匹配所有类型 |
@Around | 替代 Join Point 的代码,如果要执行原来代码的话,要使用ProceedingJoinPoint.proceed() |
注意: After 和 Before 没有返回值,但是 Around 的目标是替代原 Join Point 的,所以它一般会有返回值,而且返回值的类型需要匹配被选中的 Join Point 的代码。而且不能和其他 Advice 一起使用,如果在对一个 Pointcut 声明 Around 之后还声明 Before 或者 After 则会失效。
Advice 注解修改的方法必须为 public,Before、After、AfterReturning、AfterThrowing 四种类型修饰的方法返回值也必须为 void,Advice 需要使用 JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart 时,要在方法中声明为额外的参数,@Around 方法可以使用 ProceedingJoinPoint,用以调用 proceed() 方法.
注意(Advice 不能使用 After 和 Around)
例子用法说明
/**
* Created by wuminjian on 17/11/2.
*/
@Aspect
public class FragmentAspect {
private static final String TAG = "FragmentAspect";
@Before("execution(* com.test.aspectj.MainActivity.on**(..))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
String key = joinPoint.getSignature().toString();
Log.d(TAG, "onActivityMethodBefore: " + key);
}
}
- @Aspect:这个注解的意思是一个AspectJ文件,编译器在编译的时候,就会自动去解析,然后将代码注入到相应的JPonit中。
- @Before: 是Advice,也就是具体的插入点,是指调用这个某个Jpoint之前插入代码。
- execution: 处理JPoint的类型,例如call,execution
- com.test.aspectj.MainActivity.on*(..)):这个是重要的表达式,第一个[ * ]表示返回值, 表示返回任意类型,后面的com.test.aspectj.MainActivity.on **表示典型的包名路径,其中可用 * 来进行通配.
- onActivityMethodBefore:是调用MainActivity.on**方法之前插入的实际代码。
通过这种方式编译后,我们来看下生成的代码是怎样的。AspectJ的原理实际上是在编译的时候,根据一定的规则解析,然后插入一些代码,通过aspectjx生成的代码,会在Build目录:
通过JD-GUI查看jar文件如下:
我们可以发现,在onCreate的最前面,插入了一行AspectJ的代码。这个就是AspectJ的主要功能。
自定义Pointcuts
自定义Pointcuts可以让我们更加精确的切入一个或多个指定的切入点.
- 首先,我们需要自定义一个注解类,例如——WmjLog
/**
* Created by wuminjian on 17/11/3.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface WmjLog {
}
- 接着,创建一个切入文件,内部通过一个Pointcut来指定在带有我们上面自定义注解类WmjLog注解的所有方法上进行拦截。
/**
* Created by wuminjian on 17/11/3.
*/
@Aspect
public class WmjAppLogAspect {
//在带有AopLog注解的方法进行切入(注:此处的 * *前面都要有一个空格)
@Pointcut("execution(@com.test.aspectj.model.WmjLog * *(..))")
public void wmjLogPointcut() {
}
//注意,这个函数必须要有实现,否则Java编译器会报错
@After("wmjLogPointcut()")
public void onLogPointcutAfter(JoinPoint joinPoint) throws Throwable {
Log.i("WmjAOP", "onLogPointcutAfter:" + joinPoint.getSignature());
}
}
- 最后,在app项目中写一个类来测试,代码如下:
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import com.test.aspectj.model.WmjLog;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@WmjLog
public void WmjTestAop(){
Log.i("AOP","in wmjTestAop");
}
}
通过这种方式,我们可以非常方便的监控指定的Pointcut,从而增加监控的粒度,其实AspectJ很简单关键就是Points的匹配写法。
call和execution
在AspectJ的切入点表达式中,我们前面都是使用的execution,实际上,还有一种类型——call,execution是在被切入的方法中,call是在调用被切入的方法前或者后。具体事咧就不说了,大家自己去多试下就知道了。
@Before、@After、@ Around
Before、After
这两个Advice应该是使用的最多的,所以,我们先来看下这两个Advice的实例,首先看下Before和After:
@Before("execution(* com.test.aspectj.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.test.aspectj.MainActivity.on*(android.os.Bundle))")
public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable {
String key = joinPoint.getSignature().toString();
Log.d(TAG, "onActivityMethodAfter: " + key);
}
经过上面的语法解释,现在看这个应该很好理解了,我们来看下编译后的类:
其实就是在方法的前后插入代码。
Around
Before和After其实还是很好理解的,也就是在Pointcuts之前和之后,插入代码,那么Around呢,从字面含义上来讲,也就是在方法前后各插入代码,是的,他包含了Before和After的全部功能,代码如下:
@Around("execution(* com.test.aspectj.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);
}
其中,proceedingJoinPoint.proceed()代表执行原始的方法,在这之前、之后,都可以进行各种逻辑处理。
在主工程测试代码:
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("wmj", "testAOP");
}
}
编译之后查看class文件如下:
我们可以发现,Around确实实现了Before和After的功能,但是要注意的是,Around和After是不能同时作用在同一个方法上的,会产生重复切入的问题。