AOP-从代理模式学习SpringAop(适合初学者的Aop讲解)

6 篇文章 0 订阅

第一节 情景设定

1、声明接口

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);
    
}

2、给接口声明一个纯净版实现

没有额外功能
在这里插入图片描述

package com.utaha.proxy.imp;
    
import com.utaha.proxy.api.Calculator;
    
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;
    }
}

3、再声明一个带日志功能的实现

在这里插入图片描述

package com.utaha.proxy.imp;
import com.utaha.proxy.api.Calculator;
public class CalculatorLogImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] add 方法结束了,结果是:" + result);
    
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
    
        int result = i - j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] sub 方法结束了,结果是:" + result);
    
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
    
        int result = i * j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] mul 方法结束了,结果是:" + result);
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
    
        int result = i / j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] div 方法结束了,结果是:" + result);
    
        return result;
    }
}

4、提出问题

①现有代码缺陷

针对带日志功能的实现类,我们发现有如下缺陷:
对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
附加功能分散在各个业务功能方法中,不利于统一维护

②解决思路

解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

③困难

解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。

第二节 代理模式

1、概念

①介绍

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

使用代理后:
在这里插入图片描述
在这里插入图片描述

②生活中的代理

广告商找大明星拍广告需要经过经纪人
合作伙伴找大老板谈合作要约见面时间需要经过秘书
房产中介是买卖双方的代理
太监是大臣和皇上之间的代理

③相关术语

代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。

动词:指做代理这个动作,或这项工作
名词:扮演代理这个角色的类、对象、方法
目标:被代理“套用”了非核心逻辑代码的类、对象、方法。

理解代理模式、AOP的核心关键词就一个字:套

2、静态代理

创建静态代理类:

@Slf4j
public class CalculatorStaticProxy implements Calculator {
    
    // 将被代理的目标对象声明为成员变量
    private Calculator target;
    
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    
    @Override
    public int add(int i, int j) {
    
        // 附加功能由代理类中的代理方法来实现
        log.debug("[日志] add 方法开始了,参数是:" + i + "," + j);
    
        // 通过目标对象来实现核心业务逻辑
        int addResult = target.add(i, j);
    
        log.debug("[日志] add 方法结束了,结果是:" + addResult);
    
        return addResult;
    }
    ……

静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

3、动态代理

在这里插入图片描述

①生产代理对象的工厂类

JDK本身就支持动态代理,这是反射技术的一部分。下面我们还是创建一个代理类(生产代理对象的工厂类):

@Slf4j
// 泛型T要求是目标对象实现的接口类型,本代理类根据这个接口来进行代理
public class LogDynamicProxyFactory<T> {
    
    // 将被代理的目标对象声明为成员变量
    private T target;
    
    public LogDynamicProxyFactory(T target) {
        this.target = target;
    }
    
