Spring源码(8)-AOP


一、AspectJ

spectJ是Java的一个AOP框架,可以单独使用,也可以整合到其它框架中。

AspectJ是Eclipse旗下的一个项目。至于它和Spring AOP的关系,不妨可将Spring AOP看成是Spring这个庞大的集成框架为了集成AspectJ而出现的一个模块。

毕竟很多地方都是直接用到AspectJ里面的代码。典型的比如 @Aspect@Around@Pointcut 注解等等。而且从相关概念以及语法结构上而言,两者其实非常非常相似。比如 Pointcut 的表达式语法以及 Advice 的种类,都是一样一样的。

与 Spring AOP 最大的区别在于两者实现AOP的底层原理不太一样:

  • Spring AOP: 基于代理(Proxying)
  • AspectJ: 基于字节码操作(Bytecode Manipulation)

织入方式

AspectJ 提供了三种方式来实现 AOP,通过在类加载的不同时间段来完成相关代码的织入以达到目的。具体有以下三种方式:

  • 编译期(compiler-time)织入:在类进行编译的时候就将相应的代码织入到元类文件的 .class 文件中
  • 编译后(post-compiler)织入:在类编译后,再将相关的代码织入到 .class 文件中
  • 加载时(load-time) 织入:在 JVM 加载 .class 文件的时候将代码织入

springAOP使用运行时织入(runtime weaving)

  • 在运行时织入,是使用目标对象的代理对象织入的。

AspectJ代码织入原理

其实就是在编译时,修改源代码,在对应的方法最前面加上织入的代码

1、ajc 增强

先看aop的第一种实现ajc编译器代码增强,这是一种编译时的代码增强。

新建一个普通的maven项目

  • 添加依赖

    使用ajc编译器进行代码增强,首先需要在pom.xml文件中加入ajc编译器插件依赖

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>aspectj-maven-plugin</artifactId>
            <version>1.11</version>
            <configuration>
                <complianceLevel>1.8</complianceLevel>
                <source>8</source>
                <target>8</target>
                <showWeaveInfo>true</showWeaveInfo>
                <verbose>true</verbose>
                <Xlint>ignore</Xlint>
                <encoding>UTF-8</encoding>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <!-- use this goal to weave all your main classes -->
                        <goal>compile</goal>
                        <!-- use this goal to weave all your test classes -->
                        <goal>test-compile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

加入aspectjweaver的依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

需要增强的类MyService

public class MyService {

    private static final Logger log = LoggerFactory.getLogger(MyAspect.class);

    public void foo() {
        log.debug("foo()");
    }
}

切面类MyAspect,编写execution表达式,对MyService类的foo()方法进行增强

@Aspect // 注意此切面并未被 Spring 管理,本项目pom文件中根本没有引入spring的相关类
public class MyAspect {
    private static final Logger log = LoggerFactory.getLogger(MyAspect.class);
    @Before("execution(* top.jacktgq.service.MyService.foo())")
    public void before() {
        log.debug("before()");
    }
}

测试代码

public class Aop_Aspectj_Test {
    @Test
    public void testAopAjc() {
        new MyService().foo();
    }
}

编译项目,这里需要使用maven来编译,打开idea中的maven面板,点击compile

然后再运行测试代码,可以看到创建MyService对象并调用foo()方法会先执行切面类中的before()方法

运行以后代码并没有增强。这是由于idea中在执行代码之前会默认编译一遍代码,这本来是正常的,可是,如果使用maven来编译代码,会在执行代码前将maven编译的代码覆盖,这就会导致maven的ajc编译器增强的代码被覆盖,所以会看不到最终的运行效果。

解决办法:在idea设置-编译器中将自动构建项目的选项勾上,就不会出现多次编译覆盖的问题了。

反编译class文件

打开编译后的MyService.class文件,双击以后idea会反编译该字节码文件,可以看到foo()方法体的开头加了一行代码,这就是增强的代码,这是ajc编译器在编译MyService类的时候为我们添加的代码,这是一种编译时的增强。

public class MyService {

    private static final Logger log = LoggerFactory.getLogger(MyAspect.class);

    public void foo() {
        MyAspect.aspectOf().before();//自动帮我们添加的代码
        log.debug("foo()");
    }
}

2、agent增强

现在来看aop的另外一种实现agent增强,这是一种类加载时的代码增强。

  • 新建一个普通的maven项目
    • 加入aspectjweaver的依赖
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

需要增强的类MyService

@Slf4j
public class MyService {
    public void foo() {
        log.debug("foo()");
        bar();
    }
    public void bar() {
        log.debug("bar()");
    }
}

切面类MyAspect,编写execution表达式,对MyService类的foo()方法进行增强

@Aspect // 注意此切面并未被 Spring 管理,本项目pom文件中根本没有引入spring的相关类
@Slf4j
public class MyAspect {
    @Before("execution(* top.jacktgq.service.MyService.*())")
    public void before() {
        log.debug("before()");
    }
}

