面向切面:AOP

面向切面:AOP

大家好,今天本篇博客我们来了解Spring里边的另一个重要部分,叫做AOP,也就是我们说的面向切面编程。

1、场景模拟

首先第一部分,咱们做一个场景模拟。我们先写一个简单的例子,然后通过例子引出我们的问题。

咱们写一个简单的计算器的例子,就做一个加减乘除的功能。

然后把例子写完之后,在加减乘除的每个方法中分别添加上日志。

就是在你的方法之前,还有之后每个里边都加上日志。然后这个之后我们会看到代码中的问题,咱们分析问题,然后提出解决方案。

搭建子模块:spring6-aop

1.1 声明接口

声明计算器接口Calculator,包含加减乘除的抽象方法

package com.jie.aop.service;

/**
 * 计算器接口
 *
 * @author 阿杰 2416338031@qq.com
 * @version 1.0
 * @date 2023/10/30 4:16
 */
public interface CalculatorService {

    /**
     * 加法
     *
     * @param i
     * @param j
     * @return: int
     * @date: 2023/10/30 4:16
     */
    int add(int i, int j);

    /**
     * 减法
     *
     * @param i
     * @param j
     * @return: int
     * @date: 2023/10/30 4:16
     */
    int sub(int i, int j);

    /**
     * 乘法
     *
     * @param i
     * @param j
     * @return: int
     * @date: 2023/10/30 4:16
     */
    int mul(int i, int j);

    /**
     * 除法
     *
     * @param i
     * @param j
     * @return: int
     * @date: 2023/10/30 4:16
     */
    int div(int i, int j);

}

1.2 创建实现类

package com.jie.aop.service.impl;

import com.jie.aop.service.CalculatorService;

/**
 * 计算器接口实现类
 *
 * @author 阿杰 2416338031@qq.com
 * @version 1.0
 * @date 2023/10/30 4:24
 */
public class CalculatorServiceImpl implements CalculatorService {
    @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;
    }
}

1.3 创建带日志功能的实现类

image-20231030042801654

package com.jie.aop.service.impl;

import com.jie.aop.service.CalculatorService;

/**
 * 带日志的计算器接口实现类
 *
 * @author 阿杰 2416338031@qq.com
 * @version 1.0
 * @date 2023/10/30 4:27
 */
public class CalculatorLogImpl implements CalculatorService {

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

1.4 提出问题

就以这个带日志这个代码为例,咱们现在来分析这个代码中它会有什么问题。

image-20231030043136568

①现有代码缺陷

各位注意,我刚才提到这两个输出代表的是不是日志功能,但是大家明确这个日志跟核心的业务是不是没有关系。

就是我现在可以这么理解,没有日志这功能是不是也能做,有日志之后可以让它信息输出的更完整,但是没有日志对我核心功能没有影响。

但是你看现在问题是什么?我的日志跟核心功能它们是不是混合到一起了?

不管你是加减乘除里边是不是都这么做的,那这个时候我们在写代码的时候会有一个问题,我们既要关心核心业务,也要关注这个日志的一个业务。但是它里边核心业务跟它因为混到一起了,所以造成里边维护起来是不是特别不方便。

那这个时候我们肯定想这么做,让我们的核心功能和核心代码跟我的日志要分离出来。但是目前没有分离,他们是整合到一起了,给它混到一起来写的。

这么做不管你是从代码的可维护性,还是代码的各方面来看,它肯定是特别混乱的。

②解决思路

所以我们对他要进行一个完善,那怎么完善呢?核心就是:解耦。

思路很明确,让核心业务或者核心代码和日志的功能给它分离出来。

③困难

现在这个日志具体到了我们的每个方法里边,每个方法中都做了日输出,都是在之前之后。

而这个时候如果按照普通方式,比如咱们写一个工具类,然后做这个过程,它好像并不能做到。

因为它是渗透到你的方法内部,在核心的方法业务的前后做这个过程。

所以咱要把它把核心业务和日志分离出来,用原始方式就没法做到了。

这个时候咱需要用到一个新的技术才能实现。那技术是什么呢?

2、代理模式

2.1、概念

①介绍

代理是一种二十三种设计模式中的一种,属于结构型模式。

它的作用是什么呢?

大家可以这么理解,我们提供一个代理类,让我们调用目标方法的时候,不是直接调用,而是通过代理类间接调用,这个过程就叫做代理模式。

那具体怎么理解?大家看这张图里面画的,这里没有使用代理模式。咱直接调方法。

image-20231030044316204

比如我现在我想做一个加的操作,直接调A的方法,想做减操作,想做乘,想做除,那就直接调这方法,这是我们的普通方式。

但是有了代理模式之后,咱们怎么来做?大家看这过程。

image-20231030044357580

我们调的时候并不是直接调你的具体方法,调什么?调你的代理对象。

通过代理对象或者代理类去间接调用咱们的目标方法,最终得到结果。

大家看这张图里边,咱们调用目标方法,比如说调这个加减乘除这个方法,那怎么做?先去经过你的代理,然后由代理返回,而内部由代理去调用他的目标方法,这个过程就叫代理模式。

②生活中的代理

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

③相关术语

  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
  • 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。

2.2 静态代理

静态你可以理解为它把里面部分都是写固定了。

创建静态代理类:

package com.jie.aop.service.impl;

import com.jie.aop.service.CalculatorService;

/**
 * 静态代理类
 *
 * @author 阿杰 2416338031@qq.com
 * @version 1.0
 * @date 2023/10/30 4:49
 */
public class CalculatorStaticProxy implements CalculatorService {
    
