⭐表示重要。

第一章:基于注解的 AOP 用到的技术(⭐)

基于注解的AOP用到的技术.png

  • 动态代理(InvocationHandler):JDK 原生的实现方法,需要被代理的目标类必须实现接口。因为该技术要求 代理对象和目标对象实现同样的接口
  • Cglib:通过 继承被代理的目标类 实现代理,所以不需要目标类实现接口。
  • AspectJ:本质上是静态代理,将代理逻辑织入被代理的目标类编译得到的字节码文件 ,所以最终效果是动态的。weaver 就是织入器,Spring 只是借用了AspectJ中的注解。

第二章:环境搭建

  • IDEA 2021+。
  • JDK 11+。
  • Maven 3.8。

  • pom.xml

  1. <!-- Spring -->
  2. <dependency>
  3. <groupId>org.springframework</groupId>
  4. <artifactId>spring-context</artifactId>
  5. <version>5.3.12</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework</groupId>
  9. <artifactId>spring-aspects</artifactId>
  10. <version>5.3.12</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.springframework</groupId>
  14. <artifactId>spring-test</artifactId>
  15. <version>5.3.12</version>
  16. </dependency>
  17. <!-- junit单元测试 -->
  18. <dependency>
  19. <groupId>junit</groupId>
  20. <artifactId>junit</artifactId>
  21. <version>4.13.2</version>
  22. <scope>test</scope>
  23. </dependency>

第三章:入门案例(⭐)

3.1 准备接口和实现类

  • Calculator.java
  1. package com.github.fairy.era.aop;
  2. /**
  3. * 计算器
  4. *
  5. * @author 许大仙
  6. * @version 1.0
  7. * @since 2021-11-08 08:50
  8. */
  9. public interface Calculator {
  10. int add(int a, int b);
  11. int sub(int a, int b);
  12. int mul(int a, int b);
  13. int div(int a, int b);
  14. }
  • CalculatorImpl.java
package com.github.fairy.era.aop.impl;

import com.github.fairy.era.aop.Calculator;
import org.springframework.stereotype.Component;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:51
 */
@Component
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    public int sub(int a, int b) {
        return a - b;
    }

    @Override
    public int mul(int a, int b) {
        return a * b;
    }

    @Override
    public int div(int a, int b) {
        return a / b;
    }
}

3.2 创建 Spring 的配置文件,配置自动扫描的包和开启 AOP 的注解支持

  • applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.github.fairy.era"></context:component-scan>

    <!-- 开启注解的AOP支持 -->
    <aop:aspectj-autoproxy/>
</beans>

3.3 创建切面类

  • LogAspect.java
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {

    /**
     * @Before注解: 声明当前方法是前置通知方法,value属性用来指定切入点表达式
     */
    @Before("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void printLogBefore() {
        System.out.println("[AOP前置通知] 方法开始了");
    }

    /**
     * @AfterReturn注解: 声明当前方法是返回通知方法,value属性用来指定切入点表达式
     */
    @AfterReturning("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void printLogAfterReturn() {
        System.out.println("[AOP返回通知] 方法开始了");
    }

    /**
     * @AfterThrowing: 声明当前方法是异常通知方法,value属性用来指定切入点表达式
     */
    @AfterThrowing("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void printLogAfterThrowing() {
        System.out.println("[AOP异常通知] 方法开始了");
    }

    /**
     * @After:声明当前方法是最终通知方法,value属性用来指定切入点表达式
     */
    @After("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void printLogAfter() {
        System.out.println("[AOP最终通知] 方法开始了");
    }

}

3.4 测试

package com.github.fairy.era.bean;

import com.github.fairy.era.aop.Calculator;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class SpringTest {

    @Autowired
    private Calculator calculator;

    @Test
    public void test() {
        int add = calculator.add(1, 2);
        System.out.println("add = " + add);
    }
}

3.5 通知执行顺序(了解)

  • Spring 版本 5.3.x 以前:
    • 前置通知。
    • 目标操作。
    • 后置通知。
    • 返回通知或异常通知。
  • Spring 版本 5.3.x 以后:
    • 前置通知。
    • 目标操作。
    • 返回通知或异常通知。
    • 后置通知。

第四章:通知的细节(⭐)