测试代码

public class Aop_agent_Test {
    @Test
    public void testAopAgent() throws Exception {
        MyService myService = new MyService();
        myService.foo();
        System.in.read();
    }
}

运行时需要在 VM options 里加入 -javaagent:jar包路径\aspectjweaver-1.9.7.jar

注:还需要在resources/META-INF目录下建一个aop.xml配置文件,内容如下,aspectj会自动扫描到这个配置文件,不加这个配置文件不会出效果。

<aspectj>
    <aspects>
        <aspect name="top.jacktgq.aop.MyAspect"/>
        <weaver options="-verbose -showWeaveInfo">
            <include within="top.jacktgq.service.MyService"/>
            <include within="top.jacktgq.aop.MyAspect"/>
        </weaver>
    </aspects>
</aspectj>

运行测试代码,可以看到创建MyService对象并调用foo()方法会先执行切面类中的before()方法

打开编译后的MyService.class文件,双击以后idea会反编译该字节码文件,可以看到foo()方法体中并没有添加多余的代码,所以就不是编译时增强了,而是类加载的时候增强的,这里可以借助阿里巴巴的Arthas工具,下载地址:https://arthas.aliyun.com/doc/en/download.html,解压以后进入到arthas的bin目录下,启动黑窗口,输入java -jar .\arthas-boot.jar,在输出的java进程列表里面找到我们要连接的进程,输入对应进程的序号,我这里是,连接上以后会打印ARTHAS的logo

再输入jad top.jacktgq.service.MyService反编译内存中的MyService

可以看到foo()bar()方法体的第一行都加了一行代码,这就说明通过添加虚拟机参数-javaagent的方式可以在类加载的时候对代码进行增强。

image-20221019212442319

所以这种方法的织入,即使是在本类的方法中通过 this.本类其他方法 也可以被增强。而代理方法织入的则不行。

二、Advisor切面

Spring 提供Advisor 切面和 @Aspect 切面:

  • Advisor 低级切面:更偏向于底层,在Spring源码中使用
  • @Aspect 高级切面:是高度封装之后的,更适用于普通开发的使用,其底层也是使用 Advisor

1、接口及目标类实现

先创建一个包含又foo和bar方法的接口,再创建一个Target1目标类实现该接口

interface I1 {
    void foo();
    void bar();
}

static class Target1 implements I1 {
    public void foo() {
        System.out.println("target1 foo");
    }
    public void bar() {
        System.out.println("target1 bar");
    }
}

static class Target2 {
    public void foo() {
        System.out.println("target2 foo");
    }
    public void bar() {
        System.out.println("target2 bar");
    }
}

2、准备切点

创建切点对象,调用setExpression设置切点表达式

AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* foo())");

3、准备通知

MethodInterceptor advice = new MethodInterceptor() {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("before...");
        Object res = invocation.proceed();//调用目标
        System.out.println("after...");
        return res;
    }
};

4、准备切面

创建DefaultPointcutAdvisor对象,传入切点和通知

然后再创建ProxyFactory对象,添加目标对象和切面,最后通过代理对象调用方法完成测试

ProxyFactory内部会根据不同的情况选择cglib或者jdk的代理)

DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut,advice);

ProxyFactory factory = new ProxyFactory();
factory.setTarget(new Target1());//添加目标
factory.addAdvisor(advisor);//添加切面

I1 proxy = (I1) factory.getProxy();
proxy.foo();
proxy.bar();

5、选择代理的方式

ProxyFactory内部存在一个成员变量proxyTargetClass决定spring的代理方式

  • proxyTargetClass = false:如果目标实现接口,则用 jdk 动态代理
  • proxyTargetClass = false:如果目标没实现接口,则用 cglib 动态代理
  • proxyTargetClass = true:无论如何都用 cglib 动态代理

(1)情况1

proxyTargetClass = false,而且目标实现了接口, 使用 JdkDynamicAopProxy 实现(默认proxyTargetClass为alse)

DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut,advice);

Target1 target1 = new Target1();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target1);//添加目标
factory.addAdvisor(advisor);//添加切面

factory.setInterfaces(target1.getClass().getInterfaces());//添加目标类上实现的接口
//factory.setProxyTargetClass(false);

I1 proxy = (I1) factory.getProxy();

proxy.foo();
proxy.bar();

(2)情况2

proxyTargetClass = false,而且目标没有接口, 使用 ObjenesisCglibAopProxy 实现

DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut,advice);

Target1 target1 = new Target1();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target1);//添加目标
factory.addAdvisor(advisor);//添加切面

//factory.setProxyTargetClass(false);

I1 proxy = (I1) factory.getProxy();

proxy.foo();
proxy.bar();

(3)情况3

proxyTargetClass = true总是使用 ObjenesisCglibAopProxy 实现

DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut,advice);

