Bootstrap

【SSM详细教程】-14-SpringAop超详细讲解

 精品专题:

01.《C语言从不挂科到高绩点》课程详细笔记

https://blog.csdn.net/yueyehuguang/category_12753294.html?spm=1001.2014.3001.5482

02. 《SpringBoot详细教程》课程详细笔记

https://blog.csdn.net/yueyehuguang/category_12789841.html?spm=1001.2014.3001.5482

03.《SpringBoot电脑商城项目》课程详细笔记

https://blog.csdn.net/yueyehuguang/category_12752883.html?spm=1001.2014.3001.5482

04.《VUE3.0 核心教程》课程详细笔记 

https://blog.csdn.net/yueyehuguang/category_12769996.html?spm=1001.2014.3001.5482

05. 《SSM详细教程》课程详细笔记 

https://blog.csdn.net/yueyehuguang/category_12806942.html?spm=1001.2014.3001.5482

================================

||     持续分享系列教程,关注一下不迷路 ||

||                视频教程:墨轩大楼               ||

================================

📚 AOP 概念及优点

AOP为Aspect Oriented Programming的缩写,被称为面向切面编程。

AOP 主要用于处理共通逻辑,例如:记录日志、性能统计、安全控制、事务处理、异常处理等等。AOP可以将这些共通的逻辑从普通业务逻辑代码中分离出来,这样在日后修改这些逻辑的时候,就不会影响普通业务逻辑的代码。

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

AOP 、OOP在名字上虽然非常类似,但却是面向不同领域的两种设计思想。OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象,以获得更加清晰高效的逻辑单元划分。AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。

AOP 需要以 OOP为前提和基础。

🌾 什么是方面

面向切面编程,我们首先要知道的一个概念就是方面,也就是把什么东西给隔离出来。方面是指封装处理共通业务的组件,该组件被作用到其他目标组件方法上。

🌾 什么是目标

目标是指被一个或多个方面所作用的对象。

🌾 什么是切入点

切入点是用于指定哪些组件和方法使用方面功能,在Spring中利用一个表达式指定切入目标。

Spring提供了以下常用的切入点表达式:

  • 方法限定表达式

execution(修饰符?返回类型 方法名(参数) throws 异常类型?)

  • 类型限定表达式

within(包名.类型)

  • Bean 名称限定表达式

bean("Bean的id或name属性值")

🌾 什么是通知

通知是用于指定方面组件和目标组件作用的时机,例如方面功能在目标方法之前或之后执行等时机。

Spring框架提供以下几种类型的通知:

  • 前置通知:先执行方面功能在执行目标功能
  • 后置通知:先执行目标功能再执行方面功能(目标无异常才执行方面)
  • 最终通知:先执行目标功能再执行方面功能(目标有无异常都执行方面)
  • 异常通知:先执行目标,抛出后执行方面
  • 环绕通知:先执行方面前置部分,然后执行目标,最后再执行方面后置部分。

Spring框架提供5种通知,可以按照下面的try-catch-finally结构理解。

try{
    // 前置通知--执行方面
    // 环绕通知--前置部分
    // 执行目标组件方法
    // 环绕通知--后置部分
    // 后置通知--执行方面
}catch{
    // 异常通知--执行方面
}finally{
    // 最终通知--执行方面
}

🌾 AOP 实现原理

Spring AOP 实现主要是基于动态代理技术。当Spring采用AOP配置后,Spring容器返回的目标对象,实质上是Spring利用动态代理技术生成的一个代理类型。代理类重写了原目标组件方法的功能,在代理类种调用方面对象功能和目标对象功能。

Spring框架采用了两种动态代理实现:

  • 利用cglib工具包:目标没有接口时采用此方法,代理类是利用继承方法生成一个目标子类。
  • 利用JDK Proxy API:目标有接口时采用此方法,代理类是采用实现目标接口方法生成一个类。

📚 AOP 开发案例

🌾 AOP 前置通知案例

