likes
comments
collection
share

AOP之AspectJ的应用和原理解析

作者站长头像
站长
· 阅读数 6

AOP

AOP(Aspect Oriented Programming)就是面向切面编程,是OOP的延续。面向对象编程有三个特点:封装,继承,多态。其中封装就是将各个功能分散到不同的对象中,让不同的类有不同的职责,通过这种方式,可以实现代码的复用,降低复杂度。但是在这个过程中,代码的变得分散的同时,也增加了代码的重复性,比如说记录日志,如果几个类都需要记录,那么针对每个类,都需要添加相应的方法。而如果讲这些代码写在一个单独的类中使用单独的方法,然后在不同的类中进行调用又回增加类与类之间的耦合,这个类的改变会对其他类造成影响。面对这些问题,有没有什么更加有效的方法呢?

我们可以通过预编译的方式和运行期动态代理的方式在不同的时期动态的将代码切入到类的指定位置上,从而实现在不修改源码的前提下动态的添加一些功能,这就是AOP。

核心概念

这里首先解释AOP几个相关的术语

  • 连接点Join Points:在代码中可以被切入的切入点,在Aspectj中,方法、构造方法的执行(execution)、方法、构造方法的调用(call)、属性的读取(get)、属性的设置(set)、异常处理(handler)等都可以当做切入点

  • 切点PointCut:切入点是带有条件连接点,在程序中主要体现为编写切入点表达式。该表达式是对连接点进行拦截的定义。切入点用于定义通知应该切入到哪些连接点,不同的通知需要切入到不同的连接点。

  • 通知Advice:在定义了pointcut之后,我们也就定义好了一些Java源码中的固定点,advice是定义我们要在pointcut切入的具体操作。主要有五种通知类型:Before(前置通知),AfterReturning(后置通知),AfterThrowing(异常通知),After(最终通知),Around(环绕通知)等。

  • 切面Aspect:和Java中的类类似,有抽象和继承的结构,是AOP技术中封装以上pointcut、advice等元素,一般表现为一个java文件。

  • 织入Weaving:注入代码(advice)到目标(Join Points)中。

原理(ajc编译器)

执行AspectJ的时候,我们需要使用ajc编译器,对Aspect和需要织入的Java Source Code进行编译,得到字节码后,可以使用java命令来执行。

ajc编译器会首先调用javac将Java源代码编译成字节码,然后根据我们在Aspect中定义的pointcut找到相对应的Java Byte Code部分,使用对应的advice动作修改源代码的字节码。最后得到了经过Aspect织入的Java字节码,然后就可以正常使用这个字节码了。

AspectJ的优点

  1. 功能强大:通过一个编译器+一个API库,可以实现形形色色的功能,开发者可以有很大的操作空间。
  2. 非侵入式:可以在不修改原有程序的前提下,对原有逻辑进行修改和监控。
  3. 性能强大:在编译期间进行代码修改和注入,对于运行期没有性能损耗。
  4. 友好:使用java代码来操作字节码,不必对字节码有过多的了解就可以进行修改,对开发者非常友好。

实战

也许以上的概念介绍让您一脸蒙圈,下边就通过代码来直观的感受下AspectJ的神奇之处。

定义切面

@Aspect
public class AspectJTest {

}

也就是使用Aspect注解一个类,表明这个类就是一个切面类。

定义JoinPoint,切入点表达式

AspectJ支持三种通配符:

* 匹配任意字符,只匹配一个元素

.. 匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用

+ 表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.test.Animal+ ,表示继承该类的所有子类包括本身

JoinPoint的定义需要还需要遵循一定的规则,比如下面的execution

execution(<修饰符><返回类型><包.类.方法(参数)><异常>) //修饰符和异常可以省略

表达式    execution(* com.test.Test.*(..))
含义      Test类中声明的所有方法。
第一个“*”代表任意修饰符及任意返回值。
第二个“*”代表任意方法。
“..”匹配任意数量、任意类型的参数。
若目标类、接口与该切面类在同一个包中可以省略包名。