Target1 target1 = new Target1();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target1);//添加目标
factory.addAdvisor(advisor);//添加切面

factory.setProxyTargetClass(true);

I1 proxy = (I1) factory.getProxy();

proxy.foo();
proxy.bar();

6、切点匹配

先定义一个静态类T1,其中有用 @Transactional 注解的foo方法和一个普通的bar方法

再定义一个使用 @Transactional 注解标注的静态类T2

然后再定义一个用 @Transactional 注解标注的接口,内有方法foo,最后创建一个类实现该接口

static class T1 {
    @Transactional
    public void foo() { }
    public void bar() { }
}

@Transactional
static class T2 {
    public void foo() { }
}

@Transactional
interface I3 { void foo(); }
static class T3 implements I3 {
    public void foo() {
    }
}

1、创建AspectJExpressionPointcut对象,然后调用setExpression方法,在其中填写切点表达式

2、调用matches方法判断是否与切点表达式匹配,第一个参数为对应类的方法名,第二个参数为对应类的class

public static void main(String[] args) throws NoSuchMethodException {
    //判断方法是否与切点表达式匹配
    AspectJExpressionPointcut pointcut1 = new AspectJExpressionPointcut();
    pointcut1.setExpression("execution(* bar())");
    System.out.println(pointcut1.matches(T1.class.getMethod("foo"), T1.class));
    System.out.println(pointcut1.matches(T1.class.getMethod("bar"), T1.class));

    //判断方法上的注解类型是否与切点表达式定义的类型匹配
    AspectJExpressionPointcut pointcut2 = new AspectJExpressionPointcut();
    pointcut2.setExpression("@annotation(org.springframework.transaction.annotation.Transactional)");

    // 切点匹配
    boolean b1 = pointcut2.matches(T1.class.getMethod("foo"), T1.class)
    boolean b2 = pointcut2.matches(T1.class.getMethod("bar"), T1.class)
}

3、为了保证当出现T3类的情况时(实现了被注解标注的接口)@Transactional注解能够被匹配到,在spring内部并不是采用以上方法匹配@Transactional注解,而是创建StaticMethodMatcherPointcut对象并重写matches方法,在方法内部添加匹配注解的逻辑

//匹配@Transactional注解
StaticMethodMatcherPointcut pointcut3 = new StaticMethodMatcherPointcut() {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        //检查方法上是否添加@Transactional注解
        MergedAnnotations annotations = MergedAnnotations.from(method);
        if (annotations.isPresent(Transactional.class)) {
            return true;
        }
        //检查类上是否添加@Transactional注解,并添加检查策略
        annotations = MergedAnnotations.from(targetClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
        if (annotations.isPresent(Transactional.class)){
            return true;
        }
        return false;
    }
};

//测试
//true
System.out.println(pointcut3.matches(T1.class.getMethod("foo"), T1.class));
//false
System.out.println(pointcut3.matches(T1.class.getMethod("bar"), T1.class));    
//true
System.out.println(pointcut3.matches(T2.class.getMethod("foo"), T2.class));
//true
System.out.println(pointcut3.matches(T3.class.getMethod("foo"), T3.class));    

MergedAnnotations.from(targetClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);SearchStrategy 取值如下

img

7、注册到容器

@Configuration
public class AdvisorConfig {

   //低级切面:由一个切点和一个通知组成
   @Bean
   public Advisor advisor3(MethodInterceptor advice3){
       //定义切点
       AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
       pointcut.setExpression("execution(* foo())");
       DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice3);
       return advisor;
   }

   //定义通知
   @Bean
   public MethodInterceptor advice3() {
       return new MethodInterceptor() {
           @Override
           public Object invoke(MethodInvocation invocation) throws Throwable{
               System.out.println("advice3 before");
               Object res = invocation.proceed();
               System.out.println("advice3 after");
               return res;
           }
       };
   }
}

三、自动代理后处理器

自动代理后处理器 AnnotationAwareAspectJAutoProxyCreator 作用:

  • 找到所有的 高级切面 @Aspect 和低级切面 Advisor,并将高级切面转化为低级切面
  • 根据这些切面创建代理对象

通常代理创建的活在原始对象初始化后执行, 但碰到循环依赖会提前至依赖注入之前执行

image-20221024222425041

1、findEligibleAdvisors()

来自 AbstractAdvisorAutoProxyCreator 的方法

解析某个类的切面,一部分切面是低级的,如准备代码中的advisor3;另一部分是高级的, 由解析 @Aspect 后获得。解析时会将高级切面解析成多个低级切面

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, 
                                             String beanName) {
    List<Advisor> candidateAdvisors = findCandidateAdvisors();

    List<Advisor> eligibleAdvisors = 
        findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);

    extendAdvisors(eligibleAdvisors);

    if (!eligibleAdvisors.isEmpty()) {
        eligibleAdvisors = sortAdvisors(eligibleAdvisors);
    }
    return eligibleAdvisors;
}

  目录