4.1 JoinPoint 接口

  • org.aspectj.lang.JoinPoint
  • JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)。
  • ② 通过目标方法前面对象获取方法名。
  • ③ 通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组。

  • 示例:

package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {

    /**
     * @Before注解: 声明当前方法是前置通知方法,value属性用来指定切入点表达式
     * 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
     */
    @Before("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void printLogBefore(JoinPoint joinPoint) {
        // 通过JoinPoint对象获取目标方法签名对象
        Signature signature = joinPoint.getSignature();
        // 通过方法的签名对象获取目标方法的详细信息
        String name = signature.getName();
        String modifier = Modifier.toString(signature.getModifiers());
        String declaringTypeName = signature.getDeclaringTypeName();
        // 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("[AOP前置通知] 方法名:" + name + ",返回修饰符:" + modifier + ",声明类型的完全限定名称:" + declaringTypeName + ",方法参数:" + Arrays.asList(args));
    }
}

4.2 方法的返回值

  • @AfterReturning 注解的 returning 属性获取目标方法的返回值。

方法的返回值.png

  • 示例:
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {

    /**
     * @AfterReturn注解: 声明当前方法是返回通知方法,value属性用来指定切入点表达式
     */
    @AfterReturning(value = "execution(public int com.github.fairy.era.aop.Calculator.add(int,int))", returning = "targetMethodReturnValue")
    public void printLogAfterReturn(Object targetMethodReturnValue) {
        System.out.println("[AOP返回通知] 方法的返回值是:" + targetMethodReturnValue);
    }

}

4.3 目标方法抛出的异常

  • @AfterThrowing 注解的 throwing 属性获取目标方法抛出的异常对象。

目标方法抛出的异常.png

  • 示例:
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {

    /**
     * @AfterThrowing: 声明当前方法是异常通知方法,value属性用来指定切入点表达式
     */
    @AfterThrowing(value = "execution(public int com.github.fairy.era.aop.Calculator.add(int,int))", throwing = "targetMethodException")
    public void printLogAfterThrowing(JoinPoint joinPoint, Throwable targetMethodException) {
        System.out.println("[AOP异常通知] 方法:" + joinPoint.getSignature().getName() + "抛出了异常,异常类型是:" + targetMethodException.getClass().getName());
    }

}

第五章:重用切入点表达式(⭐)

5.1 概述

  • 在一处声明切入点表达式之后,其他有需要的地方引用这个切入点表达式。
  • 易于维护,一处修改,处处生效。

5.2 声明切入点表达式方法

  • 通过 @Pointcut 注解将一个切入点声明成简单的方法。

  • 示例:

package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {

    /**
     * 切入点表达式重用
     */
    @Pointcut("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void pointcut() {

    }

}

5.2 同一类内部引用

  • 如果 @Pointcut 注解标注的切入点表达式方法和通知在一个方法中,那么通知可以通过方法名引入该切入点。

  • 示例:

package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {

    /**
     * 切入点表达式重用
     */
    @Pointcut("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void pointcut() {

    }

    /**
     * @Before注解: 声明当前方法是前置通知方法,value属性用来指定切入点表达式
     * 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
     */
    @Before("pointcut()")
    public void printLogBefore(JoinPoint joinPoint) {
        // 通过JoinPoint对象获取目标方法签名对象
        Signature signature = joinPoint.getSignature();
        // 通过方法的签名对象获取目标方法的详细信息
        String name = signature.getName();
        String modifier = Modifier.toString(signature.getModifiers());
        String declaringTypeName = signature.getDeclaringTypeName();
        // 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("[AOP前置通知] 方法名:" + name + ",返回修饰符:" + modifier + ",声明类型的完全限定名称:" + declaringTypeName + ",方法参数:" + Arrays.asList(args));
    }

    /**
     * @AfterReturn注解: 声明当前方法是返回通知方法,value属性用来指定切入点表达式
     */
    @AfterReturning(value = "pointcut()", returning = "targetMethodReturnValue")
    public void printLogAfterReturn(Object targetMethodReturnValue) {
        System.out.println("[AOP返回通知] 方法的返回值是:" + targetMethodReturnValue);
    }

    /**
     * @AfterThrowing: 声明当前方法是异常通知方法,value属性用来指定切入点表达式
     */
    @AfterThrowing(value = "pointcut()", throwing = "targetMethodException")
    public void printLogAfterThrowing(JoinPoint joinPoint, Throwable targetMethodException) {
        System.out.println("[AOP异常通知] 方法:" + joinPoint.getSignature().getName() + "抛出了异常,异常类型是:" + targetMethodException.getClass().getName());
    }

    /**
     * @After:声明当前方法是最终通知方法,value属性用来指定切入点表达式
     */
    @After("pointcut()")
    public void printLogAfter() {
        System.out.println("[AOP最终通知] 方法开始了");
    }
}