👉 需求:使用Spring AOP 前置通知,在访问Controller中每个方法前,记录用户的操作日志。

👉 步骤:

  • 创建方面组件
  • 声明方面组件
  • 将方面组件作用到目标组件上

🍒 导入依赖

我们基于前面SpringMVC的基础上去添加AOP功能,所以在前面SpringMVC的环境基础上我们需要追加AOP的依赖。


<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.3.8</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.8.10</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.8.10</version>
</dependency>

🍒 创建Controller

创建一个AOPTestContrller,模拟查询用户数据的Controller,代码如下:

@Controller
public class AOPTestController {

    @RequestMapping("/find")
    @ResponseBody
    public String findUser(){
        // 模拟查询用户数据
        System.out.println("--》 查询用户数据");
        return "查询了用户数据";
    }
}
🍒 创建方面组件

创建方面组件OperateLogger,并在该类中创建记录用户操作日志的方法,代码如下:

package com.moxuan.mvc_study.config;

import org.aspectj.lang.ProceedingJoinPoint;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 用于记录日志的方面组件,演示Spring AOP 的各种通知类型
 */
public class OperateLogger {

    /**
     * 前置通知、后置通知、最终通知使用的方法
     */
     public void logUser(JoinPoint p){

        // 目标组件的类名
        String className = p.getTarget().getClass().getName();
        // 调用的方法名
        String method = p.getSignature().getName();
        // 当前系统时间
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        // 拼日志信息
        String msg = "--> 用户在"+time+",执行了"+className+"."+method+"()";
        // 记录日志
        System.out.println(msg);
    }
}
🍒 声明方面组件

在springmvc.xml中,声明该方面组件,关键代码如下:

<bean id="operateLogger" class="com.moxuan.mvc_study.config.OperateLogger"></bean>
🍒 将方面组件作用到目标组件上

在springmvc.xml中,将声明的方面组件作用到controller包下面所有类的所有方法上,关键代码如下:

<aop:config>
  <aop:aspect ref="operateLogger">
    <!--配置方面组件,作用到的目标方法,
    pointcut 方面组件的切入点
    -->
    <aop:before method="logUser"
      pointcut="within(com.moxuan.mvc_study.controller..*)" />
  </aop:aspect>
</aop:config>

🍒 测试效果

发送请求:http://localhost:8080/find

可以看到,当配置<aop:before> 前置通知后,方面组件会在执行目标组件的方法时自动触发执行。

🌾 AOP 环绕通知案例

🍒 创建方面组件

依赖和控制器我们延用前置通知案例中的,我们来修改一下方面组件:

public Object logUserRound(ProceedingJoinPoint p) throws Throwable{
        // 目标组件的类名
        String className = p.getTarget().getClass().getName();
        // 调用的方法名
        String method = p.getSignature().getName();
        // 当前系统时间
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        // 拼日志信息
        String msg = "--> 用户在"+time+",执行了"+className+"."+method+"()";
        // 记录日志
        System.out.println(msg);

        // 执行目标组件的方法
        Object obj = p.proceed();

        //在调用目标组件业务方法后也可以做一些业务处理
        System.out.println("---> 已经执行完毕了组件业务了....");
        return obj;

    }

🍒 配置环绕通知

组件声明我们前面已经做过了,这里我们直接配置一下环绕通知:

    <aop:config>
        <aop:aspect ref="operateLogger">
            <!--配置方面组件,作用到的目标方法,
                pointcut 方面组件的切入点
            -->
            <aop:around method="logUserRound"
                        pointcut="within(com.moxuan.mvc_study.controller..*)"/>
        </aop:aspect>
    </aop:config>

🍒 测试效果

请求地址:http://localhost:8080/find

可以看到,方面组件中的前置部分会在方法执行前执行,方法执行完毕之后执行后置部分。

🌾 AOP 异常通知案例

需求:使用AOP异常通知,在每个Controller的方法发生异常时,记录异常日志。

🍒 编写方面组件
/**
     * 异常通知使用方法
     * @param e
     */