    public T getProxy() {
    
        // 创建代理对象所需参数一:加载目标对象的类的类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
    
        // 创建代理对象所需参数二:目标对象的类所实现的所有接口组成的数组
        Class<?>[] interfaces = target.getClass().getInterfaces();
    
        // 创建代理对象所需参数三:InvocationHandler对象
        // Lambda表达式口诀:
        // 1、复制小括号
        // 2、写死右箭头
        // 3、落地大括号
        InvocationHandler handler = (
                                    // 代理对象,当前方法用不上这个对象
                                    Object proxy,
    
                                     // method就是代表目标方法的Method对象
                                     Method method,
    
                                     // 外部调用目标方法时传入的实际参数
                                     Object[] args)->{
    
            // 我们对InvocationHandler接口中invoke()方法的实现就是在调用目标方法
            // 围绕目标方法的调用,就可以添加我们的附加功能
    
            // 声明一个局部变量,用来存储目标方法的返回值
            Object targetMethodReturnValue = null;
    
            // 通过method对象获取方法名
            String methodName = method.getName();
    
            // 为了便于在打印时看到数组中的数据,把参数数组转换为List
            List<Object> argumentList = Arrays.asList(args);
    
            try {
    
                // 在目标方法执行前:打印方法开始的日志
                log.debug("[动态代理][日志] " + methodName + " 方法开始了,参数是:" + argumentList);
    
                // 调用目标方法:需要传入两个参数
                // 参数1:调用目标方法的目标对象
                // 参数2:外部调用目标方法时传入的实际参数
                // 调用后会返回目标方法的返回值
                targetMethodReturnValue = method.invoke(target, args);
    
                // 在目标方法成功后:打印方法成功结束的日志【寿终正寝】
                log.debug("[动态代理][日志] " + methodName + " 方法成功结束了,返回值是:" + targetMethodReturnValue);
    
            }catch (Exception e){
    
                // 通过e对象获取异常类型的全类名
                String exceptionName = e.getClass().getName();
    
                // 通过e对象获取异常消息
                String message = e.getMessage();
    
                // 在目标方法失败后:打印方法抛出异常的日志【死于非命】
                log.debug("[动态代理][日志] " + methodName + " 方法抛异常了,异常信息是:" + exceptionName + "," + message);
    
            }finally {
    
                // 在目标方法最终结束后:打印方法最终结束的日志【盖棺定论】
                log.debug("[动态代理][日志] " + methodName + " 方法最终结束了");
    
            }
    
            // 这里必须将目标方法的返回值返回给外界,如果没有返回,外界将无法拿到目标方法的返回值
            return targetMethodReturnValue;
        };
    
        // 创建代理对象
        T proxy = (T) Proxy.newProxyInstance(classLoader, interfaces, handler);
    
        // 返回代理对象
        return proxy;
    }
}

②测试

@Test
public void testDynamicProxy() {
    
    // 1.创建被代理的目标对象
    Calculator target = new CalculatorPureImpl();
    
    // 2.创建能够生产代理对象的工厂对象
    LogDynamicProxyFactory<Calculator> factory = new LogDynamicProxyFactory<>(target);
    
    // 3.通过工厂对象生产目标对象的代理对象
    Calculator proxy = factory.getProxy();
    
    // 4.通过代理对象间接调用目标对象
    int addResult = proxy.add(10, 2);
    log.debug("方法外部 addResult = " + addResult + "\n");
    
    int subResult = proxy.sub(10, 2);
    log.debug("方法外部 subResult = " + subResult + "\n");
    
    int mulResult = proxy.mul(10, 2);
    log.debug("方法外部 mulResult = " + mulResult + "\n");
    
    int divResult = proxy.div(10, 2);
    log.debug("方法外部 divResult = " + divResult + "\n");
}

③练习

动态代理的实现过程不重要,重要的是使用现成的动态代理类去套用到其他目标对象上。
声明另外一个接口:

public interface SoldierService {
    
    int saveSoldier(String soldierName);
    
    int removeSoldier(Integer soldierId);
    
    int updateSoldier(Integer soldierId, String soldierName);
    
    String getSoldierNameById(Integer soldierId);
    
}

给接口一个实现类:

public class SoldierServiceImpl implements SoldierService {
    
    @Override
    public int saveSoldier(String soldierName) {
    
        log.debug("核心业务逻辑:保存到数据库……");
    
        return 1;
    }
    
    @Override
    public int removeSoldier(Integer soldierId) {
    
        log.debug("核心业务逻辑:从数据库删除……");
    
        return 1;
    }
    