5.3 在不同类中引用

  • 切入点方法的访问控制符同时也控制着这个切入点的可见性。
  • 如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中,在这种情况下,它们必须被声明为 public
  • 在引入这个切入点时,必须将类名也包括在内,如果类没有与这个切面放在同一个包中,还必须包含包名。
  • 示例:
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 作为存放切入点表达式的类,可以把整个项目中所有切入点表达式全部集中过来,便于统一管理
 *
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 10:57
 */
@Component
public class GlobalPointCut {
    /**
     * 切入点表达式重用
     */
    @Pointcut("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void pointcut() {
    }
}
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {


    /**
     * @Before注解: 声明当前方法是前置通知方法,value属性用来指定切入点表达式
     * 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
     */
    @Before("com.github.fairy.era.aop.aspect.GlobalPointCut.pointcut()")
    public void printLogBefore(JoinPoint joinPoint) {
        // 通过JoinPoint对象获取目标方法签名对象
        Signature signature = joinPoint.getSignature();
        // 通过方法的签名对象获取目标方法的详细信息
        String name = signature.getName();
        String modifier = Modifier.toString(signature.getModifiers());
        String declaringTypeName = signature.getDeclaringTypeName();
        // 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("[AOP前置通知] 方法名:" + name + ",返回修饰符:" + modifier + ",声明类型的完全限定名称:" + declaringTypeName + ",方法参数:" + Arrays.asList(args));
    }

    /**
     * @AfterReturn注解: 声明当前方法是返回通知方法,value属性用来指定切入点表达式
     */
    @AfterReturning(value = "com.github.fairy.era.aop.aspect.GlobalPointCut.pointcut()", returning = "targetMethodReturnValue")
    public void printLogAfterReturn(Object targetMethodReturnValue) {
        System.out.println("[AOP返回通知] 方法的返回值是:" + targetMethodReturnValue);
    }

    /**
     * @AfterThrowing: 声明当前方法是异常通知方法,value属性用来指定切入点表达式
     */
    @AfterThrowing(value = "com.github.fairy.era.aop.aspect.GlobalPointCut.pointcut()", throwing = "targetMethodException")
    public void printLogAfterThrowing(JoinPoint joinPoint, Throwable targetMethodException) {
        System.out.println("[AOP异常通知] 方法:" + joinPoint.getSignature().getName() + "抛出了异常,异常类型是:" + targetMethodException.getClass().getName());
    }

    /**
     * @After:声明当前方法是最终通知方法,value属性用来指定切入点表达式
     */
    @After("com.github.fairy.era.aop.aspect.GlobalPointCut.pointcut()")
    public void printLogAfter() {
        System.out.println("[AOP最终通知] 方法开始了");
    }

}

第六章:设置切面的优先级(⭐)

6.1 概述

  • 在同一连接点上可能应用不止一个切面,除非明确指定,否则它们的优先级是不确定的。
  • 切面的优先级可以通过实现 Ordered 接口或通过 @Order注解 指定。
  • 实现 Ordered 接口,getOrder() 方法的返回值越小,优先级越高。
  • 使用 @Order 注解,通过注解的 value 属性指定优先级,值越小优先级越高。
  • 切面的优先级控制切面的内外嵌套顺序,优先级高的切面在外面,优先级低的切面在里面

切面优先级的概述.png

