面向切面 - hippowc/hippowc.github.io GitHub Wiki

概述

程序功能复杂后,面向对象编程出现一些不自然的现象,核心业务代码经常出现不相关联的特殊业务:如日志记录,权限验证,事务控制,性能检测,错误信息检测等。这个时候横向的抽象代码就显得必要了。这也是aop要做的事情。

面向切面框架

面向切面有很多框架,不同的语言也有不同的实现方式,这里仅探讨下java相关的框架:

  • Aspectj:Aspectj一个易用的、功能强大的aop编程语言。它稍微扩展类Java语言,增加了一些keyowords,有自己的编译器。其官网地址是:http://www.eclipse.org/aspectj/。它把自己定义为一种语言。
  • Spring Aop:之前spring有自己的aop实现,是通过java动态代理,在运行期织入的,比较难用,现在springboot,使用的注解类型的aop框架,引用的Aspectj的jar包,也就是说底层是aspectj实现的。

基本上来说,只有AspectJ这一种框架了,只是直接使用AspectJ,以及通过springboot使用AspectJ的语法有些不同。不过他们的几个基本概念是相同的。

切面的几个概念

  • Joinpoint:连接点,表示在程序的某个地方的扩展点。连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等。spring中只支持方法执行Joinpoint。AspectJ支持多种。
  • Pointcut:Joinpoint的集合,spring默认使用AspectJ的语法来表示。通常和Advice一块使用。
  • Advice:定义了两个信息,一个是要做的具体事情,另一个是执行的具体时机,对于方法执行这个Joinpoint来说,有前置(before advice)、后置(after advice)、环绕(around advice)以及异常几个时机
  • Aspect:切面,是通知、引入和切入点的组合。在springboot中体现为一个类。
  • Introduction:内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象targetObject
  • Target Object:要被织入横切关注点的对象
  • AOP Proxy:AOP框架使用代理模式创建的对象,在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面
  • Weaving:织入是一个过程,是将切面应用到目标对象从而创建出AOP代理对象的过程,织入可以在编译期、类装载期、运行期进行。可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

springboot的aop实现原理

后置处理器:AnnotationAwareAspectJAutoProxyCreator

  • @Aspect 切面类的处理方法:Aspect是一个类似Component的注解,表示这是一个bean,只要是bean,就会调用getBean方法,只要调用 getBean 方法,都会调用 AbstractAutowireCapableBeanFactory 的 createBean 方法,该方法其中有一行函数调用
// give beanPostProcessors a chance to return a proxy instead of the target bean instance.
Object bean = resolveBeforeInstantiation...

看注解可以看出,这个有机会返回一个代理。这里会调用所有注册的后置处理器的postProcessBeforeInstantiation方法,而增强类的方法就在AnnotationAwareAspectJAutoProxyCreator的postProcessBeforeInstantiation方法中。这个方法会判断该bean是否是Aspect类型,并找到该类中所有的advisor,以及pointcut表达式

上面为处理切面类的逻辑,下面为处理需要增强类的逻辑。

在doCreateBean的initializeBean的方法中,看下这个bean是否在需要增强类的列表中,如果在,则进入DefaultAopProxyFactory的createProxy类中,然后看下如何具体创建代理类

  • spring创建代理类: 有两种方法:一个是jdk动态代理,一个可以使用cglib代理。springboot的aop默认使用cglib代理

cglib cglib代理和jdk代理目的差不多,但是cglib代理不仅可以代理接口,还可以代理类,比jdk代理作用 范围更加广泛一些,cglib代理借助了ASM这个非常强大的Java字节码生成框架。

代理的研究:TODO

pointcut表达式

pointcut是指哪些方法需要被执行aop,由pointcut expression来描述。表达式可以使用下列方式进行定义,并通过&&和||和!的方式来组合

标准的Aspectj Aop的pointcut的表达式类型是很丰富的,但是Spring Aop只支持其中的9种,外加Spring Aop自己扩充的一种一共是10种类型的表达式

  • execution: 用于指定方法执行,execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?) ,支持*通配,其中returning type pattern,name pattern, and parameters pattern是必须的.