    @Override
    public int add(int i, int j) {
        return 0;
    }

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

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

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

然后代理类之中要怎么做呢?我们刚才提到了,因为我代理类最终要调目标方法,所以我们把被代理的目标对象给他要传入进来,那怎么传呢,通过构造方法把它进行传递。

image-20231030045212657

下面在里边我们可以写这个具体过程。我们就以添加为例,在添加中怎么做?比如我现在我要加上我的日志,那咱们来做个实现。这个日志就是我们那个非核心功能。

image-20231030045446902

然后写完之后,大家看这个代码,虽然说咱们写完了,针对于我们这个目标对象确实做到了解耦操作。

就是这里边你看核心业务中没有什么干扰代码。

image-20231030045622295

然后把干扰代码都放到咱这个代理类中进行实现。但是它有一些致命的缺陷。

我这代码是不是写固定了,或者说写死了。

就拿日志来讲,这个日志,如果后面在其他的代码里也要用到,那是不是需要再加个代理类实现?

也就是说我这个日志功能其实并没有给它抽取出来。如果再有相关功能,那还要写代理类进行实现。所以这个方式它并不好,它里面就是并没有做到一个真正的灵活性。

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

2.3 动态代理

动态代理它是怎么一个过程呢?咱们把这张图先看一下,然后咱用代码具体把动态代理做个实现。

image-20231030063848117

首先我们肯定也是建个代理类,但是它跟静态代理的最大区别是什么呢?

我们不需要每个部分或者说每个功能中都建个静态代理类,咱只需要动态创建一个,不管你是什么目标或者目标对象,为它建这么一个代理类。

然后在里边把日志做统一管理,在操作前在操作后。而这个时候我们调方法的时候,不管你调什么方法,它都会在操作前操作后进行日志输出。

这就叫动态代理,实现了咱们达到的效果,把日志统一进行管理,不需要每个部分都建个代理类。然后他也做到了在方法前方法后进行日志的输出。

面我们用代码把这个具体做个实现。

package com.jie.aop.proxy;

import org.springframework.cglib.proxy.InvocationHandler;
import org.springframework.cglib.proxy.Proxy;

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

/**
 * 代理工厂类
 *
 * @author 阿杰 2416338031@qq.com
 * @version 1.0
 * @date 2023/10/30 6:43
 */
public class ProxyFactory {

    /**
     * 将被代理的目标对象声明为成员变量
     */
    private final Object target;

    /**
     * 通过构造器为目标对象赋值
     *
     * @param target 目标对象
     * @date: 2023/10/30 6:43
     */
    public ProxyFactory(Object target) {
        this.target = target;
    }