6.2 定义多个切面类,并指定优先级

  • LogAspect.java
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Order(value = 1) // 设置切面的优先级
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {
    @Pointcut("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void pointcut() {

    }

    /**
     * @Before注解: 声明当前方法是前置通知方法,value属性用来指定切入点表达式
     * 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
     */
    @Before("pointcut()")
    public void printLogBefore(JoinPoint joinPoint) {
        // 通过JoinPoint对象获取目标方法签名对象
        Signature signature = joinPoint.getSignature();
        // 通过方法的签名对象获取目标方法的详细信息
        String name = signature.getName();
        String modifier = Modifier.toString(signature.getModifiers());
        String declaringTypeName = signature.getDeclaringTypeName();
        // 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("LogAspect [AOP前置通知] 方法名:" + name + ",返回修饰符:" + modifier + ",声明类型的完全限定名称:" + declaringTypeName + ",方法参数:" + Arrays.asList(args));
    }

    /**
     * @AfterReturn注解: 声明当前方法是返回通知方法,value属性用来指定切入点表达式
     */
    @AfterReturning(value = "pointcut()", returning = "targetMethodReturnValue")
    public void printLogAfterReturn(Object targetMethodReturnValue) {
        System.out.println("LogAspect [AOP返回通知] 方法的返回值是:" + targetMethodReturnValue);
    }

    /**
     * @AfterThrowing: 声明当前方法是异常通知方法,value属性用来指定切入点表达式
     */
    @AfterThrowing(value = "pointcut()", throwing = "targetMethodException")
    public void printLogAfterThrowing(JoinPoint joinPoint, Throwable targetMethodException) {
        System.out.println("LogAspect [AOP异常通知] 方法:" + joinPoint.getSignature().getName() + "抛出了异常,异常类型是:" + targetMethodException.getClass().getName());
    }

    /**
     * @After:声明当前方法是最终通知方法,value属性用来指定切入点表达式
     */
    @After("pointcut()")
    public void printLogAfter() {
        System.out.println("LogAspect [AOP最终通知] 方法开始了");
    }

}
  • LogAspect2.java
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Order(value = 2) // 设置切面的优先级
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect2 {
    @Pointcut("execution(public int com.github.fairy.era.aop.Calculator.add(int,int))")
    public void pointcut() {

    }

    /**
     * @Before注解: 声明当前方法是前置通知方法,value属性用来指定切入点表达式
     * 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
     */
    @Before("pointcut()")
    public void printLogBefore(JoinPoint joinPoint) {
        // 通过JoinPoint对象获取目标方法签名对象
        Signature signature = joinPoint.getSignature();
        // 通过方法的签名对象获取目标方法的详细信息
        String name = signature.getName();
        String modifier = Modifier.toString(signature.getModifiers());
        String declaringTypeName = signature.getDeclaringTypeName();
        // 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("LogAspect2 [AOP前置通知] 方法名:" + name + ",返回修饰符:" + modifier + ",声明类型的完全限定名称:" + declaringTypeName + ",方法参数:" + Arrays.asList(args));
    }

    /**
     * @AfterReturn注解: 声明当前方法是返回通知方法,value属性用来指定切入点表达式
     */
    @AfterReturning(value = "pointcut()", returning = "targetMethodReturnValue")
    public void printLogAfterReturn(Object targetMethodReturnValue) {
        System.out.println("LogAspect2 [AOP返回通知] 方法的返回值是:" + targetMethodReturnValue);
    }

    /**
     * @AfterThrowing: 声明当前方法是异常通知方法,value属性用来指定切入点表达式
     */
    @AfterThrowing(value = "pointcut()", throwing = "targetMethodException")
    public void printLogAfterThrowing(JoinPoint joinPoint, Throwable targetMethodException) {
        System.out.println("LogAspect2 [AOP异常通知] 方法:" + joinPoint.getSignature().getName() + "抛出了异常,异常类型是:" + targetMethodException.getClass().getName());
    }

    /**
     * @After:声明当前方法是最终通知方法,value属性用来指定切入点表达式
     */
    @After("pointcut()")
    public void printLogAfter() {
        System.out.println("LogAspect2 [AOP最终通知] 方法开始了");
    }

}

6.3 测试

  • 测试:
package com.github.fairy.era.bean;

import com.github.fairy.era.aop.Calculator;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class SpringTest {

    @Autowired
    private Calculator calculator;

    @Test
    public void test() {
        int add = calculator.add(1, 2);
        System.out.println("add = " + add);
    }
}
  • 结果:
LogAspect [AOP前置通知] 方法名:add,返回修饰符:public abstract,声明类型的完全限定名称:com.github.fairy.era.aop.Calculator,方法参数:[1, 2]
LogAspect2 [AOP前置通知] 方法名:add,返回修饰符:public abstract,声明类型的完全限定名称:com.github.fairy.era.aop.Calculator,方法参数:[1, 2]
LogAspect2 [AOP返回通知] 方法的返回值是:3
LogAspect2 [AOP最终通知] 方法开始了
LogAspect [AOP返回通知] 方法的返回值是:3
LogAspect [AOP最终通知] 方法开始了
add = 3

