day03 Spring-AOP面向切面编程

day03 Spring-AOP面向切面编程

1.AOP的概述

1.1 为什么需要AOP

1.1.1 情景设定
1.1.1.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);
    
}
1.1.1.2 给接口声明一个纯净版实现类

在这里插入图片描述

package com.atuigu;

/**
 * 计算器接口的实现类
 */

public class CalculatorPureImpl implements Calculator {
    @Override
    public int add(int i, int j) {
        return i + j;
    }

    @Override
    public int sub(int i, int j) {
        return i - j;
    }

    @Override
    public int mul(int i, int j) {
        return i * j;
    }

    @Override
    public int div(int i, int j) {
        return i / j;
    }
}

1.1.1.3 需求

在计算器的每个方法执行前后加入日志打印

在这里插入图片描述

1.1.1.4 实现方案探讨

方案一:在每个方法的前后都加上日志打印的代码

方案二:创建一个工具类,将日志打印的代码写在工具类中,然后再每个方法的前后直接调用工具类中的方法打印日志

方案三:创建一个父类,在父类的方法中打印日志,子类重写父类的方法(对目前功能不适用)

方案四:动态代理

方案五:AOP

2.代理模式

2.1 概念

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

未经过代理的情况:

在这里插入图片描述

使用了代理模式的情况:

在这里插入图片描述

2.2 相关术语

  1. 代理: 又称之为代理者,用于将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法
  2. 目标: 又称之为被代理者,用于执行核心逻辑,并且将代理者的非核心逻辑代码套用在目标类、对象、方法上

2.3 静态代理

2.3.1 创建静态代理类:

先添加依赖包

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.1</version>
        </dependency>
    </dependencies>

创建代理类

package com.atuigu.proxy;

import com.atuigu.Calculator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 代理类
 */
@Component
public class CalculatorProxy implements Calculator {

    // 自动装配 根据类型进行依赖注入
    @Autowired
    private Calculator calculator;

