文章目录

  • 一、AOP概念
  • 1.1 什么是 AOP?
  • 1.2 AOP编程的专业术语
  • 1.3 AOP(底层原理)
  • 二、AOP示例
  • 2.1 一个简单计算数功能加日记
  • 2.2 使用jdk动态代理统一日记
  • 2.3 使用Spring实现AOP简单切面编程
  • 2.4 Spring切面中的代理对象
  • 2.5 Spring的切入点表达式
  • 2.6 获取连接点信息
  • 2.7 获取拦截方法的返回值和抛的异常信息
  • 2.8 Spring的环绕通知
  • 2.9 切入点表达式的复用
  • 2.10 多个通知的执行顺序


一、AOP概念

1.1 什么是 AOP?

面向切面编程(方面),利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

通俗描述:不通过修改源代码方式,在主干功能里面添加新功能

使用登录例子说明 AOP

spring boot aop切面没生效_System

1.2 AOP编程的专业术语

通知(Advice):通知就是增强的代码。比如前置增强的代码。后置增强的代码。异常增强代码。这些就叫通知

切面(Aspect):切面就是包含有通知代码的类叫切面。

横切关注点:横切关注点,就是我们可以添加增强代码的位置。比如前置位置,后置位置,异常位置。和返回值位置。这些都叫横切关注点。

目标(Target):目标对象就是被关注的对象。或者被代理的对象。

代理(Proxy):为了拦截目标对象方法,而被创建出来的那个对象,就叫做代理对象。

连接点(Joinpoint):连接点指的是横切关注点和程序代码的连接,叫连接点。

切入点(pointcut):切入点指的是用户真正处理的连接点,叫切入点。在Spring中切入点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。

图解AOP专业术语

spring boot aop切面没生效_方法名_02

1.3 AOP(底层原理)

AOP 底层使用动态代理,有两种情况动态代理

第一种 有接口情况,使用 JDK 动态代理

创建接口实现类代理对象,增强类的方法

spring boot aop切面没生效_方法名_03


第二种 没有接口情况,使用 CGLIB 动态代理创建子类的代理对象,增强类的方法

spring boot aop切面没生效_AOP_04

二、AOP示例

2.1 一个简单计算数功能加日记

日记工具类:

public class LogUtils {

	public static void logBefore(String method, Object... args) {
		System.out.println("说明有变 。方法名:" + method + ". 参数是:" + Arrays.asList(args));
	}

	public static void logAfterReturning(String method, Object result) {
		System.out.println("方法名:" + method + ". 返回值是:" + result);
	}

}

计算器类:

public class Calculator implements Calculate {

	@Override
	public int add(Integer num1, Integer num2) {
		LogUtils.logBefore("add", num1,num2);
		int result = num1 + num2;
		LogUtils.logAfterReturning("add", result);
		return result;
	}

	@Override
	public int add(Integer num1, Integer num2, Integer num3) {
		LogUtils.logBefore("add", num1,num2,num3);
		int result = num1 + num2 + num3;
		LogUtils.logAfterReturning("add", result);
		return result;
	}

	@Override
	public int div(Integer num1, Integer num2) {
		LogUtils.logBefore("div", num1,num2);
		int result = num1 / num2;
		LogUtils.logAfterReturning("div", result);
		return result;
	}

}

2.2 使用jdk动态代理统一日记

计算器:

public class Calculator implements Calculate {

	@Override
	public int add(Integer num1, Integer num2) {
		int result = num1 + num2;
		return result;
	}

	@Override
	public int add(Integer num1, Integer num2, Integer num3) {
		int result = num1 + num2 + num3;
		return result;
	}

	@Override
	public int div(Integer num1, Integer num2) {
		int result = num1 / num2;
		return result;
	}

}

日记工具类:

public class LogUtils {
	// 前置通知===前置增强
	public static void logBefore(String method, Object... args) {
		System.out.println("前置通知 : 方法名:" + method + ". 参数是:" + Arrays.asList(args));
	}

	// 后置通知
	public static void logAfter(String method, Object... args) {
		System.out.println("后置通知: 方法名:" + method + ". 参数是:" + Arrays.asList(args));
	}

	// 返回通知===返回增强
	public static void logAfterReturning(String method, Object result) {
		System.out.println("返回通知: 方法名:" + method + ". 返回值是:" + result);
	}

	// 异常通知===异常增强
	public static void logAfterThrowing(String method, Exception e) {
		System.out.println("异常通知: 方法名:" + method + ". 异常信息是:" + e);
	}

}
public class JdkProxyFactory {