    /**
     * 通过动态代理创建代理对象
     *
     * @param
     * @return: java.lang.Object
     * @date: 2023/10/30 6:43
     */
    public Object getProxy() {

        /**
         * newProxyInstance():创建一个代理实例
         * 其中有三个参数:
         * 1、classLoader:加载动态生成的代理类的类加载器
         * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
         * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
         */
        // 获取目标对象的类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
        // 获取目标对象实现的所有接口的class对象所组成的数组
        Class<?>[] interfaces = target.getClass().getInterfaces();
        // 设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy,
                                 Method method,
                                 Object[] args) {
                /**
                 * proxy:代理对象
                 * method:代理对象需要实现的方法,即其中需要重写的方法
                 * args:method所对应方法的参数
                 */
                Object result = null;
                try {
                    System.out.println("[动态代理][日志] " + method.getName() + ",参数:" + Arrays.toString(args));
                    // 调用目标对象的方法
                    result = method.invoke(target, args);
                    System.out.println("[动态代理][日志] " + method.getName() + ",结果:" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("[动态代理][日志] " + method.getName() + ",异常:" + e.getMessage());
                } finally {
                    System.out.println("[动态代理][日志] " + method.getName() + ",方法执行完毕");
                }
                return result;
            }
        };

        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
}

动态代理测试:

import com.jie.aop.proxy.ProxyFactory;
import com.jie.aop.service.CalculatorService;
import com.jie.aop.service.impl.CalculatorLogImpl;
import org.junit.jupiter.api.Test;

/**
 *  动态代理测试类
 *
 * @author 阿杰 2416338031@qq.com
 * @version 1.0
 * @date 2023/10/30 6:54
 */
public class TestCal {

    @Test
    public void testDynamicProxy(){
        // 创建代理工厂
        ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl());
        // 获取代理对象
        CalculatorService proxy = (CalculatorService) factory.getProxy();
        // 调用方法
        proxy.add(1,0);
    }
}

image-20231030065719494

3、AOP概念及相关术语

3.1 概述

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

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

3.2 相关术语

①横切关注点

首先看第一个术语叫横切关注点。那这个什么意思呢?

给大家说明,比如我现在有很多模块,现在我各个模块中都想解决同样一个问题。比如说我这些模块中都想加日志,事务,用户验证。那这个时候这里边的用户验证,日志,事务就属于叫横切关注点。

就是我们从方法中抽取同样一类非核心业务,那这个业务就叫横切关注点。

②通知(增强)

然后再看第二个,通知或者叫增强。什么叫增强呢?通俗来说就是想要增强的功能。

比如说我想加一个用户的安全校验,想加一个事务,想加上一个日志等等,这个就是叫通知或者叫增强。

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

通知拥有5种类型:

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

image-20231113071736786

③切面

什么叫切面呢?就是我们封装通知方法的类。比如说我写个类,里面有前置、后置、什么返回等等,那这个类就叫做切面。

④目标

被代理的目标对象。

⑤代理

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

⑥连接点

什么叫连接点?我们看这张图里边,给大家说明连接点怎么去理解。

image-20231113072039489

我现在把方法排成一排,就是什么添加和其他方法,然后每个横切位置看成一个X轴,另外把方法从上到下的执行顺序看成一个Y轴,而X和Y轴的交叉点就叫连接点,这是我们从字面上的解释。

那通俗来说,什么叫连接点?你可以这么理解,就是spring里边允许你使用通知的地方就叫连接点

那大家想一下,这个使用通知地方就太多了,我在方法前,在方法后是不是都可以使用这个通知,所以这个就叫连接点,就是通俗说你允许使用通知的地方就叫连接点。

⑦切入点

什么叫切入点?

你可以理解为我们现在通过spring的AOP技术,可以直接定位到你的特定的连接点。

比如定位到我方法之前,定位到方法之后,然后这个时候通俗的说就是实际去增强的方法,那个地方就叫切入点。比如现在我想对A的方法做增强,想在A的之后进行输出,这个就叫切入点。

4、基于注解的AOP

下面我们用spring进行AOP的具体操作,在spring里面进行AOP操作,它也是基于两种方式,第一种基于注解实现,第二个基于XML配置文件实现。我们先知道基于注解该怎么去做。

4.1 技术说明

首先我们说第一个内容,动态代理的这个分类,然后它分成哪些呢?给大家说明它有两类。第一类叫做JDK的动态代理,然后还有第二部分叫做CG lib的动态代理,它有两个分类,那这两个具体什么意思,咱们来做一个说明。

大家看我这个图。

image-20231113073030523

它有两种情况。第一种情况,当你代理的这个对象是一个有接口情况,第二个是没有接口。

如果有接口的时候,那他用的一个叫JDK的动态代理。如果没有接口,用的是CG lib的动态代理。

首先第一个情况就是有接口。有接口的时候,它使用的是JDK的动态代理。然后它怎么做的呢?

比如说我现在这部分我们有一个接口和它的实现类。

image-20231113073435234

然后过程中比如现在我们想用动态代理来增强里面的方法,那怎么做呢?那

我们就要创建接口实现类的代理对象。说的简单点,我们的这个代理类也要实现这个接口。只是代理类不是直接new出来的,但是跟new出来的对象具有相同的效果。

image-20231113073706314

因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。

第二种情况。

如果说你没有接口,那这个时候使用就是CG lib的动态代理。

而这个是怎么做的呢?你可以理解为它继承了这个目标这个类,生成了一个子类代对象。

通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。

AspectJ:

这是个什么东西呢?

其实这个本身它也一个框架,它是一个AOP的这么一个框架,就是本身它不是spring一部分。

但是为什么这里面出现一个AspectJ 呢?给大家强调,因为AspectJ里边很多的方法特别的方便,它里面注解也特别强大,spring就是依赖于这个AspectJ的注解实现AOP的功能,所以spring用到里面的注解,最终可以很方便的实现AOP中的各种功能。

其实这个AspectJ 本质上是一个静态代理,它用代理逻辑“织入”被代理的目标类编译得到的字节码文件这个,所以它效果是一个动态的。但是本身是一个静态代理,spring只是借用里面的注解,最终实现了AOP的功能。

4.2 准备工作

①添加依赖

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

<dependencies>
    <!--spring context依赖-->
    <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.0.2</version>
    </dependency>