    @Override
    public int add(int i, int j) {
        System.out.println("日志 add 方法开始了,参数是:" + i + "," + j);
        // 调用代理对象的核心方法
        int result = calculator.add(i, j);
        System.out.println("日志 add 方法结束了,结果是:" + result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        System.out.println("日志 sub 方法开始了,参数是:" + i + "," + j);
        // 调用代理对象的核心方法
        int result = calculator.sub(i, j);
        System.out.println("日志 sub 方法结束了,结果是:" + result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        System.out.println("日志 mul 方法开始了,参数是:" + i + "," + j);
        // 调用代理对象的核心方法
        int result = calculator.mul(i, j);
        System.out.println("日志 mul 方法结束了,结果是:" + result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        System.out.println("日志 div 方法开始了,参数是:" + i + "," + j);
        // 调用代理对象的核心方法
        int result = calculator.div(i, j);
        System.out.println("日志 div 方法结束了,结果是:" + result);
        return result;
    }
}

2.3.2 测试结果
package com.atguigu;

import com.atuigu.Calculator;
import com.atuigu.proxy.CalculatorProxy;
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.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
public class CalculatorTest {

    // 自动装配 根据类型匹配
    @Autowired
    private CalculatorProxy calculatorProxy;

    @Test
    public void test01(){
        System.out.println(calculatorProxy.add(1, 2));
    }
}

实现需求

在这里插入图片描述

2.3.3 问题思考

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

提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

2.4 动态代理

在这里插入图片描述

2.4.1 创建生产代理对象的工厂类

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

package com.atuigu.proxy;

import com.atuigu.Calculator;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 在此类中编写代理规则
 */
public class AtguiguInvocationHandler<T> implements InvocationHandler {
    // 被代理者
    private T t;

    public AtguiguInvocationHandler(T t) {
        this.t = t;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1.invoke()方法它会在代理对象调用任何方法的时候都执行,所以我们在invoke()方法中编写代理规则
        // 参数一 proxy:指定的是代理对象
        // 参数二 method:指定的是代理对象调用的方法
        String methodName = method.getName();
        // 参数三 args:指定的是代理对象调用的方法传入的参数

        // 编写代理的规则
        // 1.如果被代理者是Calculator接口的实现类,并且调用的方法不是Object类的方法(就表示调用的是加减乘除方法)
        if (t instanceof Calculator && !Object.class.equals(method.getDeclaringClass())){
            System.out.println("日志" + methodName + "方法开始了,参数是:" + args[0] + "," + args[1]);
            // 调用被代理者的方法
            Object result = method.invoke(t, args);
            System.out.println("日志" + methodName + "方法结束了,参数是:" + result);
        }
        // 表示调用不是我们需要代理的方法,按照原本的方法执行
        return method.invoke(t, args);
    }
}

package com.atuigu.proxy;

import java.lang.reflect.Proxy;

public class ProxyFactory<T> {
    // 被代理者
    private final T t;
    // 创建代理工厂的时候将代理者传进来

    public ProxyFactory(T t) {
        this.t = t;
    }

    // 创建代理对象
    public T newInstance(AtguiguInvocationHandler<T> atguiguInvocationHandler){
        // Proxy.newProxyInstance(类加载器对象,要代理的接口的字节码对象数组,InvocationHandler接口的实现类对象)就是JDK提供的动态代理的API
        // 参数一:类加载器,我们可以使用被代理对象的类加载器
        // 1.获取被代理对象的字节码对象
        Class<?> clazz = t.getClass();
        // 2.使用被代理对象的字节码对象获取类加载器
        ClassLoader classLoader = clazz.getClassLoader();
        // 参数二:要代理的接口的字节码对象数组,我们可以使用被代理者对象实现的接口的数组
        // 3.使用被代理者的字节码对象获取被代理者实现的接口的数组
        Class<?>[] interfaces = clazz.getInterfaces();
        // 参数三:InvocationHandler接口的实现类对象,我们可以创建一个InvocationHandler接口的实现类
        // 转成代理类型
        return (T) Proxy.newProxyInstance(classLoader, interfaces, atguiguInvocationHandler);

    }
}

2.4.2 测试
package com.atguigu;

import com.atuigu.Calculator;
import com.atuigu.CalculatorPureImpl;
import com.atuigu.proxy.AtguiguInvocationHandler;
import com.atuigu.proxy.ProxyFactory;
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.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
public class CalculatorTest {

    // 自动装配 根据类型匹配
    @Autowired
    private CalculatorPureImpl calculatorPure;

    @Test
    public void test01(){
        // calculatorPure是被代理对象
        // 1.创建代理工厂
        ProxyFactory<Calculator> calculatorProxyFactory = new ProxyFactory<>(calculatorPure);
        // 2.创建代理对象
        Calculator proxyCalculatorPure = calculatorProxyFactory.newInstance(new AtguiguInvocationHandler<>(this.calculatorPure));

        // 3.代理方法调用方法
        // 调用Object方法并不会进行代理,没有在方法执行前后添加日志
        System.out.println(proxyCalculatorPure.toString());

        System.out.println(proxyCalculatorPure.add(1, 2));
    }
}

在这里插入图片描述

3.AOP的相关概念

3.1 概念

AOP:Aspect Oriented Programming 面向切面编程

3.2 作用

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

3.3 AOP的核心思路

在这里插入图片描述

3.4 AOP的相关术语

3.4.1 横切关注点(了解)

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

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

在这里插入图片描述

3.4.2 通知

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

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

在这里插入图片描述

3.4.3 切面

封装通知方法的类。

在这里插入图片描述

3.4.4 目标

被代理的目标对象,执行核心业务代码的那个对象

3.4.5 代理(了解)

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

3.4.6连接点(了解)

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

把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。连接点其实就是各个方法中可以被增强或修改的点
在这里插入图片描述

3.4.7 切入点

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。而切入点指的则是方法中真正要去配置增强或者配置修改的地方

4 基于注解方式配置AOP

4.1 加入依赖

在IOC所需依赖基础上载入下面依赖即可

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.1</version>
            <scope>test</scope>
        </dependency>

<!--        引入aop的依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.1</version>
        </dependency>
    </dependencies>

4.2 准备被代理的目标资源

4.2.1 接口
package com.atuigu;


/**
 * 计算器
 */
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);
}

4.2.2 接口的实现类

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

package com.atuigu;

import org.springframework.stereotype.Component;

/**
 * 计算器接口的实现类
 * 被代理类
 */

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