ret-type-pattern:可以为表示任何返回值,全路径的类名等. name-pattern:指定方法名, 代表所有 set代表以set开头的所有方法. parameters pattern:指定方法参数(声明的类型),(..)代表所有参数,()代表一个参数 (*,String)代表第一个参数为任何值,第二个为String类型.

  • within:指定某些类型的全部方法执行,也可以指定一个包:

pointcutexp包里的任意类. within(com.test.spring.aop.pointcutexp.)pointcutexp包和所有子包里的任意类.within(com.test.spring.aop.pointcutexp..)

  • this:

  • target:

  • args:args用来匹配方法参数的 1、“args()”匹配任何不带参数的方法。 2、“args(java.lang.String)”匹配任何只带一个参数,而且这个参数的类型是String的方法。 3、“args(..)”带任意参数的方法。 4、“args(java.lang.String,..)”匹配带任意个参数,但是第一个参数的类型是String的方法。 5、“args(..,java.lang.String)”匹配带任意个参数,但是最后一个参数的类型是String的方法。

  • @target: 匹配当被代理的目标对象对应的类型及其父类型上拥有指定的注解时。 @target(com.elim.spring.support.MyAnnotation)”匹配被代理的目标对象对应的类型上拥有MyAnnotation注解时。

  • @args: @args匹配被调用的方法上含有参数,且对应的参数类型上拥有指定的注解的情况 “@args(com.elim.spring.support.MyAnnotation)”匹配方法参数类型上拥有MyAnnotation注解的方法调用。

  • @within用于匹配被代理的目标对象对应的类型或其父类型拥有指定的注解的情况,但只有在调用拥有指定注解的类上的方法时才匹配。 “@within(com.elim.spring.support.MyAnnotation)”匹配被调用的方法声明的类上拥有MyAnnotation注解的情况。比如有一个ClassA上使用了注解MyAnnotation标注,并且定义了一个方法a(),那么在调用ClassA.a()方法时将匹配该Pointcut;如果有一个ClassB上没有MyAnnotation注解,但是它继承自ClassA,同时它上面定义了一个方法b(),那么在调用ClassB().b()方法时不会匹配该Pointcut,但是在调用ClassB().a()时将匹配该方法调用,因为a()是定义在父类型ClassA上的,且ClassA上使用了MyAnnotation注解。但是如果子类ClassB覆写了父类ClassA的a()方法,则调用ClassB.a()方法时也不匹配该Pointcut

  • @annotation用于匹配方法上拥有指定注解的情况。 “@annotation(com.elim.spring.support.MyAnnotation)”匹配所有的方法上拥有MyAnnotation注解的方法外部调用。

  • bean:bean用于匹配当调用的是指定的Spring的某个bean的方法时 1、“bean(abc)”匹配Spring Bean容器中id或name为abc的bean的方法调用。 2、“bean(user*)”匹配所有id或name为以user开头的bean的方法调用。

其中,within,target和this,很像,有什么区别呢?

首先这几个参数都是类,然后匹配这些类中的方法

apsectj是动态、静态植入结合的。 那么Target() this()就是属于他动态植入的方式,within是静态植入的。故target(),this()需要在运行时才能确定那些被拦截。 比如刚才的例子,我们在给Animal加多一个实现类,用target() 他仍然可以被拦截。 所以target()和this()会用继承关系作用,也就是说:如果你的signature是一个基类,那么这个pointcut同时也会对他的子类也起作用。 另外target 和 this 可以获取他们对应的实例。 但是within没法作到

详细:https://blog.csdn.net/zl3450341/article/details/7673979

spring中只支持方法执行这个切入点,还有call(方法调用切入点)等是不支持的。

几个问题

多个切面同时作用与一个类是,增强方法执行顺序?

先看AspectJ的通知优先级:

  • 单个aspect内部,before,around,advice:先声明先执行;after advice也是先定义先执行,但是后定义的优先级高,高优先级的后执行
  • 多个aspect之间:提供了declare precedence类定义优先级

Spring AOP 采用和 AspectJ 一样的优先顺序来织入增强处理:在进入连接点时,高优先级的增强处理将先被织入;在退出连接点时,高优先级的增强处理会后被织入。在spring中的实现方式为:实现Order接口或者增加@Order注解

扩展:AspectJ