    <!--spring aop依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>6.0.2</version>
    </dependency>
    <!--spring aspects依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>6.0.2</version>
    </dependency>

    <!--junit5测试-->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.3.1</version>
    </dependency>

    <!--log4j2的依赖-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.19.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j2-impl</artifactId>
        <version>2.19.0</version>
    </dependency>
</dependencies>

②准备被代理的目标资源

接口:

package com.jie.aop.annoaop.service;

/**
 * 计算器接口
 *
 * @author 阿杰 2416338031@qq.com
 * @version 1.0
 * @date 2023/10/30 4:16
 */
public interface CalculatorService {

    /**
     * 加法
     *
     * @param i
     * @param j
     * @return: int
     * @date: 2023/10/30 4:16
     */
    int add(int i, int j);

    /**
     * 减法
     *
     * @param i
     * @param j
     * @return: int
     * @date: 2023/10/30 4:16
     */
    int sub(int i, int j);

    /**
     * 乘法
     *
     * @param i
     * @param j
     * @return: int
     * @date: 2023/10/30 4:16
     */
    int mul(int i, int j);

    /**
     * 除法
     *
     * @param i
     * @param j
     * @return: int
     * @date: 2023/10/30 4:16
     */
    int div(int i, int j);

}

实现类:

package com.jie.aop.annoaop.service.impl;

import com.jie.aop.annoaop.service.CalculatorService;
import org.springframework.stereotype.Component;

/**
 * 带日志的计算器接口实现类
 *
 * @author 阿杰 2416338031@qq.com
 * @version 1.0
 * @date 2023/10/30 4:27
 */
@Component
public class CalculatorLogImpl implements CalculatorService {

    @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.3 创建切面类并配置

image-20231113202616133

下一步需要建一个配置文件,在配置文件中做一些相关配置包的扫描规则,以及配置AspectJ 的自动代理。

image-20231113203142716

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

    <!-- 开启注解扫描 -->
    <context:component-scan base-package="com.jie.aop.annoaop"/>