    // 通过创建一个代理对象方式解决所有类,需要添加这些日记 的功能
    public static Object createProxy(Object target) {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
                /**
                 * 第一个参数是代理目标对象的类加载器<br/>
                 * 第二个参数是代理对象需要实现哪些接口 <br/>
                 * 第三个参数是InvocationHandler实现类
                 */
                (proxy, method, args) -> {
                    // 前置通知 ===>>>目标方法 ====>>>后置通知====>>>返回通知
                    // 前置通知 ===>>>目标方法 ====>>>后置通知====>>>异常通知
                    Object result = null; // 记录目标方法返回值
                    try {
                        try {
                            // 前置通知
                            LogUtils.logBefore(method.getName(), args);
                            // 调用目标对象方法
                            result = method.invoke(target, args);
                        } finally {
                            // 后置通知
                            LogUtils.logAfter(method.getName(), args);
                        }
                        // 返回通知
                        LogUtils.logAfterReturning(method.getName(), result);
                    } catch (Exception e) {
                        LogUtils.logAfterThrowing(method.getName(), e);
                    }
                    return result;
                });
    }

    public static void main(String[] args) {

        Calculate calculate = new Calculator();

        Calculate proxy = (Calculate) createProxy(calculate);

        System.out.println(proxy.add(100, 200));
        System.out.println("===============================");
        System.out.println(proxy.div(100, 0));

    }
}

spring boot aop切面没生效_spring_05

优点:这种方式已经解决我们前面所有日记需要的问题。非常的灵活。而且可以方便的在后期进行维护和升级。

缺点:当然使用jdk动态代理,需要有接口。如果没有接口。就无法使用jdk动态代理。

2.3 使用Spring实现AOP简单切面编程

springboot项目开启切面注解生效:@EnableAspectJAutoProxy或配置

<!-- 加代理信息 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

bean对象:

@Component
public class Calculator implements Calculate {
    @Override
    public int add(Integer num1, Integer num2) {
        int result = num1 + num2;
        System.out.println("目标方法 add被执行了");
        return result;
    }

    @Override
    public int add(Integer num1, Integer num2, Integer num3) {
        int result = num1 + num2 + num3;
        System.out.println("目标方法 add被执行了");
        return result;
    }

    @Override
    public int div(Integer num1, Integer num2) {
        System.out.println("目标方法 div被执行了");
        int result = num1 / num2;
        return result;
    }

}

切面类:

@Aspect
@Component
public class LogUtils {
    /**
     * @Before是前置通知
     */
    @Before(value="execution(public int com.hc.boot.aop.spring.Calculator.add(Integer, Integer))")
    // 前置通知===前置增强
    public static void logBefore() {
        System.out.println("前置通知 : 方法名:xxxx. 参数是:args");
    }

}

测试代码:

@Test
public void AOP1() throws Exception {
    // 创建Spring IOC 容器对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");

    Calculate calculate = (Calculate) applicationContext.getBean("calculator");

    calculate.add(100, 100);
}

spring boot aop切面没生效_AOP_06

2.4 Spring切面中的代理对象

springboot-aop在2.0版本下,默认的情况下,都是用的CGLIB代理,即非接口类代理

在Spring中,可以对有接口的对象和无接口的对象分别进行代理。在使用上有些细微的差别。

  1. 如果被代理的对象实现了接口。在获取对象的时候,必须要以接口来接收返回的对象。
  2. 如果被代理对象,如果没有实现接口。获取对象的时候使用对象类型本身

2.5 Spring的切入点表达式

@PointCut 切入点表达式语法格式是:

execution(访问权限 返回值类型 方法全限定名(参数类型列表))

例:execution(public int com.hc.pojo.Calculator.add(Integer, Integer))

限定符:

*表示任意的意思:

  1. 匹配某全类名下,任意或多个方法。
//表示匹配任意的方法
execution(public int com.hc.pojo.Calculator.*(Integer, Integer))
  1. 在Spring中只有public权限能拦截到,访问权限可以省略(访问权限不能写*)
//可以省略public访问权限
execution(int com.hc.pojo.Calculator.*(Integer, Integer))
  1. 匹配任意类型的返回值,可以使用 * 表示
//表示可以接受任意返回值类型
execution(public * com.hc.pojo.Calculator.*(Integer, Integer))
  1. 匹配任意类型的返回值,可以使用 * 表示
//匹配任意子包
execution(public int com.hc.*.Calculator.add(Integer, Integer))
  1. 任意类型参数
//表示第二个参数是任意类型的参数
execution(public int com.hc.pojo.Calculator.add(Integer, *))

..:可以匹配多层路径,或任意多个任意类型参数

  1. 任意层级的包
//表示com和pojo之间可以有任意层级的包。
execution(public int com..pojo.Calculator.add(Integer, Integer))
  1. 任意类型的参数
//不关心参数的个数,和参数的类型。
execution(public int com.hc.pojo.Calculator.add(..))

模糊匹配:

// 表示任意返回值,任意方法全限定符,任意参数
execution(* *(..))
// 表示任意返回值,任意包名+任意方法名,任意参数
execution(* *.*(..))

精确匹配:

execution(public int com.hc.aop.Calculator.add(int, int))

切入点表达式连接:&& 、||

// 表示需要同时满足两个表达式
@Before("execution(public int com.hc.aop.Calculator.add(int, int))" + " && "
+ "execution(public * com.hc.aop.Calculator.add(..))")

// 表示两个条件只需要满足一个,就会被匹配到
@Before("execution(public int com.hc.aop.Calculator.add(int, int))" + " || "
+ "execution(public * com.hc.aop.Calculator.a*(int))")

2.6 获取连接点信息

JoinPoint 是连接点的信息。只需要在通知方法的参数中,加入一个JoinPoint参数。就可以获取到拦截方法的信息。

注意:是org.aspectj.lang.JoinPoint这个类。

@Before(value="execution(public int com.hc.boot.aop.spring.Calculator.add(Integer, Integer))")
public static void logBefore(JoinPoint jp) {
    // jp.getSignature().getName() 获取方法名
    // jp.getArgs() 获取目标方法传递的参数
    System.out.println("前置通知 : 方法名:" + jp.getSignature().getName() + ". 参数是:" + Arrays.asList(jp.getArgs()));
}

2.7 获取拦截方法的返回值和抛的异常信息

获取方法返回的值分为两个步骤:

  1. 在返回值通知的方法中,追加一个参数 Object result
  2. 然后在@AfterReturning注解中添加参数returning=“参数名”
/**
 * 返回值通知<br/>
 * 	returning属性设置用哪个参数来接收返回值
 */
@AfterReturning(value = "execution(public int com.hc.pojo.Calculator.*(Integer, Integer))",returning="result")
public static void logAfterReturning(JoinPoint jp, Object result) {
	System.out.println("返回通知: 方法名:" + jp.getSignature().getName() + ". 返回值是:" + result);
}

获取方法抛出的异常分为两个步骤:

  1. 在异常通知的方法中,追加一个参数Exception exception
  2. 然后在@AfterThrowing 注解中添加参数 throwing=“参数名”
/**
 * 异常通知<br/>
 * throwing="e" 表示使用参数Throwable e来接收抛出的异常
 */
@AfterThrowing(value = "execution(public int com.hc.pojo.Calculator.*(Integer, Integer))",throwing="e")
public static void logAfterThrowing(JoinPoint jp,Throwable e) {
	System.out.println("异常通知: 方法名:" + jp.getSignature().getName() + ". 异常信息是:" + e);
}

2.8 Spring的环绕通知

  1. 环绕通知使用@Around注解。
  2. 环绕通知如果和其他通知同时执行。环绕通知会优先于其他通知之前执行。
  3. 环绕通知一定要有返回值(环绕如果没有返回值。后面的其他通知就无法接收到目标方法执行的结果)。
  4. 在环绕通知中。如果拦截异常。一定要往外抛。否则其他的异常通知是无法捕获到异常的。
@Around(value = "execution(public int com.hc.boot.aop.spring.Calculator.*(Integer, Integer))")
public static Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object result = null;
    try {
        try {
            System.out.println("环绕的前置通知");
            // 调用目标方法
            result = pjp.proceed();
        } finally {
            System.out.println("环绕的后置通知");
        }
        System.out.println("环绕的返回通知:" + result);
    } catch (Throwable e) {
        System.out.println("环绕的异常通知:" + e);
        throw e;// 普通的异常通知就会收到异常
    }
    return result;// 普通的返回通知,就会收到返回值
}

执行测试代码:

@Test
public void AOP1() throws Exception {
    // 创建Spring IOC 容器对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");

    Calculate calculate = (Calculate) applicationContext.getBean("calculator");

    calculate.add(100, 100);
}

spring boot aop切面没生效_System_07

2.9 切入点表达式的复用

切入点表达式的复用:

  1. 第一步:定义一个方法
  2. 第二步: 在方法上,使用@Pointcut定义一个切入点表达式
  3. 第三步:在需要复用切入点表达式的地方换成方法调用
@Pointcut(value="execution(public int com.hc.boot.aop.spring.Calculator.*(Integer, Integer))")
public static void pointcut1() {}

/**
 * @Before是前置通知
 */
@After(value = "pointcut1()")
// 前置通知===前置增强
public static void logAfter(JoinPoint jp) {
    System.out.println("前置通知 : 方法名:" + jp.getSignature().getName() + ". 参数是:" + Arrays.asList(jp.getArgs()));
}

2.10 多个通知的执行顺序

当我们有多个切面,多个通知的时候:

  1. 通知的执行顺序默认是由切面类的字母先后顺序决定。
  2. 在切面类上使用@Order注解决定通知执行的顺序(值越小,越先执行)