表达式    execution(public * Test.*(..))
含义      Test类中的所有公有方法

表达式    execution(public double Test.*(..))
含义      Test类中返回double类型数值的方法

表达式    execution(**test(..))
含义      以test结尾的方法
“..”     匹配任意数量、任意类型的参数。

表达式   execution(* com.test.*.*(..)) 
含义     匹配 com.test 包下的所有类的所有方法

表达式    execution(public double Test.*(double, double))
含义      Test类中参数类型为double,double类型的方法

在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。

表达式    execution (* *.add(int,..)) || execution(* *.sub(int,..))
含义      任意类中第一个参数为int类型的add方法或sub方法

表达式    !execution (* *.add(int,..)) 
含义      匹配不是任意类中第一个参数为int类型的add方法

execution代表方法的执行,除此之外还有其他的修饰符可供选择,并且不同的修饰符对应不同的表达式规则,上边所说的是execution所对应的表达式规则。

修饰符含义
execution(MethodPattern)方法执行
call(MethodPattern)方法被调用
execution(ConstructorPattern)构造方法执行
call(ConstructorPattern)构造方法被调用
get(FieldPattern)读取属性
set(FieldPattern)设置属性
staticinitialization(TypePattern)static 块初始化
handler(TypePattern)异常处理

不同的修饰符对应的表达式规则各不相同:

Pattern规则(注意空格)
MethodPattern[@注解] [访问权限] 返回值类型 [类名.]方法名(参数) [throws 异常类型]
ConstructorPattern[@注解] [访问权限] [类名.]new(参数) [throws 异常类型]
FieldPattern[@注解] [访问权限] 变量类型 [类名.]变量名
TypePattern* 单独使用事表示匹配任意类型, .. 匹配任意字符串, .. 单独使用时表示匹配任意长度任意类型, + 匹配其自身及子类,还有一个 ... 表示不定个数。也可以使用` &&,

定义切点

有了切点表达式,接下来就要把这个表达式定义到切点中。

@Pointcut(executionTest)
public void executionMethod(){
}

定义Advice,也就是插入的时机

可以定义如:Before(前置通知),AfterReturning(后置通知),AfterThrowing(异常通知),After(最终通知),Around(环绕通知)等类型的通知

    @Around("executionMethod()")
    public void aroundMethod(ProceedingJoinPoint point){
    
        //doSomeThing
        try {
            point.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        //doSomeThing
    }
    
    
    @Before("executionMethod()")
    public void aroundMethod(JoinPoint point){
        //doSomething
    }
    
    @After("executionMethod()")
    public void afterMethod(){
        //doSomething
    }
  1. 经过以上几个步骤,一个简单的AspectJ切面类就完成了,看一下
@Aspect
public class AspectJTest {

    //所有onCreate方法
    private static final String METHOD_EXECUTION =
        "execution(* android.app.Activity+.onCreate(..))";

    @Pointcut(METHOD_EXECUTION)
    public void methodCutting() {
    }

    @Before("methodCutting()")
    public void beforMethodExecution(JoinPoint joinPoint) {
        printLog(joinPoint, "before method execution");
    }

    @After("methodCutting()")
    public void afterMethodExecution(JoinPoint joinPoint) {
        printLog(joinPoint, "after method execution");
    }
}

小结

AOP作为OOP的补充,在开发过程中会有很大的用处,比如日志的打印和上传,用户行为的监控,方法耗时监控等等。

当然了,一个硬币有两面,一个工具有他的优点同时也带有缺点,使用AspectJ因为在编译器搞了事情,所以肯定会增加程序编译的时间。另外,由于其切入点是固定的,虽然能满足我们很多场景的需求,但是仍然有一定的局限性,如果您对字节码的了解非常深刻,又想要更愉快地玩耍字节码,这个工具还是对于您来说还是太局促了。

总之AspectJ作为一个AOP工具为我们提供了非常大的便捷,利用好能帮助我们省去很多不必要的麻烦,同时也可以提高我们的代码的整洁度,何乐而不为呢。

参考资料