    <!-- 开启AspectJ的自动代理,为目标对象自动生成代理 -->
    <aop:aspectj-autoproxy/>
</beans>

完成之后,最后我们来到这个切面类中,在里边做什么?设置你的切入点,还有我们那个通知的类型。

package com.jie.aop.annoaop.aspect;

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

import java.util.Arrays;

/**
 * 日志切面类
 *
 * @author 阿杰 2416338031@qq.com
 * @version 1.0
 * @date 2023/11/13 20:22
 */
@Aspect
@Component
public class LogAspect {

    /**
     * 前置通知 @Before(value = "切入点表达式配置切入点")
     *
     * @param joinPoint 连接点
     *                  JoinPoint 业务方法,要加入切面功能的业务方法
     *                  作用是:可以在通知方法中获取方法执行时的信息,例如方法名称,方法参数等
     *                  如果你的切面功能中需要用到方法的信息,就加入JoinPoint
     *                  这个JoinPoint参数的值是由框架赋予,必须是第一个位置的参数
     *                  通知方法是是spring框架调用的,不是我们调用的,我们只是配置
     *                  spring框架在调用通知方法时,会把这个连接点对象作为实参传递给通知方法
     */
    @Before("execution(public int com.jie.aop.annoaop.service.impl.CalculatorLogImpl.*(..))")
    public void beforePrintLog(JoinPoint joinPoint) {
        // 获取方法名称
        String name = joinPoint.getSignature().getName();
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        System.out.println("[日志] 前置通知 方法开始了,方法名称是:" + name + ",方法参数是:" + Arrays.toString(args));
    }

    /**
     * 后置通知 @AfterReturning(value = "切入点表达式配置切入点")
     */
    @AfterReturning(value = "execution(public int com.jie.aop.annoaop.service.impl.CalculatorLogImpl.*(..))")
    public void afterReturningPrintLog(JoinPoint joinPoint) {
        // 获取方法名称
        String name = joinPoint.getSignature().getName();
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        System.out.println("[日志] 后置通知 方法开始了,方法名称是:" + name + ",方法参数是:" + args);
    }

    /**
     * 异常通知 @AfterThrowing(value = "切入点表达式配置切入点")
     * throwing = "ex":指定通知方法中的参数名,用于接收异常信息
     */
    @AfterThrowing(value = "execution(public int com.jie.aop.annoaop.service.impl.CalculatorLogImpl.*(..))", throwing = "ex")
    public void afterThrowingPrintLog(JoinPoint joinPoint, Throwable ex) {
        // 获取方法名称
        String name = joinPoint.getSignature().getName();
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        System.out.println("[日志] 异常通知 方法开始了,方法名称是:" + name + ",方法参数是:" + args + ",异常信息是:" + ex);
    }

    /**
     * 最终通知 @After(value = "切入点表达式配置切入点")
     * 无论是否有异常,都会执行
     * returning = "result":指定通知方法中的参数名,用于接收返回值
     */
    @AfterReturning(value = "execution(public int com.jie.aop.annoaop.service.impl.CalculatorLogImpl.*(..))", returning = "result")
    public void afterPrintLog(JoinPoint joinPoint, Object result) {
        // 获取方法名称
        String name = joinPoint.getSignature().getName();
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        System.out.println("[日志] 最终通知 方法开始了,方法名称是:" + name + ",方法参数是:" + args + ",结果是:" + result);
    }

    /**
     * 环绕通知 @Around(value = "切入点表达式配置切入点")
     * proceedingJoinPoint:等同于Method
     */
    @Around("execution(* com.jie.aop.annoaop.service.impl.CalculatorLogImpl.*(..))")
    public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint) {
        Object result = null;
        try {
            // 获取方法名称
            String name = proceedingJoinPoint.getSignature().getName();
            // 获取方法参数
            Object[] args = proceedingJoinPoint.getArgs();
            System.out.println("[日志] 环绕通知 方法开始了,方法名称是:" + name + ",方法参数是:" + args);
            // 明确调用业务层方法(切入点方法)
            result = proceedingJoinPoint.proceed(args);
            System.out.println("[日志] 环绕通知 方法结束了,结果是:" + result);
        } catch (Throwable throwable) {
            System.out.println("[日志] 环绕通知 方法出现异常了,异常信息是:" + throwable);
            throwable.printStackTrace();
        } finally {
            System.out.println("[日志] 环绕通知 方法结束了");
        }
        return result;
    }
}

执行测试:

image-20231113211503068

image-20231113211745090

4.4 各种通知

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

各种通知的执行顺序:

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

4.5 切入点表达式语法

①作用

image-20231113212434538

②语法细节