    @Override
    public int updateSoldier(Integer soldierId, String soldierName) {
    
        log.debug("核心业务逻辑:更新……");
    
        return 1;
    }
    @Override
    public String getSoldierNameById(Integer soldierId) {
    
        log.debug("核心业务逻辑:查询数据库……");
    
        return "good";
    }
}

④测试

@Test
public void testSoldierServiceDynamicProxy() {
    
    // 1.创建被代理的目标对象
    SoldierService soldierService = new SoldierServiceImpl();
    
    // 2.创建生产代理对象的工厂对象
    LogDynamicProxyFactory<SoldierService> factory = new LogDynamicProxyFactory<>(soldierService);
    
    // 3.生产代理对象
    SoldierService proxy = factory.getProxy();
    
    // 4.通过代理对象调用目标方法
    String soldierName = proxy.getSoldierNameById(1);
    log.debug("soldierName = " + soldierName);
    
}

第三节 AOP的核心套路

在这里插入图片描述

第四节 AOP术语

1、AOP概念介绍

①名词解释

AOP:Aspect Oriented Programming面向切面编程
切面(ASPECT):横切关注点被模块化的特殊对象。即,它是一个类。
通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。
目标(Target):被通知对象。
代理(Proxy):向目标对象应用通知之后创建的对象。
切入点(PointCut):切面通知执行的“地点"的定义。
连接点(JointPoint) :与切入点匹配的执行点。

补充:连接点和切点的区别
定义:
1.连接点(Join point):连接点是在应用执行过程中能够插入切面(Aspect)的一个点。这些点可以是调用方法时、甚至修改一个字段时。
2.切点(Pointcut):切点是指通知(Advice)所要织入(Weaving)的具体位置。
理解:
连接点:连接点是一个虚拟的概念,可以理解为所有满足切点扫描条件的所有的时机。
具体举个例子:比如开车经过一条高速公路,这条高速公路上有很多个出口(连接点),但是我们不会每个出口都会出去,只会选择我们需要的那个出口(切点)开出去。
简单可以理解为,每个出口都是连接点,但是我们使用的那个出口才是切点。每个应用有多个位置适合织入通知,这些位置都是连接点。但是只有我们选择的那个具体的位置才是切点。
再理解:
1.连接点(JoinPoint)
这个更好解释了,就是spring允许你使用通知的地方,那可真就多了,基本每个方法的前,后(两者都有也行),或抛出异常时都可以是连接点,spring只支持方法连接点.其他如aspectJ还可以让你在构造器或属性注入时都行,不过那不是咱关注的,只要记住,和方法有关的前前后后(抛出异常),都是连接点。
2.切入点(Pointcut)
上面说的连接点的基础上,来定义切入点,你的一个类里,有15个方法,那就有几十个连接点了对把,但是你并不想在所有方法附近都使用通知(使用叫织入,以后再说),你只想让其中的几个,在调用这几个方法之前,之后或者抛出异常时干点什么,那么就用切点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法。

②AOP的作用

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

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

2、横切关注点

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。

在这里插入图片描述
横切关注点是一个『逻辑层面』的概念,而不是『语法层面』的概念。

3、通知[记住]

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

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

4、切面[记住]

封装通知方法的类。根据不同的非核心业务逻辑,我们可以创建不同的切面类:

日志功能:日志切面
缓存功能:缓存切面
事务功能:事务切面

5、目标

被代理的目标对象。

6、代理

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

就动态代理技术而言,JDK会在运行过程中根据我们提供的接口动态生成接口的实现类。那么我们这里谈到的代理对象就是这个动态生成的类的对象。

7、连接点

和横切关注点一样,这又是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。
在这里插入图片描述

8、切入点[记住]

定位连接点的方式。
我们通过切入点,可以将通知方法精准的植入到被代理目标方法的指定位置。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
封装了代理逻辑的通知方法就像一颗制导导弹,在切入点这个引导系统的指引下精确命中连接点这个打击目标:

第五节 基于注解的AOP

1、基于注解的AOP用到的技术

在这里插入图片描述

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

2、实验操作

[实验一 初步实现]

1、加入依赖

在IOC所需依赖基础上再加入下面依赖即可:

<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.1</version>
</dependency>
2、准备被代理的目标资源
①接口

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);

}

②纯净的实现类

在 Spring 下工作,所有的一切都必须放在 IOC 容器中。现在接口的实现类是 AOP 要代理的目标类,所以它也必须放入 IOC 容器。

package com.utaha.aop.imp;
    