    @Override
    public int sub(int i, int j) {
        return i - j;
    }

    @Override
    public int mul(int i, int j) {
        return i * j;
    }

    @Override
    public int div(int i, int j) {
        return i / j;
    }
}

4.2.3 创建切面类
package com.atuigu.aspect;

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

/**
 * Aspect注解:指定一个切面类
 * Component注解:对这个切面类进行IOC
 * <p>
 * 注解AOP的关键点:
 * 1.一定要在配置文件中加上<aop:aspectj-autoproxy></> 表示允许自动代理
 * 2.切面类一定要加上Aspect注解,并且切面类一定要进行IOC
 * 3.其他的类改进型IOC和依赖注入的就一定要进行IOC和依赖注入
 * 4.通知上一定要指定切入点(怎么使用切入点表达式描述切入点又是一个难点)
 */
@Aspect
@Component
public class LogAspect {

    // 切入点 后续再介绍
    @Before("execution(int com.atuigu.CalculatorPureImpl.* (int,int))")
    public void printLogBefore() {
        System.out.println("[前置通知]在方法执行之前打印日志...");
    }

    @AfterReturning("execution(int com.atuigu.CalculatorPureImpl.* (int,int))")
    public void printLogAfterReturning() {
        System.out.println("[返回通知]在方法执行成功之后打印日志...");    }

    @AfterThrowing("execution(int com.atuigu.CalculatorPureImpl.* (int,int))")
    public void printLogAfterThrowing() {
        System.out.println("[AOP异常通知]在方法抛出异常之后打印日志...");
    }

    @After("execution(int com.atuigu.CalculatorPureImpl.* (int,int))")
    public void printLogAfter() {
        System.out.println("[AOP后置通知]在方法最终结束之后打印日志...");
    }



}

4.2.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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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.atuigu"></context:component-scan>

<!--    开启注解AOP-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>
4.2.5 测试
package com.atguigu;

import com.atuigu.Calculator;
import com.atuigu.CalculatorPureImpl;
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.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
public class CalculatorTest {

    // 自动装配 根据类型匹配
    @Resource
    private Calculator Calculator;

    @Test
    public void testAdd(){
        System.out.println(Calculator.add(22, 33));

    }
}

在这里插入图片描述

4.3 通知执行顺序

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

4.4 在通知内部获取细节信息

4.4.1 JoinPoint接口
  • 要点1:JoinPoint接口通过getSignature()方法获取目标方法的签名
  • 要点2:通过目标方法签名对象获取方法名
  • 要点3:通过JoinPoint对象获取外界调用目标方法时传入的实参列表组成的数组
    // 切入点表达式:告诉Sprint当前通知方法套用到哪个目标方法上
    // 在前置通知方法形参位置声明一个JoinPoint类型的参数,spring就会将这个对象传入
    // 根据JoinPoint对象就可以获取目标方法名称/实际参数列表
    @Before("execution(int com.atuigu.CalculatorPureImpl.* (int,int))")
    public void printLogBefore(JoinPoint joinPoint) {
        // 1.通过JoinPoint对象获取目标方法签名对象
        // 获取方法的签名信息
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 2.从方法的签名信息中,我们可以获取到很多东西
        // 2.1 获取切入点方法
        Method method = methodSignature.getMethod();
        // 2.2 获取方法的所属的类的全限定名
        Class<?> clazz = method.getDeclaringClass();
        String className = clazz.getName();
        // 2.3 获取方法名
        String methodName = method.getName();
        // 2.4 获取方法的返回值类型
        Class<?> returnType = method.getReturnType();
        String returnTypeName = returnType.getName();
        // 2.5 获取方法的参数信息
        Object[] args = joinPoint.getArgs();

        System.out.println("[前置通知]" + className + "." + methodName + "方法开始了,参数是:" + args[0] + "," + args[1]);
    }

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

