Java框架 Spring-AOP

1、AOP概念介绍

1.1 名词解释

AOP:Aspect Oriented Programming面向切面编程

1.2、概述

        AOP( Aspect Oriented Programming )是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。

1.3 AOP的作用

下面两点是同一件事的两面,一枚硬币的两面:

  • 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
  • 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。

2、相关术语

2.1 横切关注点

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。

2.2、通知

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

  • 前置通知:在被代理的目标方法执行
  • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝
  • 异常通知:在被代理的目标方法异常结束后执行(死于非命
  • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论
  • 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

2.3、切面

        封装通知方法的类。

2.4、目标

        被代理的目标对象。

2.5、代理

        向目标对象应用通知之后创建的代理对象。

2.6、连接点

        这也是一个纯逻辑概念,不是语法定义的。

        把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。

2.7、切入点

        定位连接点的方式。

        每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。

        如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

        Spring 的 AOP 技术可以通过切入点定位到特定的连接点。

        切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

3、AOP的核心套路

4、基于注解的AOP

4.1、技术说明

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

4.2、准备工作

① 添加依赖

<dependencies>
		<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>5.3.1</version>
		</dependency>
		<!-- spring-aspects会帮我们传递过来aspectjweaver -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aspects</artifactId>
			<version>5.3.1</version>
		</dependency>
		<!-- junit测试 -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

② 准备被代理的目标资源

接口:

public interface Calculator {
    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}

实现类:


import com.chenyixin.ssm.annotation.Calculator;
import org.springframework.stereotype.Component;

@Component
public class CalculatorPureImpl implements Calculator {
    @Override
    public int add(int i, int j) {

        int result = i + j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int sub(int i, int j) {

        int result = i - j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int mul(int i, int j) {

        int result = i * j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int div(int i, int j) {

        int result = i / j;

        System.out.println("方法内部 result = " + result);

        return result;
    }
}

4.3、创建切面类并配置

① 创建切面类


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

@Component
@Aspect // @Aspect表示这个类是一个切面类
public class LogAspect {
    // @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    @Before(value="execution(public int com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.add(..))")
    public void add() {
        System.out.println("[LogAspect --> 前置通知] 方法执行了");
    }

    @AfterReturning(value = "execution(public int com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.add(int,int))")
    public void printLogAfterSuccess() {
        System.out.println("[LogAspect --> 返回通知] 方法成功返回了");
    }

    @AfterThrowing(value = "execution(public int com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.add(int,int))")
    public void printLogAfterException() {
        System.out.println("[LogAspect --> 异常通知] 方法抛异常了");
    }

    @After(value = "execution(public int com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.add(int,int))")
    public void printLogFinallyEnd() {
        System.out.println("[LogAspect --> 后置通知] 方法最终结束了");
    }
}

 ② 在Spring的配置文件中配置 

	<!--
		基于注解的AOP的实现:
		1、将目标对象和切面交给IOC容器管理(注解+扫描)
		2、开启AspectJ的自动代理,为目标对象自动生成代理
		3、将切面类通过注解@Aspect标识
	-->
	<!-- 配置自动扫描的包 -->
	<context:component-scan base-package="com.chenyixin.ssm.annotation"/>
	<!-- 开启基于注解的AOP功能 -->
	<aop:aspectj-autoproxy/>

③ 测试

    @Test
    public void testAnnotationAOP() {
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        calculator.add(1, 2);
        // [LogAspect --> 前置通知] 方法执行了
        // 方法内部 result = 3
        // [LogAspect --> 返回通知] 方法成功返回了
        // [LogAspect --> 后置通知] 方法最终结束了
    }

4.4、各种通知

         前置通知 :使用 @Before 注解标识,在被代理的目标方法执行
         返回通知 :使用 @AfterReturning 注解标识,在被代理的目标方法成功结束后执行(寿终正寝
         异常通知 :使用 @AfterThrowing 注解标识,在被代理的目标方法异常结束后执行(死于非命
        后置通知 :使用 @After 注解标识,在被代理的目标方法最终结束后执行(盖棺定论
         环绕通知 :使用 @Around 注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包
括上面四种通知对应的所有位置
各种通知的执行顺序:
        Spring版本 5.3.x 以前:
                前置通知
                目标操作
                后置通知
                返回通知或异常通知
        Spring版本5.3.x以后:
                前置通知
                目标操作
                返回通知或异常通知
                后置通知

4.5、切入点表达式语法

① 切入点表达式的作用

 

② 语法细节

  • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
  • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
    • 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
  • 在包名的部分,使用“*..”表示包名任意、包的层次深度任意
  • 在类名的部分,类名部分整体用*号代替,表示类名任意
  • 在类名的部分,可以使用*号代替类名的一部分
*Service

上面例子表示匹配所有名称以Service结尾的类或接口

  • 在方法名部分,可以使用*号表示方法名任意
  • 在方法名部分,可以使用*号代替方法名的一部分
*Operation

上面例子表示匹配所有方法名以Operation结尾的方法

  • 在方法参数列表部分,使用(..)表示参数列表任意
  • 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
    • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
execution(public int *..*Service.*(.., int))

上面例子是对的,下面例子是错的:

execution(* int *..*Service.*(.., int))

但是public *表示权限修饰符明确,返回值任意是可以的。

  • 对于execution()表达式整体可以使用三个逻辑运算符号
    • execution() || execution()表示满足两个execution()中的任何一个即可
    • execution() && execution()表示两个execution()表达式必须都满足
    • !execution()表示不满足表达式的其他方法

③ 总结

 TIP

        虽然我们上面介绍过的切入点表达式语法细节很多,有很多变化,但是实际上具体在项目中应用时有比较固定的写法。

        典型场景:在基于 XML 的声明式事务配置中需要指定切入点表达式。这个切入点表达式通常都会套用到所有 Service 类(接口)的所有方法。那么切入点表达式将如下所示:

        execution(* *..*Service.*(..))

4.6、重用切入点表达式

① 声明

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

声明方式如下:

    @Pointcut(value = "execution(* com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.*(..))")
    public void pointCut() {}

② 同一个类内部引用

@Component
@Aspect // @Aspect表示这个类是一个切面类
public class LogAspect {

    @Pointcut(value = "execution(* com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.*(..))")
    public void pointCut() {}

    // @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    @Before(value="pointCut()")
    public void add() {
        System.out.println("[LogAspect --> 前置通知] 方法执行了");
    }

    @AfterReturning("pointCut()")
    public void printLogAfterSuccess() {
        System.out.println("[LogAspect --> 返回通知] 方法成功返回了");
    }

    @AfterThrowing("pointCut()")
    public void printLogAfterException() {
        System.out.println("[LogAspect --> 异常通知] 方法抛异常了");
    }

    @After("pointCut()")
    public void printLogFinallyEnd() {
        System.out.println("[LogAspect --> 后置通知] 方法最终结束了");
    }
}

③ 在不同类中引用

@Component
@Aspect
public class ValidateAspect {
    
    // 相当于调用 :全类名.方法名
    @Before(value="com.chenyixin.ssm.annotation.LogAspect.pointCut()")
    public void beforeMethod() {

    }
}

④ 集中管理

而作为存放切入点表达式的类,可以把整个项目中所有切入点表达式全部集中过来,便于统一管理

@Component
public class AtguiguPointCut {
    
    @Pointcut(value = "execution(public int *..Calculator.sub(int,int))")
    public void atguiguGlobalPointCut(){}
    
    @Pointcut(value = "execution(public int *..Calculator.add(int,int))")
    public void atguiguSecondPointCut(){}
    
    @Pointcut(value = "execution(* *..*Service.*(..))")
    public void transactionPointCut(){}
}

4.7、获取通知的相关信息

① JoinPoint接口

org.aspectj.lang.JoinPoint

  • 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
  • 要点2:通过目标方法签名对象获取方法名
  • 要点3:通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组
    // @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    // 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
    // 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
    @Before(value = "pointCut()")
    public void add(JoinPoint joinPoint) {
        // 1.通过JoinPoint对象获取目标方法签名对象
        // 方法的签名:一个方法的全部声明信息
        Signature signature = joinPoint.getSignature();

        // 2.通过方法的签名对象获取目标方法的详细信息
        // 获取方法名
        String name = signature.getName();

        int modifiers = signature.getModifiers();
        // System.out.println(modifiers);
        String declaringTypeName = signature.getDeclaringTypeName();
        // System.out.println(declaringTypeName);
        Class declaringType = signature.getDeclaringType();
        // System.out.println(declaringType);

        // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("[LogAspect --> 前置通知] 方法" + name + "执行了,参数为:" + Arrays.toString(args));
    }

② 方法返回值

在返回通知中,通过@AfterReturning注解的returning属性获取目标方法的返回值

    // @AfterReturning注解标记返回通知方法
    // 在返回通知中获取目标方法返回值分两步:
    // 第一步:在@AfterReturning注解中通过returning属性设置一个名称
    // 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
    @AfterReturning(value = "pointCut()", returning = "result")
    public void printLogAfterSuccess(JoinPoint joinPoint, Object result) {
        String name = joinPoint.getSignature().getName();
        System.out.println("[LogAspect --> 返回通知] 方法" + name + "成功返回了,返回值为:" + result);
    }

③ 目标方法抛出的异常

在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象

    // @AfterThrowing注解标记异常通知方法
    // 在异常通知中获取目标方法抛出的异常分两步:
    // 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
    // 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
    @AfterThrowing(value = "pointCut()", throwing = "exception")
    public void printLogAfterException(JoinPoint joinPoint, Exception exception) {
        String name = joinPoint.getSignature().getName();
        System.out.println("[LogAspect --> 异常通知] 方法" + name + "抛异常了,异常为:" + exception);
    }

④ 测试

    @Test
    public void testAnnotationAOP() {
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        System.out.println("测试1:");
        calculator.add(1, 2);
        System.out.println();
        System.out.println("测试2:");
        calculator.div(1, 0);
    }

结果:

 

4.8 环绕通知

① 代码

//使用@Around注解标明环绕通知方法
    @Around("pointCut()")
    public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint) {
        // 声明变量用来存储目标方法的返回值
        Object result = null;
        // 获取参数列表
        Object[] args = joinPoint.getArgs();
        try {
            // 在目标方法执行前执行
            System.out.println("环绕通知 --> 前置位置");

            // 过ProceedingJoinPoint对象调用目标方法
            // 目标方法的返回值一定要返回给外界调用者
            result = joinPoint.proceed(args);

            // 在目标方法成功返回后执行
            System.out.println("环绕通知 --> 返回位置");

        } catch (Throwable e) {
            // e.printStackTrace();
            // 在目标方法抛异常后执行
            System.out.println("环绕通知 --> 异常位置");

        }finally {
            // 在目标方法最终结束后执行
            System.out.println("环绕通知 --> 后置位置");

        }
        return result;
    }

② 测试

public class AOPTest {

    @Test
    public void testAnnotationAOP() {
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        System.out.println("测试1:");
        calculator.add(1, 2);
        System.out.println();
        System.out.println("测试2:");
        calculator.div(1, 0);
    }
}

③ 结果

 

4.9、切面的优先级

① 概念

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用 @Order 注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

② 实际意义

实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。

此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。  

③ 代码示例

@Component
@Aspect
@Order(1)
public class ValidateAspect {

    // 相当于调用 :全类名.方法名
    @Before(value = "com.chenyixin.ssm.annotation.LogAspect.pointCut()")
    public void beforeMethod(JoinPoint joinPoint) {
        String name = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("[LogAspect --> 前置通知] 方法" + name + "执行了,参数为:" + args);

    }
}
@Component
@Aspect // @Aspect表示这个类是一个切面类
@Order(2)
public class LogAspect {...}

测试:

    @Test
    public void testAnnotationAOP() {
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        calculator.add(1, 2);

    }

结果:

 

4.10、小结

1、在切面中,需要通过指定的注解将方法标识为通知方法
* @Before:前置通知,在目标对象方法执行之前执行
* @After:后置通知,在目标对象方法的finally字句中执行
* @AfterReturning:返回通知,在目标对象方法返回值之后执行
* @AfterThrowing:异常通知,在目标对象方法的catch字句中执行
*
*
* 2、切入点表达式:设置在标识通知的注解的value属性中
* execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int, int)
* execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..)
* 第一个*表示任意的访问修饰符和返回值类型
* 第二个*表示类中任意的方法
* ..表示任意的参数列表
* 类的地方也可以使用*,表示包下所有的类
* 3、重用切入点表达式
* //@Pointcut声明一个公共的切入点表达式
* @Pointcut("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))")
* public void pointCut(){}
* 使用方式:@Before("pointCut()")
*
* 4、获取连接点的信息
* 在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点所对应方法的信息
* //获取连接点所对应方法的签名信息
* Signature signature = joinPoint.getSignature();
* //获取连接点所对应方法的参数
* Object[] args = joinPoint.getArgs();
*
* 5、切面的优先级
* 可以通过@Order注解的value属性设置优先级,默认值Integer的最大值
* @Order注解的value属性值越小,优先级越高

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

5、基于XML的AOP

5.1、准备工作

① 添加依赖

参考基于注解的 AOP 环境

② 准备被代理的目标资源

接口:

public interface Calculator {
    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}

接口实现类:

@Component
public class CalculatorPureImpl implements Calculator {
    @Override
    public int add(int i, int j) {

        int result = i + j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int sub(int i, int j) {

        int result = i - j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int mul(int i, int j) {

        int result = i * j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int div(int i, int j) {

        int result = i / j;

        System.out.println("方法内部 result = " + result);

        return result;
    }
}

③ 准备切面类


@Component
public class LogAspect {

    public void add(JoinPoint joinPoint) {
        // 1.通过JoinPoint对象获取目标方法签名对象
        // 方法的签名:一个方法的全部声明信息
        Signature signature = joinPoint.getSignature();

        // 2.通过方法的签名对象获取目标方法的详细信息
        // 获取方法名
        String name = signature.getName();

        int modifiers = signature.getModifiers();
        // System.out.println(modifiers);
        String declaringTypeName = signature.getDeclaringTypeName();
        // System.out.println(declaringTypeName);
        Class declaringType = signature.getDeclaringType();
        // System.out.println(declaringType);

        // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        System.out.println("[LogAspect --> 前置通知] 方法" + name + "执行了,参数为:" + Arrays.toString(args));
    }

    public void printLogAfterSuccess(JoinPoint joinPoint, Object result) {
        String name = joinPoint.getSignature().getName();
        System.out.println("[LogAspect --> 返回通知] 方法" + name + "成功返回了,返回值为:" + result);
    }

    public void printLogAfterException(JoinPoint joinPoint, Exception exception) {
        String name = joinPoint.getSignature().getName();
        System.out.println("[LogAspect --> 异常通知] 方法" + name + "抛异常了,异常为:" + exception);
    }

    public void printLogFinallyEnd(JoinPoint joinPoint) {
        String name = joinPoint.getSignature().getName();
        System.out.println("[LogAspect --> 后置通知] 方法" + name + "最终结束了");
    }

    public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint) {
        // 声明变量用来存储目标方法的返回值
        Object result = null;
        // 获取参数列表
        Object[] args = joinPoint.getArgs();
        try {
            // 在目标方法执行前执行
            System.out.println("环绕通知 --> 前置位置");

            // 过ProceedingJoinPoint对象调用目标方法
            // 目标方法的返回值一定要返回给外界调用者
            result = joinPoint.proceed(args);

            // 在目标方法成功返回后执行
            System.out.println("环绕通知 --> 返回位置");

        } catch (Throwable e) {
            // e.printStackTrace();
            // 在目标方法抛异常后执行
            System.out.println("环绕通知 --> 异常位置");

        }finally {
            // 在目标方法最终结束后执行
            System.out.println("环绕通知 --> 后置位置");

        }
        return result;
    }
}
@Component
public class ValidateAspect {

    public void beforeMethod(JoinPoint joinPoint) {
        String name = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("[LogAspect --> 前置通知] 方法" + name + "执行了,参数为:" + args);

    }
}

5.2、配置Spring配置文件及测试

	<!-- 扫描组件 -->
	<context:component-scan base-package="com.chenyixin.ssm.xml"/>
	<!-- 配置AOP -->
	<aop:config>
		<!-- 配置切入点表达式 -->
		<aop:pointcut id="pointcut" expression="execution(* com.chenyixin.ssm.xml.imp.CalculatorPureImpl.*(..))"/>

		<!-- aop:aspect标签:配置切面(类) -->
		<!-- ref属性:关联切面类的bean -->
		<aop:aspect ref="logAspect" order="1">
			<!-- aop:before标签:配置前置通知 -->
			<!-- method属性:指定前置通知的方法名 -->
			<!-- pointcut-ref属性:引用切入点表达式 -->
			<aop:before method="add" pointcut-ref="pointcut"/>

			<!-- aop:after-returning标签:配置返回通知 -->
			<!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
			<aop:after-returning method="printLogAfterSuccess" pointcut-ref="pointcut"
								 returning="result"/>

			<!-- aop:after-throwing标签:配置异常通知 -->
			<!-- throwing属性:指定通知方法中用来接收目标方法抛出异常的异常对象的参数名 -->
			<aop:after-throwing method="printLogAfterException" pointcut-ref="pointcut"
								throwing="exception"/>

			<!-- aop:after标签:配置后置通知 -->
			<aop:after method="printLogFinallyEnd" pointcut-ref="pointcut"/>

			<!-- aop:around标签:配置环绕通知 -->
			<aop:around method="aroundAdviceMethod" pointcut-ref="pointcut"/>
		</aop:aspect>

		<aop:aspect ref="validateAspect" order="2">
			<aop:before method="beforeMethod" pointcut-ref="pointcut"/>
		</aop:aspect>

	</aop:config>

测试:

    @Test
    public void testXmlAOP() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-xml.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        calculator.add(10, 30);
    }

结果:

 

6、AOP 对获取 bean 的影响

6.1、根据类型获取 bean

① 情景一

  • bean 对应的类没有实现任何接口
  • 根据 bean 本身的类型获取 bean
    • 测试:IOC容器中同类型的 bean 只有一个

      正常获取到 IOC 容器中的那个 bean 对象

    • 测试:IOC 容器中同类型的 bean 有多个

      会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个

② 情景二

  • bean 对应的类实现了接口,这个接口也只有这一个实现类
    • 测试:根据接口类型获取 bean
    • 测试:根据类获取 bean
    • 结论:上面两种情况其实都能够正常获取到 bean,而且是同一个对象

③ 情景三

  • 声明一个接口
  • 接口有多个实现类
  • 接口所有实现类都放入 IOC 容器
    • 测试:根据接口类型获取 bean

      会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个

    • 测试:根据类获取bean

      正常

④ 情景四

  • 声明一个接口
  • 接口有一个实现类
  • 创建一个切面类,对上面接口的实现类应用通知
    • 测试:根据接口类型获取bean  正常
    • 测试:根据类获取bean  错误

原因分析:

  • 应用了切面后,真正放在IOC容器中的是代理类的对象
  • 目标类并没有被放到IOC容器中,所以根据目标类的类型从IOC容器中是找不到的

从内存分析的角度来说,IOC容器中引用的是代理对象,代理对象引用的是目标对象。IOC容器并没有直接引用目标对象,所以根据目标类本身在IOC容器范围内查找不到。

debug查看代理类的类型:

⑤ 情景五

  • 声明一个类
  • 创建一个切面类,对上面的类应用通知
    • 测试:根据类获取 bean,能获取到

debug查看实际类型:

6.2、自动装配

        自动装配需先从 IOC 容器中获取到唯一的一个 bean 才能够执行装配。所以装配能否成功和装配底层的原理,和前面测试的获取 bean 的机制是一致的。

① 情景一

  • 目标bean对应的类没有实现任何接口
  • 根据bean本身的类型装配这个bean
    • 测试:IOC容器中同类型的bean只有一个

      正常装配

    • 测试:IOC容器中同类型的bean有多个

      会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

② 情景二

  • 目标bean对应的类实现了接口,这个接口也只有这一个实现类
    • 测试:根据接口类型装配bean

      正常

    • 测试:根据类装配bean

      正常

③ 情景三

  • 声明一个接口
  • 接口有多个实现类
  • 接口所有实现类都放入IOC容器
    • 测试:根据接口类型装配bean

      @Autowired注解会先根据类型查找,此时会找到多个符合的bean,然后根据成员变量名作为bean的id进一步筛选,如果没有id匹配的,则会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

    • 测试:根据类装配bean

      正常

④ 情景四

  • 声明一个接口
  • 接口有一个实现类
  • 创建一个切面类,对上面接口的实现类应用通知
    • 测试:根据接口类型装配bean

      正常

    • 测试:根据类装配bean

      此时获取不到对应的bean,所以无法装配,抛出下面的异常:

Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'fruitApple' is expected to be of type 'com.atguigu.bean.impl.FruitAppleImpl' but was actually of type 'com.sun.proxy.$Proxy15'

⑤ 情景五

  • 声明一个类
  • 创建一个切面类,对上面的类应用通知
    • 测试:根据类装配bean

      正常

6.3、总结

① 对实现了接口的类应用切面

② 对没实现接口的类应用切面

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值