import com.utaha.aop.api.Calculator;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class CalculatorPureImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        int result = i + j;
    
        log.debug("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        int result = i - j;
    
        log.debug("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        int result = i * j;
    
        log.debug("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        int result = i / j;
    
        log.debug("方法内部 result = " + result);
    
        return result;
    }
}
3、创建切面类
// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
@Slf4j
public class LogAspect {
        
    // @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    @Before(value = "execution(public int com.utaha.aop.api.Calculator.add(int,int))")
    public void printLogBeforeCore() {
        log.debug("[AOP前置通知] 方法开始了");
    }
    
    @AfterReturning(value = "execution(public int com.utaha.aop.api.Calculator.add(int,int))")
    public void printLogAfterSuccess() {
        log.debug("[AOP返回通知] 方法成功返回了");
    }
    
    @AfterThrowing(value = "execution(public int com.utaha.aop.api.Calculator.add(int,int))")
    public void printLogAfterException() {
        log.debug("[AOP异常通知] 方法抛异常了");
    }
    
    @After(value = "execution(public int com.utaha.aop.api.Calculator.add(int,int))")
    public void printLogFinallyEnd() {
        log.debug("[AOP后置通知] 方法最终结束了");
    }
    
}
4、创建Spring的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
    <!-- 开启基于注解的AOP功能 -->
    <aop:aspectj-autoproxy/>
    
    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.utaha.aop"/>
    
</beans>
5、测试
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class AOPTest {
    
    @Autowired
    private Calculator calculator;
    
    @Test
    public void testAnnotationAOP() {
    
        int add = calculator.add(10, 2);
        log.debug("方法外部 add = " + add);
    
    }
    
}

打印效果如下:

[AOP前置通知] 方法开始了
方法内部 result = 12
[AOP返回通知] 方法成功返回了
[AOP后置通知] 方法最终结束了
方法外部 add = 12
6、通知执行顺序

Spring版本5.3.x以前:

前置通知
目标操作
后置通知
返回通知或异常通知

Spring版本5.3.x以后:

前置通知
目标操作
返回通知或异常通知
后置通知

[实验二 各个通知获取细节信息]

1、JoinPoint接口

org.aspectj.lang.JoinPoint

  • 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
  • 要点2:通过目标方法签名对象获取方法名
  • 要点3:通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组
// @Before注解标记前置通知方法
// value属性:切入点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
// 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
// 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
@Before(value = "execution(public int com.utaha.aop.api.Calculator.add(int,int))")
public void printLogBeforeCore(JoinPoint joinPoint) {
    
    // 1.通过JoinPoint对象获取目标方法签名对象
    // 方法的签名:一个方法的全部声明信息
    Signature signature = joinPoint.getSignature();
    
    // 2.通过方法的签名对象获取目标方法的详细信息
    String methodName = signature.getName();
    System.out.println("methodName = " + methodName);
    
    int modifiers = signature.getModifiers();
    System.out.println("modifiers = " + modifiers);
    
    String declaringTypeName = signature.getDeclaringTypeName();
    System.out.println("declaringTypeName = " + declaringTypeName);
    
    // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
    Object[] args = joinPoint.getArgs();
    
    // 4.由于数组直接打印看不到具体数据,所以转换为List集合
    List<Object> argList = Arrays.asList(args);
    
    System.out.println("[AOP前置通知] " + methodName + "方法开始了,参数列表:" + argList);
}

需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。

2、方法返回值

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

// @AfterReturning注解标记返回通知方法
// 在返回通知中获取目标方法返回值分两步:
// 第一步:在@AfterReturning注解中通过returning属性设置一个名称
// 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
@AfterReturning(
        value = "execution(public int com.utaha.aop.api.Calculator.add(int,int))",
        returning = "targetMethodReturnValue"
)
public void printLogAfterCoreSuccess(JoinPoint joinPoint, Object targetMethodReturnValue) {
    
    String methodName = joinPoint.getSignature().getName();
    
    System.out.println("[AOP返回通知] "+methodName+"方法成功结束了,返回值是:" + targetMethodReturnValue);
}
3、目标方法抛出的异常

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

// @AfterThrowing注解标记异常通知方法
// 在异常通知中获取目标方法抛出的异常分两步:
// 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
// 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
@AfterThrowing(
        value = "execution(public int com.utaha.aop.api.Calculator.add(int,int))",
        throwing = "targetMethodException"
)
public void printLogAfterCoreException(JoinPoint joinPoint, Throwable targetMethodException) {
    
    String methodName = joinPoint.getSignature().getName();
    
    System.out.println("[AOP异常通知] "+methodName+"方法抛异常了,异常类型是:" + targetMethodException.getClass().getName());
}

打印效果局部如下:

[AOP异常通知] div方法抛异常了,异常类型是:java.lang.ArithmeticException
java.lang.ArithmeticException: / by zero
at com.utaha.aop.imp.CalculatorPureImpl.div(CalculatorPureImpl.java:42) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)