  • 用 *** **号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限

  • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。

    • 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
  • 在包名的部分,使用“ ***… **” 表示包名任意、包的层次深度任意

  • 在类名的部分,类名部分整体用*号代替,表示类名任意

  • 在类名的部分,可以使用*号代替类名的一部分

    • 例如:*Service匹配所有名称以Service结尾的类或接口
  • 在方法名部分,可以使用*号表示方法名任意

  • 在方法名部分,可以使用*号代替方法名的一部分

    • 例如:*Operation匹配所有方法名以Operation结尾的方法
  • 在方法参数列表部分,使用(…)表示参数列表任意

  • 在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头

  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的

    • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符

    • 例如:execution(public int Service.(…, int)) 正确
      例如:execution(
      int *…Service.(…, int)) 错误

image-20231113212549694

4.6 重用切入点表达式

大家看我刚才的例子中,有没有发现我这里写的时候。

image-20231113212815157

你发现我这个Before上边,还有AfterReturning上边,包括你看里边我这个切入点表达式写的是不是都是一样的?

那这表达式其实咱们把它定义一次,其他地方直接引用就可以了,而不需要重复写入多次,这就叫做重用切入点表达式。

①声明

    /**
     * 重用切入点表达式
     */
    @Pointcut("execution(public int com.jie.aop.annoaop.service.impl.CalculatorLogImpl.*(..))")
    public void pointcut() {
    }

②在同一个切面中使用

@Before("pointcut()")
    public void beforePrintLog(JoinPoint joinPoint) {
        // 获取方法名称
        String name = joinPoint.getSignature().getName();
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        System.out.println("[日志] 前置通知 方法开始了,方法名称是:" + name + ",方法参数是:" + Arrays.toString(args));
    }

③在不同切面中使用

 @Before("com.jie.aop.annoaop.aspect.LogAspect.pointcut()")
    public void beforePrintLog(JoinPoint joinPoint) {
        // 获取方法名称
        String name = joinPoint.getSignature().getName();
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        System.out.println("[日志] 前置通知 方法开始了,方法名称是:" + name + ",方法参数是:" + Arrays.toString(args));
    }

4.7 切面的优先级

大家注意,我们相同的方法可能同时存在于多个切面,就是多个切面对它都做增强。那这个时候有一个优先级的问题。那优先级是什么样的。我们有一个约定。

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

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

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

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

image-20231113213604574

5、基于XML的AOP

在前面内容中,咱们已经完成了基于注解方式实现AOP,通过注解配置了AOP等5种通知类型。那下面我们基于XML的方式实现AOP,因为过程准备工作跟注解一样,所以里边的准备代码我就直接复制。咱们重点写xml配置文件中的具体配置。

5.1 准备工作

image-20231113214912823

然后我们将复制的切面类的注解全部去掉。因为我们要通过XML的方式去实现AOP。

image-20231113215214971

5.2、实现

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

    <!-- 开启注解扫描 -->
    <context:component-scan base-package="com.jie.aop.xmlaop"/>