第七章:切入点表达式(⭐)

7.1 概述

  • 通过切入点表达式可以定位一个或多个具体的连接点。

7.2 语法格式

  • 语法:
execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))
  • 简化:

    • * 代替 权限修饰符返回值类型 部分表示 权限修饰符返回值类型 不限。
    • 在包名的部分,一个 * 只能代表包层次结构中的一层,表示这一层是任意的。如:*.Hello匹配com.Hello ,但是不匹配 com.github.Hello
    • 在类名的部分,使用 *.. 表示包名任意,包的层次深度任意。
    • 在类名部分,可以使用使用 * 号代替,表示类名任意。
    • 在类名部分,可以使用 * 号代替类名的一部分,如:*Service
    • 在方法名部分,可以使用 * 号代替,表示方法名任意。
    • 在方法名部分,可以使用 * 号代替方法名的一部分,如:*Emp
    • 在方法参数列表部分,使用 (..) 表示参数列表任意。
    • 在方法参数列表部分,使用 (int,..) 表示参数列表以 int 类型的参数开头。
    • 在方法参数部分,基本数据类型和对应的包装类型是不一样的,如:切入点表达式中使用 int 和实际方法中 Integer 是不匹配的。
    • 在方法返回值类型部分,如果想要明确的指定一个返回值类型,那么必须同时明确写明权限修饰符。

      // 正确
      execution(public int *..*Service.*(.., int))
      
      // 错误
      execution(* int *..*Service.*(.., int))
      
    • 在方法返回值类型部分,public * 表示权限修饰符明确,返回值任意。

  • 切入点表达式还可以使用逻辑运算符(了解):

    • execution() || execution():表示满足两个 execution() 中的任何一个即可。
    • execution() && execution():表示两个 execution() 表达式必须都满足。
    • !execution():表示不满足表达式的其他方法。
  • 示例:

package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {
    @Pointcut("execution(* com.github.fairy.era.aop.Calculator.add(..))")
    public void pointcut() {

    }

    @Before("pointcut()")
    public void printLogBefore(JoinPoint joinPoint) {
        // 通过JoinPoint对象获取目标方法签名对象
        Signature signature = joinPoint.getSignature();
        // 通过方法的签名对象获取目标方法的详细信息
        String name = signature.getName();
        String modifier = Modifier.toString(signature.getModifiers());
        String declaringTypeName = signature.getDeclaringTypeName();
        // 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("LogAspect [AOP前置通知] 方法名:" + name + ",返回修饰符:" + modifier + ",声明类型的完全限定名称:" + declaringTypeName + ",方法参数:" + Arrays.asList(args));
    }

}
  • 示例:
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {
    @Pointcut("execution(* *..Calculator.add(..))")
    public void pointcut() {

    }

    @Before("pointcut()")
    public void printLogBefore(JoinPoint joinPoint) {
        // 通过JoinPoint对象获取目标方法签名对象
        Signature signature = joinPoint.getSignature();
        // 通过方法的签名对象获取目标方法的详细信息
        String name = signature.getName();
        String modifier = Modifier.toString(signature.getModifiers());
        String declaringTypeName = signature.getDeclaringTypeName();
        // 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("LogAspect [AOP前置通知] 方法名:" + name + ",返回修饰符:" + modifier + ",声明类型的完全限定名称:" + declaringTypeName + ",方法参数:" + Arrays.asList(args));
    }
}
  • 示例:
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {
    @Pointcut("execution(* *..Calculator.*(..))")
    public void pointcut() {

    }

    @Before("pointcut()")
    public void printLogBefore(JoinPoint joinPoint) {
        // 通过JoinPoint对象获取目标方法签名对象
        Signature signature = joinPoint.getSignature();
        // 通过方法的签名对象获取目标方法的详细信息
        String name = signature.getName();
        String modifier = Modifier.toString(signature.getModifiers());
        String declaringTypeName = signature.getDeclaringTypeName();
        // 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("LogAspect [AOP前置通知] 方法名:" + name + ",返回修饰符:" + modifier + ",声明类型的完全限定名称:" + declaringTypeName + ",方法参数:" + Arrays.asList(args));
    }

}