新定义的几个概念:pointcuts,advice,inner-type declaration,aspect。其中pointcuts和advice动态影响程序流,inner-type declaration 静态影响类的层级和结构,aspect用来封装这些结构。

一个关键的元素:连接点joint point模型的定义,当前指讨论一个模型:method call,它包含了一个对象接受一个方法调用的所有动作

pointcuts:挑选出来的特定的一些joint point组合,它也可以由其他的pointcuts通过 && || !组合而成。 pointcuts可以基于名称来定义,如:call(void Figure.make(*)),也可以基于特性来定义横切(property-based),如:cflow()基于是否出现在该jointpoint的dynamic context中, cflow(move())

pointcuts不仅可以采集连接点,还可以获得当前jointpoint的context,pointcuts可以采集到的context内容发布到advice的参数列表中,其中:target,this,args使用用来发布这些context内容的pointcuts。基于名称的pointcuts可以含有参数,这使他可以向target这种一样发布context

inner-type declaration可以横跨多个class定义成员或者改变类的继承关系。这其中有个概念introduction,它和advice动态操作不同,它是在编译期间,静态的进行操作。 其实java可以通过继承和实现接口,获取统一的能力(譬如成员变量),inner-type也提供了一种类似的能力,它可以将这些字段或者方法,单独定义处理,在和已经存在的类相关联。

譬如:需要给类Point新增一个字段observers,可以这样定义:

aspect PointObserving {
    private Vector Point.observers = new Vector();

    public static void addObserver(Point p, Screen s) {
        p.observers.add(s);
    }
    public static void removeObserver(Point p, Screen s) {
        p.observers.remove(s);
    }

    pointcut changes(Point p): target(p) && call(void Point.set*(int));

    after(Point p): changes(p) {
        Iterator iter = p.observers.iterator();
        while ( iter.hasNext() ) {
            updateObserver(p, (Screen)iter.next());
        }
    }

    static void updateObserver(Point p, Screen s) {
        s.display(p);
    }
}

然后增加相应的切点,当point的set方法执行后,对所有observer进行操作(不过不知道啥时候这些observer被加进去的)

Aspect:将所有的这些结构封装成一个模块,它像一个类,可以有成员变量,方法,还有构造函数。aspect也可以被实例化,但是是由AspectJ框架来控制,而不是用new

aspect的用途:开发用aspect的一些例子

这些切面提供的功能是为了方便开发用的,譬如debug,profile,trace等,当然相对应的还有生产用的aspect,这些直接参与线上代码。

tracing:debug代码可以单独写,不需嵌入到实际代码中

profiling and logging:

withincode 这个pointcuts的使用,以及declare error,可以再编译期间报错。

aspect的用途:生产用aspect的一些例子

如果说开发中aspect的功能是为了更好的看到程序的内部结构,那么生产中使用aspect就是为了增加新功能了。eg:change monitoring,context passing,consistent behavior(譬如记日志)

详细介绍AspectJ语言

 1 aspect FaultHandler {
 2
 3   private boolean Server.disabled = false;
 4
 5   private void reportFault() {
 6     System.out.println("Failure! Please fix it.");
 7   }
 8
 9   public static void fixServer(Server s) {
10     s.disabled = false;
11   }
12
13   pointcut services(Server s): target(s) && call(public * *(..));
14
15   before(Server s): services(s) {
16     if (s.disabled) throw new DisabledException();
17   }
18
19   after(Server s) throwing (FaultException e): services(s) {
20     s.disabled = true;
21     reportFault();
22   }
23 }

一个aspect可以包含这些内容:一个Server类的inner-type field,两个方法,一个pointcut定义,两个通知定义。

pointcuts

pointcuts会选出一些join point,例子: pointcut services(Server s): target(s) && call(public * *(..)) 这个pointcut名称为services,它的joint point集合是:程序执行过程中,Server对象的public方法被调用时。一个例子:

class Point {
    private int x, y;

    Point(int x, int y) { this.x = x; this.y = y; }

    void setX(int x) { this.x = x; }
    void setY(int y) { this.y = y; }

    int getX() { return x; }
    int getY() { return y; }
}