4.4.2 获取目标方法的方法返回值

只有在AfterReturning返回通知中才能够获取目标方法的返回值

    // @AfterReturning注解标记返回通知方法
    // 在返回通知中获取目标方法返回值分两步:
    // 第一步:在@AfterRetruning注解中通过returnning属性设置一个名称
    // 第二部:使用returning属性设置的名称在通知方法中声明一个对应的形参
    @AfterReturning(value = "execution(int com.atuigu.CalculatorPureImpl.* (int,int))", returning = "result")
    public void printLogAfterReturning(JoinPoint joinPoint, Object result) {
        // 1.获取方法的签名信息
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 获取切入点方法
        Method method = methodSignature.getMethod();
        // 获取全限定名
        String className = method.getDeclaringClass().getName();
        // 获取方法名
        String methodName = method.getName();
        // 2.获取切入点执行的结果
        System.out.println("[返回通知]" + className + "." + methodName + "方法正常结束了,执行结果是:" + result);    }

打印效果:

在这里插入图片描述

4.4.3 获取目标方法抛出的异常

只有在AfterThrowing异常通知中才能获取到目标方法抛出的异常

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

    // @AfterThrowing注解标记异常通知方法
    // 在异常通知中获取目标方法抛出的异常分两步:
    // 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
    // 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
    @AfterThrowing(value = "execution(int com.atuigu.CalculatorPureImpl.* (int,int))", throwing = "throwable")
    public void printLogAfterThrowing(JoinPoint joinPoint, Throwable throwable) {
        // 1.获取方法的签名信息
         MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 获取切入点方法
        Method method = methodSignature.getMethod();
        // 获取全限定名
        String className = method.getDeclaringClass().getName();
        // 获取方法名
        String methodName = method.getName();
        System.out.println("[AOP异常通知]" + className + "." + methodName + "方法异常结束了,异常信息是" + throwable);
    }

打印效果:

在这里插入图片描述

4.4.4 After 目标方法执行后执行

无论是正常结束还是异常结束都会执行

    // @AfterThrowing注解通知方法
    // 不管是正常结束还是异常结束 都会执行此方法 无法获取到返回值和报错信息 因为不确定是否正常执行结束和异常结束
    @After("execution(int com.atuigu.CalculatorPureImpl.* (int,int))")
    public void printLogAfter(JoinPoint joinPoint) {
        // 1.h获取方法的签名信息
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 2.获取方法对象
        Method method = methodSignature.getMethod();
        // 3.获取全限定名
        String className = method.getDeclaringClass().getName();
        // 4.获取方法名
        String methodName = method.getName();

        System.out.println("[AOP后置通知]" + className +"." + methodName + "方法执行结束了");
    }

4.5 切入点

4.5.1 重用切入点
4.5.1.1 声明切入点

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