[实验三 重用切入点表达式]

1、声明

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

// 切入点表达式重用
@Pointcut("execution(public int com.utaha.aop.api.Calculator.add(int,int)))")
public void declarPointCut() {}
2、同一个类内部引用
@Before(value = "declarPointCut()")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
3、在不同类中引用
@Around(value = "com.utaha.spring.aop.aspect.LogAspect.declarPointCut()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {
4、集中管理

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

@Component
public class UtahaPointCut {
    
    @Pointcut(value = "execution(public int *..Calculator.sub(int,int))")
    public void utahaGlobalPointCut(){}
    
    @Pointcut(value = "execution(public int *..Calculator.add(int,int))")
    public void utahaSecondPointCut(){}
    
    @Pointcut(value = "execution(* *..*Service.*(..))")
    public void transactionPointCut(){}
}
5.引入多个参数

我们先定义一个注解

@Target({ElementType.METHOD}) //声明注解作用在方法上面
@Retention(RetentionPolicy.RUNTIME) //注解保留至运行时 
public @interface AopInter {

}

改良后的advice

/**
 * 参数@Before 注解标记前置通知方法
 * 参数@annotation(com.utaha.aop.AopInter) 被@AopInter标记的方法才会生效
 * value属性:切入点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
 * 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
 * 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
 * @param joinPoint 连接点
 */
@Before(value = "execution(public int com.utaha.service.Calculator.add(int,int)) " +
        "&& @annotation(com.utaha.aop.AopInter)" + "&& @annotation(aopInter)")
public void printLogBeforeCore(JoinPoint joinPoint, AopInter aopInter) {
    ...
    log.debug("还说啥呢,躺一把呗***"+ aopInter.demo());

[实验四 切入点表达式语法]

1、切入点表达式的作用

在这里插入图片描述

2、语法细节
用*号代替“权限修饰符”和“返回值”这两个部分的整体,表示“权限修饰符”和“返回值”不限
在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。

例如:*.Hello匹配com.Hello,不匹配com.utaha.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()表示不满足表达式的其他方法
3、总结

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

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

[实验五 环绕通知]

环绕通知对应整个 try…catch…finally 结构,包括前面四种通知的所有功能。
// 使用@Around注解标明环绕通知方法

@Around(value = "com.utaha.aop.aspect.UtahaPointCut.transactionPointCut()")
public Object manageTransaction(
    
        // 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,
        // Spring会将这个类型的对象传给我们
        ProceedingJoinPoint joinPoint) {
    
    // 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
    Object[] args = joinPoint.getArgs();
    
    // 通过ProceedingJoinPoint对象获取目标方法的签名对象
    Signature signature = joinPoint.getSignature();
    
    // 通过签名对象获取目标方法的方法名
    String methodName = signature.getName();
    
    // 声明变量用来存储目标方法的返回值
    Object targetMethodReturnValue = null;
    
    try {
    
        // 在目标方法执行前:开启事务(模拟)
        log.debug("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));
    
        // 过ProceedingJoinPoint对象调用目标方法
        // 目标方法的返回值一定要返回给外界调用者
        targetMethodReturnValue = joinPoint.proceed(args);
    
        // 在目标方法成功返回后:提交事务(模拟)
        log.debug("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetMethodReturnValue);
    
    }catch (Throwable e){
    
        // 在目标方法抛异常后:回滚事务(模拟)
        log.debug("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());
    
    }finally {
    
        // 在目标方法最终结束后:释放数据库连接
        log.debug("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);
    
    }
    
    return targetMethodReturnValue;
}

[实验六 切面的优先级]

1、概念

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
优先级高的切面:外面
优先级低的切面:里面
使用 @Order 注解可以控制切面的优先级:
@Order(较小的数):优先级高
@Order(较大的数):优先级低

2、实际意义

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

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

在这里插入图片描述

[实验七 没有接口的情况]

在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。为了证明这一点,我们做下面的测试:

1、创建目标类

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

@Slf4j
@Service
public class EmployeeService {
    
    public void getEmpList() {
        log.debug("方法内部 com.utaha.aop.imp.EmployeeService.getEmpList");
    }
    
}
2、测试
@Autowired
private EmployeeService employeeService;

@Test
public void testNoInterfaceProxy() {
    employeeService.getEmpList();
    log.debug("...");
}
3、小结

常用@Pointcut()
需要注意:匹配的类型中支持或(||)与(&&)非(!)运算。

   /**
     * 1、使用within表达式匹配
     * 下面示例表示匹配com.leo.controller包下所有的类的方法
     */
    @Pointcut("within(com.leo.controller..*)")
    public void pointcutWithin(){

    }

    /**
     * 2、this匹配目标指定的方法,此处就是HelloController的方法
     */
    @Pointcut("this(com.leo.controller.HelloController)")
    public void pointcutThis(){

    }

    /**
     * 3、target匹配实现UserInfoService接口的目标对象
     */
    @Pointcut("target(com.leo.service.UserInfoService)")
    public void pointcutTarge(){

    }

    /**
     * 4、bean匹配所有以Service结尾的bean里面的方法,
     * 注意:使用自动注入的时候默认实现类首字母小写为bean的id
     */
    @Pointcut("bean(*ServiceImpl)")
    public void pointcutBean(){

    }

    /**
     * 5、args匹配第一个入参是String类型的方法
     */
    @Pointcut("args(String, ..)")
    public void pointcutArgs(){

    }

    /**
     * 6、@annotation匹配是@Controller类型的方法
     */
    @Pointcut("@annotation(org.springframework.stereotype.Controller)")
    public void pointcutAnnocation(){

    }
    /**
     * 7、@within匹配@Controller注解下的方法,要求注解的@Controller级别为@Retention(RetentionPolicy.CLASS)
     */
    @Pointcut("@within(org.springframework.stereotype.Controller)")
    public void pointcutWithinAnno(){

    }
    /**
     * 8、@target匹配的是@Controller的类下面的方法,要求注解的@Controller级别为@Retention(RetentionPolicy.RUNTIME)
     */
    @Pointcut("@target(org.springframework.stereotype.Controller)")
    public void pointcutTargetAnno(){

    }
    /**
     * 9、@args匹配参数中标注为@Sevice的注解的方法
     */
    @Pointcut("@args(org.springframework.stereotype.Service)")
    public void pointcutArgsAnno(){

    }


    /**
     * 10、使用excution表达式
     * execution(
     *  modifier-pattern?           //用于匹配public、private等访问修饰符
     *  ret-type-pattern            //用于匹配返回值类型,不可省略
     *  declaring-type-pattern?     //用于匹配包类型
     *  name-pattern(param-pattern) //用于匹配类中的方法,不可省略
     *  throws-pattern?             //用于匹配抛出异常的方法
     * )
     *
     * 下面的表达式解释为:匹配com.leo.controller.HelloController类中以hello开头的修饰符为public返回类型任意的方法
     */
    @Pointcut(value = "execution(public * com.leo.controller.HelloController.hello*(..))")
    public void pointCut() {

    }

在这里插入图片描述

第六节 基于XML的AOP[了解]

1、准备工作

①加入依赖

和基于注解的 AOP 时一样。

②准备代码

把测试基于注解功能时的Java类复制到新module中,去除所有注解。

2、配置Spring配置文件

<!-- 配置目标类的bean -->
<bean id="calculatorPure" class="com.utaha.aop.imp.CalculatorPureImpl"/>
    
<!-- 配置切面类的bean -->
<bean id="logAspect" class="com.utaha.aop.aspect.LogAspect"/>
    
<!-- 配置AOP -->
<aop:config>
    
    <!-- 配置切入点表达式 -->
    <aop:pointcut id="logPointCut" expression="execution(* *..*.*(..))"/>
    
    <!-- aop:aspect标签:配置切面 -->
    <!-- ref属性:关联切面类的bean -->
    <aop:aspect ref="logAspect">
        <!-- aop:before标签:配置前置通知 -->
        <!-- method属性:指定前置通知的方法名 -->
        <!-- pointcut-ref属性:引用切入点表达式 -->
        <aop:before method="printLogBeforeCore" pointcut-ref="logPointCut"/>
    
        <!-- aop:after-returning标签:配置返回通知 -->
        <!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
        <aop:after-returning
                method="printLogAfterCoreSuccess"
                pointcut-ref="logPointCut"
                returning="targetMethodReturnValue"/>
    
        <!-- aop:after-throwing标签:配置异常通知 -->
        <!-- throwing属性:指定通知方法中用来接收目标方法抛出异常的异常对象的参数名 -->
        <aop:after-throwing
                method="printLogAfterCoreException"
                pointcut-ref="logPointCut"
                throwing="targetMethodException"/>
    
        <!-- aop:after标签:配置后置通知 -->
        <aop:after method="printLogCoreFinallyEnd" pointcut-ref="logPointCut"/>
    
        <!-- aop:around标签:配置环绕通知 -->
        <!--<aop:around method="……" pointcut-ref="logPointCut"/>-->
    </aop:aspect>
    
</aop:config>

3、测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:spring-context.xml"})
@Slf4j
public class AOPTest {
    
    @Autowired
    private Calculator calculator;
    
    @Test
    public void testLogAspect() {
        int add = calculator.add(10, 2);
        log.debug("add = " + add);
    }
}

第七节 AOP对获取bean的影响

一、根据类型获取 bean

1、情景一

bean 对应的类没有实现任何接口
根据 bean 本身的类型获取 bean

测试:IOC容器中同类型的 bean 只有一个
正常获取到 IOC 容器中的那个 bean 对象
测试:IOC 容器中同类型的 bean 有多个
会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个

2、情景二

bean 对应的类实现了接口,这个接口也只有这一个实现类

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

3、情景三

声明一个接口
接口有多个实现类
接口所有实现类都放入 IOC 容器

测试:根据接口类型获取 bean
会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个
测试:根据类获取bean
正常

4、情景四

声明一个接口
接口有一个实现类
创建一个切面类,对上面接口的实现类应用通知

测试:根据接口类型获取bean
测试:根据类获取bean
原因分析:
应用了切面后,真正放在IOC容器中的是代理类的对象
目标类并没有被放到IOC容器中,所以根据目标类的类型从IOC容器中是找不到的

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

debug查看代理类的类型:

补充:绕过 IOC 容器,单独创建目标对象是无法享受 AOP 增强的。

// 现象:调用目标对象的方法但是切面中的通知方法没有起作用
// 原因:AOP 增强的是 IOC 容器中的目标对象,
// 如果我们自己另外创建一个,那么和 AOP 没有任何关系,
// 无法享受到 AOP 的增强,
// 同时也说明切入点表达式查找目标方法也是在 IOC 容器中查找
GoodServiceImpl goodService = new GoodServiceImpl();
goodService.sayHello();

5、情景五

声明一个类
创建一个切面类,对上面的类应用通知

测试:根据类获取 bean,能获取到

debug查看实际类型:

二、自动装配

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

1、情景一

目标bean对应的类没有实现任何接口
根据bean本身的类型装配这个bean

测试:IOC容器中同类型的bean只有一个
正常装配
测试:IOC容器中同类型的bean有多个
会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

2、情景二

目标bean对应的类实现了接口,这个接口也只有这一个实现类,也没有被切面应用通知

测试:根据接口类型装配bean
正常
测试:根据类装配bean
正常

3、情景三

声明一个接口
接口有多个实现类
接口所有实现类都放入IOC容器

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

4、情景四

声明一个接口
接口有一个实现类
创建一个切面类,对上面接口的实现类应用通知

测试:根据接口类型装配bean
正常
测试:根据类装配bean
此时获取不到对应的bean,所以无法装配,抛出下面的异常:

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

5、情景五

声明一个类
创建一个切面类,对上面的类应用通知

测试:根据类装配bean
正常

三、总结

1、对实现了接口的类应用切面
在这里插入图片描述

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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值