关于这段代码: void setX(int x) { this.x = x; } 可以这么描述:当一个方法名为setX,带有一个参数int被Point对象调用是,会执行方体的这段代码:{ this.x = x; };相似的构造函数也可以这样描述。在面向对象的程序中,如java的join points包含:方法调用,方法执行,对象实例化,构造函数执行,field references以及处理异常。

pointcut语法:左边是pointcut名称和参数,中间是冒号:,右边是pointcut本身。

具体语法见可以看官网:pointcut语法

call和execution: 分别指的是方法被调用,和方法被执行。还是有些区别的,call是指在方法调用的地方,而execution是已经开始执行这个方法了。

pointcut params

pointcut setter(): target(Point) &&
                     (call(void setX(int)) ||
                      call(void setY(int)));
  pointcut setter(Point p): target(p) &&
                            (call(void setX(int)) ||
                             call(void setY(int)));

这两个取得的是同样的join points,只是上一个不会发布任何context到pointcut上。

this & target & within 一般与call结合调用,target(Type)表示调用方法的对象为Type对象,而this(Type)表示方法的调用在Type对象中执行或者说当前正在执行的对象是Type类型,

而within(Myclass)表示正在执行的方法属于类Myclass,感觉应该和execution一块使用

写pointcut的一些最佳实践:基本上有三种类型的pointcut:kinded,scoping,context

  • kinded指选择一种特定类型的join point:execution,get,set,call,handler
  • scoping指选择一组join point:within,withincode
  • 基于context进行匹配的:this,target,@annotation 一个好的pointcut要至少包含前两种,如果需要基于context或者publish参数则加上第三种

advice

advice将pointcut和一段要执行的代码组合到一起。

advice有这么几种:

  • before,在pointcut之前执行
  • after,在pointcut之后执行,不管是否正常返回或者抛异常
  • after returning 当正常return之后执行,这时返回结果可以获得
  • after throwing 当抛出异常之后执行
  • around 使用proceed代替join point的执行

inter-type declarations

aspect可以定义变量,方法,构造方法,统称为inter-type members,一个例子: private boolean Server.disabled = false; 这个定义表明每个Server类都有一个boolean字段disabled,初始化为false。private表明对于aspect是私有的,只有这个aspect中的code才能访问这个字段。如果类中原本就有这个字段,也不会冲突,因为private。所以这时也没有必要去考虑织入到类中是什么类型,因为这个类中的代码也访问不到。

public int Point.getX() { return this.x; } 表示在Point类中有类一个int类型的getX方法,其中this表示当前Point对象,由于method是public如果其他地方有这个方法,那么会出现冲突。

declare parents: Point implements Comparable; 表示Point class实现Comparable接口,当然如果同时不去实现这个接口的方法会报错。

declare parents: Point extends GeometricObject; 表示Point class继承GeometricObject

一个aspect中可以定义多个inter-type

其他

thisJoinPoint内部变量,可以在advice中使用,类似与类中的this

使用

aspect首页

1、使用mvn和ide mvn需要引入一个jar包以及编译插件:

<dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.8.7</version>
      </dependency>
<!-- AspectJ 编译插件 -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
        <version>1.8</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>       <!-- use this goal to weave all your main classes -->
              <goal>test-compile</goal>  <!-- use this goal to weave all your test classes -->
            </goals>
          </execution>
        </executions>
      </plugin>

之后可以现在mvn中编译之后,到idea中去查看编译后的代码,会发现编译后的代码已经插入了切面。切面代码可以使用注解也可以使用原生的AspectJ语言,可以放在任意位置,编译器会去寻找。

ps:maven-compiler-plugin可以配置也可以不配置,默认在编译期都会调用这个plugin,mvn会先执行完默认的编译,再进行aspect的编译工作。在mvn编译完后,ide中运行会优先使用编译好的代码(至少idea是这样)

ps1: 可以使用mvn的一个插件进行执行,这样就可以确定使用mvn编译的class进行执行了,由不用自己使用java命令去执行。

mvn exec:java -Dexec.mainClass="xx.xx.App"

2、也可以自己使用aspectJ的编译工具手动进行编译并执行(不推荐),但是有助于我们理解AspectJ的使用以及结构:

下载aspectJ的jar包,下载的jar包版本号应该和jdk的版本号相同,这个安装程序使用java写的,可以使用java -jar xxx.jar执行并安装,一路next