public void logException(Exception e){
	StackTraceElement[] elements = e.getStackTrace();
	// 将异常信息记录
	System.out.println("--》"+elements[0].toString());
}
🍒 配置异常通知

将异常通知方面组件作用到目标组件上

    <aop:config>
        <aop:aspect ref="operateLogger">
     
            <aop:after-throwing method="logException" throwing="e"
                            pointcut="within(com.moxuan.mvc_study.controller..*)"/>
        </aop:aspect>
    </aop:config>
🍒 编写目标组件
@RequestMapping("/find")
@ResponseBody
public String findUser(){
    // 模拟查询用户数据
    System.out.println("目标组件:--》 查询用户数据");
    // 制造一个异常,便于测试异常通知
    Integer.valueOf("abc");
    return "查询了用户数据";
}
🍒 测试效果

发送请求:http://localhost:8080/find

🌾 AOP 注解使用案例

👉需求: 使用Spring AOP 注解替代XML配置,重构上面三个案例

👉方案:

  • @Aspect : 用于声明方面组件
  • @Before:用于声明前置通知
  • @AfterReturning:用于声明后置通知
  • @After:用于声明最终通知
  • @Around:用于声明环绕通知
  • @AfterThrowing:用于声明异常通知

🍒 开启AOP注解扫描

在springmvc.xml中,去掉方面组件声明以及作用的xml配置,并开启AOP注解扫描,关键代码如下:

<!-- 开启AOP注解扫描-->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
🍒 使用注解声明方面组件

在OperateLogger中使用@Aspect注解声明方面组件,并分别用@Before、@Around、@AfterThrowing注解声明三个方法,将方面组件作用到目标组件上,代码如下:

package com.moxuan.mvc_study.config;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 用于记录日志的方面组件,演示Spring AOP 的各种通知类型
 */
@Component
@Aspect
public class OperateLogger {

    /**
     * 前置通知、后置通知、最终通知使用的方法
     */
    @Before("within(com.moxuan.mvc_study.controller..*)")
    public void logUser(JoinPoint p){
        System.out.println("^^^^^进入到了前置通知^^^^^^");
        // 目标组件的类名
        String className = p.getTarget().getClass().getName();
        // 调用的方法名
        String method = p.getSignature().getName();
        // 当前系统时间
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        // 拼日志信息
        String msg = "--> 用户在"+time+",执行了"+className+"."+method+"()";
        // 记录日志
        System.out.println(msg);
        System.out.println("^^^^^前置通知结束^^^^^^");
    }


    @Around("within(com.moxuan.mvc_study.controller..*)")
    public Object logUserRound(ProceedingJoinPoint p) throws Throwable{
        System.out.println("^^^^^进入环绕通知^^^^^^");
        // 目标组件的类名
        String className = p.getTarget().getClass().getName();
        // 调用的方法名
        String method = p.getSignature().getName();
        // 当前系统时间
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        // 拼日志信息
        String msg = "--> 用户在"+time+",执行了"+className+"."+method+"()";
        // 记录日志
        System.out.println(msg);

        // 执行目标组件的方法
        Object obj = p.proceed();

        //在调用目标组件业务方法后也可以做一些业务处理
        System.out.println("---> 已经执行完毕了组件业务了....");
        System.out.println("^^^^^环绕通知结束^^^^^^");
        return obj;

    }

    /**
     * 异常通知使用方法
     * @param e
     */
    @AfterThrowing(pointcut = "within(com.moxuan.mvc_study.controller..*)",throwing ="e")
    public void logException(Exception e){
        System.out.println("^^^^^进入异常通知^^^^^^");
        StackTraceElement[] elements = e.getStackTrace();
        // 将异常信息记录
        System.out.println("--》"+elements[0].toString());
        System.out.println("^^^^^异常通知结束^^^^^^");
    }
}
🍒 测试效果

请求路径:http://localhost:8080/find

从结果可以看到,当发生异常之后,环绕通知后置部分将不会执行。

;