@Pointcut("execution(int com.atguigu.component.CalculatorPureImpl.*(int,int))")
public void calculatorPointCut(){
}
4.5.1.2 同一个类内部引用切入点
@Before("calculatorPointCut()")
public void printLogBeforeCore(JoinPoint joinPoint){
4.5.1.3 在其他类中引用切入点
@Before("com.atguigu.pointcut.AtguiguPointCut.calculatorPointCut()")
public void printLogBeforeCore(JoinPoint joinPoint){}
4.5.1.4 对项目中的所有切入点进行统一管理

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

package com.atguigu.pointcut;

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

@Component
public class AtguiguPointCut {

    // 表示返回值可以任意类型 任意包中任意类的任意方法 任意参数类型和任意个数
    @Pointcut("execution(* *..*.*(..))")
    public void pt1(){}

    // 精确匹配
    @Pointcut("!execution(int com.atguigu.component.CalculatorPureImpl.add(int,int))")
    public void pt2(){}

    // 两者任意一个都可以匹配
    @Pointcut("execution(int com.atguigu.component.CalculatorPureImpl.add(int,int)) || execution(int com.atguigu.component.CalculatorPureImpl.sub(int,int))")
    public void pt3(){}

    // 两者都满足才可以匹配
    @Pointcut("!execution(int com.atguigu.component.CalculatorPureImpl.add(int,int)) && execution(int com.atguigu.component.CalculatorPureImpl.*(int,int))")
    public void pt4(){}
}

4.6 切入点表达式语法

4.6.1 切入点表达式的作用

在这里插入图片描述

切入点表达式的作用是用于描述将代理逻辑套用在哪些目标方法上

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

(1). 返回值类型
① 精确
② * 表示任意返回值类型

(2). 包名:
① 精确
② *. 表示一级包,包名可以任意
③ *… 表示包的层级任意,包名也任意

(3). 类名:
① 精确
② * 任意类名
③ *Impl

(4). 方法名:
① 精确 add
② * 任意方法名
③ select*

(5). 参数:
① 精确 (int,int)
② * 表示一个参数,但是类型可以任意
③ (…) 参数的个数和类型都任意

对于execution()表达式整体可以使用三个逻辑运算符号

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

在这里插入图片描述

4.7 环绕通知

4.7.1 什么是环绕通知

环绕通知对应整个try…catch…finally结构,可以在目标方法的各个部位进行套用代理逻辑,它能够真正介入并改变目标方法的执行
(1). 作用位置: 切入点执行的前后
(2). 注解: @Around

4.7.2 什么情况下使用环绕通知?

​ 在切入点执行之前,校验切入点的参数; 在切入点执行成功之后,记录切入点执行的结果; 在切入点执行出现异常之后,将异常信息记录到日志文件中。不适合用环绕通知,因为这里是多件事情
​ 在切入点上加入事务控制: 适合使用环绕通知
​ (1). 开启事务
​ (2). 执行切入点
​ (3). 没有异常,则提交事务
​ (4). 出现异常,则回滚事务
​ (5). 无论出没出现异常,都需要设置connection的autoCommit为true,并且归还连接

​ 计算切入点的执行时长: 适合使用环绕通知
​ (1). 获取切入点执行之前的毫秒数
​ (2). 执行切入点
​ (3). 获取切入点执行结束之后的毫秒数
​ (4). 两个毫秒数之间的差值就是切入点的执行时长

​ 缓存: 适合使用环绕通知
​ (1). 从缓存中查询数据,如果有数据则直接返回
​ (2). 如果缓存中没有数据,则执行切入点从数据库查询数据
​ (3). 将数据库查询出来的数据存储到缓存中
​ (4). 返回数据库查询到的数据

4.7.3 计算方法执行总时长
    @Around("com.atguigu.pointcut.AtguiguPointCut.pt1()")
    public Object printExecuteTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 1.获取切入点执行前的毫秒数
        long startTime = System.currentTimeMillis();
        // 2.执行切入点
        try {
            Object result = proceedingJoinPoint.proceed();
            return result;
        } catch (Throwable e) {
            e.printStackTrace();
            throw e;
        } finally {
            // 休眠3秒
            Thread.sleep(3000);
            // 3.获取切入点执行之后的毫秒数
            long endTime = System.currentTimeMillis();
            // 4.计算执行时长
            long executeTime = endTime - startTime;
            MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
            String className = methodSignature.getMethod().getDeclaringClass().getName();
            String methodName = methodSignature.getMethod().getName();
            // 5.日志输出执行时长
            System.out.println("[环绕通知]" + className + "." + methodName + "方法执行的总时长是:" + executeTime);
        }
    }

测试结果

在这里插入图片描述