最后根据提示增加classpath和path,我classpath没加,似乎也没问题

使用ajc去编译,或者最好使用maven去做,引入相关jar包,以及配置编译插件

AspectJ的结构

aspectJ支持两套语法,一套是通过注解,这种不需要额外的语法插件支持,一套是通过aspect自定义的语法

主要jar包:aspectjrt.jar 提供运行时,定义了各种语法

aspectjweaver.jar,主要是提供了一个java agent用于在 类加载期 间织入切面(Load time weaving)。并且提供了对切面语法的相关处理等基础方法,供ajc使用或者供第三方开发使用。这个包一般我们不需要显式引用,除非需要使用LTW

aspectjtools.jar 提供了ajc编译工具,可以在编译期将将java文件或者class文件或者aspect文件定义的切面织入到业务代码中。通常这个东西会被封装进各种IDE插件或者自动化插件中

它支持三种时机织入代码, 编译时织入 ,利用ajc编译器替代javac编译器,直接将源文件(java或者aspect文件)编译成class文件并将切面织入进代码。 编译后织入 ,利用ajc编译器向javac编译期编译后的class文件或jar文件织入切面代码。 加载时织入 ,不使用ajc编译器,利用aspectjweaver.jar工具,使用java agent代理在类加载期将切面织入进代码。

我比较关注编译时和编译后织入,尤其是编译后织入,这样能最大化的解耦切面代码和主干代码。

织入切面代码

不管使用ajc编译器编译,还是使用maven的编译插件进行编译,本质都是使用aspectjtools.jar。前两种都得将aspect的代码与工程代码放在统一工程下,不够解耦,所以就看出直接使用aspectjtools.jar的必要性。

1 编译时织入

使用方法:使用java命令执行aspectjtool.jar包并指定aspectjrt的classpath,以及需要编译的路径。

#!/usr/bin/env bash

ASPECTJ_TOOLS=/xxx/aspectjtools-1.8.9.jar
ASPECTJ_RT=/xxx/aspectjrt-1.8.9.jar
java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -sourceroots <javaDir>:<aspectDir> -d <classDir>

使用这个命令可以实现目标类和切面类的分离,其中-sourceroots是aj的参数,可以填写多个通过 : 分割, 譬如:

java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -sourceroots <javaDir>:<aspectDir>

这样执行过后,会生成主类的class文件和aj的class文件,在执行时,需要将这些class都放入classpath,包括AJ_RT aspectj的runtime包(因为这里面包含类很多生成的类定义)

2 编译后织入 当切面文件还没编译时:

java -jar $AJ_TOOLS_8 -cp $AJ_RT_8 -inpath <javaDir> -sourceroots <aspectDir> -d <targetDir>

当切面文件也被编译后:

java -jar $AJ_TOOLS_8 -cp $AJ_RT_8 -inpath <classDir>:<aspectClassDir> -d <targetDir>

注意:执行经过切面的文件后,要在classpath中加入aspect的runtime和编译完后的切面类

另外,以上的dir也可以替换成jar包,也就是说aspect也可以织入jar包,但是它会将jar包的东西都解压出来,而不是直接在jar中织入,也比较好理解,因为织入后还需要依赖新的aspect类,也就是需要重新打下包。其中sourceroots不能是jar包,inpath的路径可以是jar包,其中aspect文件如果没有与具体类的引用,也可以先单独进行编译,然后打入jar包,然后放入inpath中,也是会先进行解压再织入。

3 加载时织入 加载时织入摆脱ajc编译器,直接使用javac编译所有代码(额,不包括aj文件,这时只能通过注解方式定义切面),再使用java agent在运行时织入,但是在加载时处理的化,我们并不知道某个类需要被切面处理,那么需要一个配置文件,告诉类加载器。不赘述

#!/usr/bin/env bash
ASPECTJ_WEAVER=/xxx/aspectjweaver-1.8.13.jar
ASPECTJ_RT=/xxx/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=/xxx/aspectjtools-1.8.9.jar

java -javaagent:$ASPECTJ_WEAVER -cp $ASPECTJ_RT:target/classes/ com.mythsman.test.App

详细可以参照:https://www.colabug.com/2102191.html

⚠️ **GitHub.com Fallback** ⚠️