7.3 总结

切入点表达式总结.png

  • 虽然上面介绍过的切入点表达式语法细节很多,有很多变化,但是实际上具体在项目中应用时有比较固定的写法。
  • 典型场景:
    • 在基于 XML 的声明式事务配置中需要指定切入点表达式,这个切入点表达式通常都会织入到所有 Service 类(接口)的所有方法。
    • 那么切入点表达式将如右所示:execution(* *..*Service.*(..))

第八章:环绕通知(⭐)

  • 环绕通知对应整个 try...catch...finally 结构,包括前面四种通知的所有功能。

  • 示例:

package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {
    @Pointcut("execution(* *..Calculator.*(..))")
    public void pointcut() {

    }


    /**
     * 使用@Around注解标明环绕通知方法
     *
     * @param joinPoint 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,Spring会将这个类型的对象传给我们
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        // 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
        Object[] args = joinPoint.getArgs();
        // 通过ProceedingJoinPoint对象获取目标方法的签名对象,通过签名对象获取目标方法的方法名
        String methodName = joinPoint.getSignature().getName();
        // 声明变量用来存储目标方法的返回值
        Object targetReturnValue = null;

        try {
            // 模拟事务
            System.out.println("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));

            // 通过ProceedingJoinPoint对象调用目标方法,
            targetReturnValue = joinPoint.proceed(args);

            // 在目标方法成功返回后:提交事务(模拟)
            System.out.println("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetReturnValue);
        } catch (Throwable e) {
            // 在目标方法抛异常后:回滚事务(模拟)
            System.out.println("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());
        } finally {
            // 在目标方法最终结束后:释放数据库连接
            System.out.println("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);
        }

        return targetReturnValue;
    }
}
package com.github.fairy.era.bean;

import com.github.fairy.era.aop.Calculator;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class SpringTest {

    @Autowired
    private Calculator calculator;

    @Test
    public void test() {
        int add = calculator.add(1, 2);
        System.out.println("add = " + add);
    }
}

第九章:没有接口的情况

9.1 概述

  • 在目标类没有实现任何接口的情况下,Spring 会自动使用 cglib 技术实现代理。

9.2 创建目标类

  • 确保这个类在自动扫描的包下,同时确保切面的切入点表达式能够覆盖到类中的方法。

  • EmpService.java

package com.github.fairy.era.aop.service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 14:25
 */
public class EmpService {

    public void getEmpList() {
        System.out.println("方法内部 com.github.fairy.era.aop.service.EmpService.getEmpList");
    }
}

9.2 切面类

  • LogAspect.java
package com.github.fairy.era.aop.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-08 08:54
 */
@Aspect // 标注这个类是一个切面类
@Component // 加入到Spring的IOC容器中
public class LogAspect {

    @Pointcut("execution(* *..EmpService.*(..))")
    public void pointcut() {

    }


    /**
     * 使用@Around注解标明环绕通知方法
     *
     * @param joinPoint 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,Spring会将这个类型的对象传给我们
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        // 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
        Object[] args = joinPoint.getArgs();
        // 通过ProceedingJoinPoint对象获取目标方法的签名对象,通过签名对象获取目标方法的方法名
        String methodName = joinPoint.getSignature().getName();
        // 声明变量用来存储目标方法的返回值
        Object targetReturnValue = null;

        try {
            // 模拟事务
            System.out.println("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));

            // 通过ProceedingJoinPoint对象调用目标方法,
            targetReturnValue = joinPoint.proceed(args);

            // 在目标方法成功返回后:提交事务(模拟)
            System.out.println("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetReturnValue);
        } catch (Throwable e) {
            // 在目标方法抛异常后:回滚事务(模拟)
            System.out.println("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());
        } finally {
            // 在目标方法最终结束后:释放数据库连接
            System.out.println("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);
        }

        return targetReturnValue;
    }
}

9.3 测试

package com.github.fairy.era.bean;

import com.github.fairy.era.aop.service.EmpService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class SpringTest {

    @Autowired
    private EmpService empService;

    @Test
    public void test() {
        empService.getEmpList();
    }
}

9.4 Debug 查看

没有接口的情况Debug查看.png

第十章:AOP 总结(⭐)

AOP总结.png