    <!--配置AOP五种通知类型-->
    <aop:config>
        <!--配置切面类 ref 指定切面类 -->
        <aop:aspect ref="logAspect">
            <!--配置切入点 expression 通过表达式来配置要对哪个方法进行增强-->
            <aop:pointcut id="pointcut" expression="execution(* com.jie.aop.xmlaop.service.impl.CalculatorLogImpl.*(..))"/>
            <!--配置五种通知类型-->
            <!-- 前置通知 -->
            <aop:before method="beforePrintLog" pointcut-ref="pointcut"/>
            <!-- 后置通知 -->
            <aop:after method="afterReturningPrintLog" pointcut-ref="pointcut"/>
            <!-- 返回通知 -->
            <aop:after-returning method="afterPrintLog" returning="result" pointcut-ref="pointcut"/>
            <!-- 异常通知 -->
            <aop:after-throwing method="afterThrowingPrintLog" throwing="ex" pointcut-ref="pointcut"/>
            <!-- 环绕通知 -->
            <aop:around method="aroundPrintLog" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

5.3 测试

ramework.org/schema/aop/spring-aop.xsd">

<!-- 开启注解扫描 -->
<context:component-scan base-package="com.jie.aop.xmlaop"/>

<!--配置AOP五种通知类型-->
<aop:config>
    <!--配置切面类 ref 指定切面类 -->
    <aop:aspect ref="logAspect">
        <!--配置切入点 expression 通过表达式来配置要对哪个方法进行增强-->
        <aop:pointcut id="pointcut" expression="execution(* com.jie.aop.xmlaop.service.impl.CalculatorLogImpl.*(..))"/>
        <!--配置五种通知类型-->
        <!-- 前置通知 -->
        <aop:before method="beforePrintLog" pointcut-ref="pointcut"/>
        <!-- 后置通知 -->
        <aop:after method="afterReturningPrintLog" pointcut-ref="pointcut"/>
        <!-- 返回通知 -->
        <aop:after-returning method="afterPrintLog" returning="result" pointcut-ref="pointcut"/>
        <!-- 异常通知 -->
        <aop:after-throwing method="afterThrowingPrintLog" throwing="ex" pointcut-ref="pointcut"/>
        <!-- 环绕通知 -->
        <aop:around method="aroundPrintLog" pointcut-ref="pointcut"/>
    </aop:aspect>
</aop:config>
```

5.3 测试

image-20231113220944754

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/140914.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

探索向量数据库 | 重新定义数据存储与分析

随着大模型带来的应用需求提升&#xff0c;最近以来多家海外知名向量数据库创业企业传出融资喜讯。 随着AI时代的到来&#xff0c;向量数据库市场空间巨大&#xff0c;目前处于从0-1阶段&#xff0c;预测到2030年&#xff0c;全球向量数据库市场规模有望达到500亿美元&#xff…

CSDN每日一题学习训练——Java版(数据流的中位数、乘积最大子数组、旋转链表)

版本说明 当前版本号[20231113]。 版本修改说明20231113初版 目录 文章目录 版本说明目录数据流的中位数题目解题思路代码思路参考代码 乘积最大子数组题目解题思路代码思路参考代码 旋转链表题目解题思路代码思路参考代码 数据流的中位数 题目 中位数是有序列表中间的数。…

DevChat:开发者专属的基于IDE插件化编程协助工具

DevChat&#xff1a;开发者专属的基于IDE插件化编程协助工具 一、DevChat 的介绍1.1 DevChat 简介1.2 DevChat 优势 二、DevChat 在 VSCode 上的使用2.1 安装 DevChat2.2 注册 DevChat2.3 使用 DevChat 三、DevChat 的实战四、总结 一、DevChat 的介绍 在AI浪潮的席卷下&#x…

国际化:i18n

什么是国际化&#xff1f; 国际化也称作i18n&#xff0c;其来源是英文单词 internationalization的首末字符和n&#xff0c;18为中间的字符数。由于软件发行可能面向多个国家&#xff0c;对于不同国家的用户&#xff0c;软件显示不同语言的过程就是国际化。通常来讲&#xff0…

【BMC】jsnbd介绍

jsnbd介绍 本文主要介绍一个名为jsnbd的开源项目&#xff0c;位于GitHub - openbmc/jsnbd&#xff0c;它实现了一个前端&#xff08;包含HTML和JS文件&#xff09;页面&#xff0c;作为存储服务器&#xff0c;可以指定存储内容&#xff1b;还包含一个后端的代理&#xff0c;这…

【chatglm3】(3):在AutoDL上,使用4090显卡,部署ChatGLM3API服务,并微调AdvertiseGen数据集,完成微调并测试成功!附视频演示。

在AutoDL上&#xff0c;使用4090显卡&#xff0c;部署ChatGLM3API服务&#xff0c;并微调AdvertiseGen数据集&#xff0c;完成微调并测试成功&#xff01; 其他chatgpt 和chatglm3 资料&#xff1a; https://blog.csdn.net/freewebsys/category_12270092.html 视频地址&#…

【C++入门篇】保姆级教程篇【下】

目录 一、运算符重载 1&#xff09;比较、赋值运算符重载 2&#xff09; 流插入留提取运算符重载 二、剩下的默认成员函数 1&#xff09;赋值运算符重载 2&#xff09;const成员函数 3&#xff09;取地址及const取地址操作符重载 三、再谈构造函数 1&#xff09;初始化列表 …

SparkSQL之Analyzed LogicalPlan生成过程

经过AstBuilder的处理&#xff0c;得到了Unresolved LogicalPlan。该逻辑算子树中未被解析的有UnresolvedRelation和UnresolvedAttribute两种对象。Analyzer所起到的主要作用就是将这两种节点或表达式解析成有类型的&#xff08;Typed&#xff09;对象。在此过程中&#xff0c;…

链表相关部分OJ题

&#x1f493;作者简介&#x1f44f;&#xff1a;在校大二迷茫大学生 &#x1f496;个人主页&#x1f389;&#xff1a;小李很执着 &#x1f497;系列专栏&#xff1a;Leetcode经典题 每日分享&#xff1a;人总是在离开一个地方后开始原谅它❣️❣️❣️———————————…

“第六十七天”

各位&#xff0c;昨天查找子串的方法想起来了&#xff0c;就是那个KMP算法......自己理解都有点困难&#xff0c;还看看能不能想一下&#xff0c;确实很困难啊。 不要忘了toupper函数和tolower函数不是直接改变字符的大小写&#xff0c;而是返回对应的大小写的值&#xff0c;需…

pytest-bdd快速示例和问题解决

BDD 与 pytest-bdd BDD 即 Behavior-driven development&#xff0c;行为驱动开发。BDD行为驱动是一种敏捷开发模式, 重点在于消除开发/测试对需求了解的歧义及用户场景的验证。 pytest-bdd 是一个BDD测试框架&#xff0c;类似于behave, cucumber。它可以统一单元测试和功能测…

【Git】第四篇:基本操作(理解工作区、暂存区、版本库)

Git 工作区、暂存区和版本库 工作区&#xff1a;就是我们创建的本地仓库所在的目录暂存区&#xff1a; stage或index&#xff0c;一般放在.git(可隐藏文件)目录下的index文件&#xff08;.git/index&#xff09;中&#xff0c;所以我们把暂存区有时候也叫做索引&#xff08;in…

飞书开发学习笔记(五)-Python快速开发网页应用

飞书开发学习笔记(五)-Python快速开发网页应用 一.下载示例代码 首先进入飞书开放平台: https://open.feishu.cn/app 凭证与基础信息 页面&#xff0c;在 应用凭证 中获取 App ID 和 App Secret 值。 教程和示例代码位置:https://open.feishu.cn/document/home/integrating-…

C语言 每日一题 牛客网 11.13 Day17

找零 Z国的货币系统包含面值1元、4元、16元、64元共计4种硬币&#xff0c;以及面值1024元的纸币。 现在小Y使用1024元的纸币购买了一件价值为N(0 < N≤1024)的商品&#xff0c;请问最少他会收到多少硬币&#xff1f; 思路 运用if语句进行判断分类 代码实现 int main() {…

基于php+thinkphp的网上书店购物商城系统

运行环境 开发语言&#xff1a;PHP 数据库:MYSQL数据库 应用服务:apache服务器 使用框架:ThinkPHPvue 开发工具:VScode/Dreamweaver/PhpStorm等均可 项目简介 系统主要分为管理员和用户二部分&#xff0c;管理员主要功能包括&#xff1a;首页、个人中心、用户管理、图书分类…

jupyter lab常用插件集合

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

毕业设计项目:基于java+springboot的共享单车信息网站

运行环境 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Ma…

【Java 进阶篇】JQuery DOM操作:通用属性操作的绝妙魔法

在前端的舞台上&#xff0c;JQuery犹如一位魔法师&#xff0c;为我们展现了操纵HTML元素的奇妙技巧。而在这个技巧的精妙组成中&#xff0c;通用属性操作是一门绝妙的魔法。在本篇博客中&#xff0c;我们将深入研究JQuery DOM操作中的通用属性操作&#xff0c;揭示这段魔法的神…