4.8 切面的优先级

4.8.1 优先级的规则

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

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

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

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

在这里插入图片描述

4.8.2 实际意义

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

在这里插入图片描述

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

4.9 CGLIB的动态代理

4.9.1 动态代理的分类

动态代理分成两种:

第一种是JDK内置的动态代理,这种动态代理需要被代理者实现接口,如果被代理者没有实现接口,那么则无法使用JDK的动态代理

第二种是CGLIB的动态代理,在被代理类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。

4.9.2 Debug查看
4.9.2.1 没有实现接口的情况

在这里插入图片描述

4.9.2.2 由实现接口的情况

在这里插入图片描述

4.9.3 Spring中到底使用哪种动态代理

如果要创建代理对象的类实现了接口,那么就使用JDK的动态代理;如果要创建代理对象的类没有实现接口,那么就使用CGLIB的动态代理

5.基于XML方式配置AOP(了解)

5.1 准备工作

5.1.1 加入依赖

和基于注解的AOP时一样

5.1.2 准备代码

把基于注解的Module复制一份,修改Module名,并导入到工程中,然后去除所有AOP注解。

5.2 配置Spirng配置文件

<?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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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.atguigu"/>

    <!--
        使用xml方式配置AOP:
            1. 切面: 封装非核心逻辑的那个类,非核心逻辑就是封装在切面的方法中
            2. 通知: 将非核心逻辑套在核心逻辑上进行执行
            3. 切入点: 核心逻辑
    -->
    <aop:config>
        <!--
            1. 切面: ref属性就是指定作为切面的那个对象的id,order属性表示切面的优先级
        -->
        <aop:aspect id="myAspect" ref="logAspect">
            <!--2. 通知-->
            <!--配置前置通知-->
            <aop:before method="printLogBeforeCore" pointcut-ref="calculatorPoint"/>
            <!--配置返回通知-->
            <aop:after-returning method="printLogAfterReturning" pointcut-ref="calculatorPoint" returning="result"/>
            <!--配置异常通知-->
            <aop:after-throwing method="printLogAfterThrowing" pointcut-ref="calculatorPoint" throwing="throwable"/>
            <!--配置后置通知-->
            <aop:after method="printLogFinallyEnd" pointcut-ref="calculatorPoint"/>
            <!--配置环绕通知-->
            <aop:around method="printLogAround" pointcut-ref="calculatorPoint"/>
            <!--3. 切入点-->
            <aop:pointcut id="calculatorPoint"
                          expression="execution(* com.atguigu.component.CalculatorPureImpl.*(..))"/>
        </aop:aspect>
    </aop:config>
</beans>

5.3 测试

package com.atguigu;

import com.atguigu.component.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.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-application.xml")
public class TestAop {
    @Autowired
    private Calculator calculator;

    @Test
    public void testAdd(){
        //调用CalculatorPureImpl对象的add()方法
        System.out.println("调用完目标方法之后获取返回值是:"+calculator.sub(5, 3));
    }
}

6. AOP总结

目标:

​ 1. 将目标方法中的非核心业务抽取出来制作成通知

2. 在调用目标方法的核心业务的时候,底层动态自动将非核心业务套在核心业务上执行

实现目标:

  1. 识别出哪些地方可以抽取

  2. 准备一个切面类:

    2.1 IOC:@Component

    2.2 让他称为切面类:@Aspect

  3. 将抽取出来的代码封装成方法(通知),方法放在切面类中

  4. 让切面类中的方法称为通知

    4.1 前置通知: Before

    4.2 返回通知:AfterReturning

    4.3 异常通知:AfterThrowing

    4.4 后置通知:After

    4.5 环绕通知Around

  5. 指定通知的作用位置(切入点):就是引用切入点

    5.1 如果切入点和通知在同一个类中:根据方法名引用

    5.2 如果切入点跟通知不在同一个类中:根据类的全限定名.方法名来引用

​ 6. 声明切入点以及切入点的语法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值