Java EE 突击 14 - Spring AOP

这个专栏给大家介绍一下 Java 家族的核心产品 - SSM 框架
JavaEE 进阶专栏

Java 语言能走到现在 , 仍然屹立不衰的原因 , 有一部分就是因为 SSM 框架的存在

接下来 , 博主会带大家了解一下 Spring、Spring Boot、Spring MVC、MyBatis 相关知识点

并且带领大家进行环境的配置 , 让大家真正用好框架、学懂框架

来上一篇文章复习一下吧
点击即可跳转到前置文章
CSDN 平台观感有限 , 可以私聊作者获取源笔记链接
在这里插入图片描述

一 . 什么是 Spring AOP ?

Spring AOP = Spring + AOP
Spring 我们已经知道了 , Spring 是一个包含了众多工具和方法的 IoC 容器
那么这个 AOP 是什么呢 ?
我们之前学习过 OOP ( 面向对象编程 ) , 那这个 AOP 与 OOP 有什么关联呢 ?
AOP ( Aspect Oriented Programming ) : 面向切面编程 , 所谓的切面就是针对某一方面的功能进行处理 , 是一种通过分离功能性需求和横切关注点的方式 , 将特定功能从业务逻辑中独立出来的编程思想

比如说 : 用户的登录授权功能
这个功能是一类功能 , 在发布文章的时候要检验用户是否登录 , 在评论文章的时候要检验用户是否登录 , 在用户删除文章的时候要检验用户是否登录 …
在进行这些需要验证用户登录的操作的时候 , 我们都需要做某件事 , 那么我们就可以把这件事提取出来 , 做统一处理 , 这个功能我们就可以用 AOP 来完成
这里就不能用 OOP 处理了 , 这种高度抽象的事情我们就需要用 AOP 处理了 , 但是还离不开 OOP 思想
AOP 就相当于对 OOP 的补充
对于一样功能 , 我们可以分为好几个阶段 :

  1. 开发初期阶段 : 所有的方法都去实现一遍 , 比如添加文章、评论文章、删除文章 , 我们都需要各自实现一遍 , 同样的代码写了三遍 , 封装性不太好
  2. 开发中期阶段 : 封装成公共的方法 , 在需要的位置进行调用 . 但是我们仍然需要调用这个方法 , 这个方法与业务无关 , 这就造成了我们代码仍然是比较冗余 , 混杂进去了许多与业务无关的代码 , 并未实现代码的高度业务化
  3. 开发高级阶段 : 使用 AOP ( 拦截器 / 过滤器 ) 对某个功能做统一的处理 , 在业务代码中就不再需要去混杂非业务代码 ( 比如 : 登录状态校验 ) 了

举个栗子 :

开发中期阶段 : 火车站有 5 个站台 , 每个站台列车员都要检查违禁物品
开发高级阶段 : 火车站虽然有 5 个站台 , 但是违禁物品我们在进火车站的时候就统一检验了

使用 AOP , 更加实现了代码的解耦合

开发中期阶段 : 我们安全校验的函数需要传入两个参数 , 但是某一天业务变了 , 需要传入三个参数 , 因为我们还在业务代码中调用安全校验函数 , 所以业务代码中函数调用的部分也需要更改参数个数
开发高级阶段 : 我们把安全校验的函数统一处理 , 如果发生参数个数的改变 , 也与业务代码无关 , 实现了代码的解耦合

不使用 AOP , 我们的程序也能正常实现 , 但是一旦出现问题 , 我们的代码可维护性是非常差的 .
AOP 还可以实现 :

  • 统⼀日志记录
  • 统⼀方法执行时间统计
  • 统⼀的返回格式设置

code : 返回错误码
message : 返回错误信息
data : 返回数据

  • 统⼀的异常处理
  • 声明型事务的开启和提交等

也就是说使用 AOP 可以扩充多个对象的某个能力 , 所以 AOP 可以说是 OOP ( Object Oriented Programming , 面向对象编程 ) 的补充和完善。
AOP 是一种思想 , Spring AOP 就是 AOP 思想的一种具体实现 ( 类似于 IoC 与 DI )

二 . AOP 基础组成

2.1 切面 (Aspect)

切面指的是当前 AOP 的作用 , 我们可以笼统的认为当前的 AOP 是针对谁的
切面可以看做是一个与业务逻辑无关 , 但对多个对象产生影响的模块化单元
比如 : 用户登录的判断 , 这就是一个切面 . 我还可以设计其他切面 , 比如 : 记录日志
这是一个很大的概念
切面由切点 ( Pointcut ) 和通知 ( Advice , 也叫做增强 ) 组成 . 它既包含了横切逻辑的定义 , 也包括了连接点的定义

2.2 连接点 (Join Point)

应用程序执行过程中 , 能够插入切面的一个点 , 就叫做连接点
举个栗子 : 我现在提供了一个功能 , 程序中有哪些位置需要这个功能 , 在哪些位置需要调用 AOP , 就称为连接点

2.3 切点 (Pointcut)

切点的作用 : 提供一组规则 , 用来匹配通知的
切面就是定义了哪件事需要重复调用 , 比如登录检查 , 切点就是制定我们拦截的规则
比如 : 注册是不需要进行登录检查的 , 因为我都没登录 , 如果注册阶段就进行登录检查的话 , 那这个账号就永远不会被注册成功 .

2.4 通知 (Advice)

通知就是具体要执行的动作 , 比如 : 借助切点 , 我们把某项操作拦截下来 , 但是拦截下来我们要干嘛呢
通知分为 5 种 :

  • 前置通知使用 @Before : 通知方法会在目标方法调用之前执行

执行某个业务 , 执行之前先执行前置通知方法 (前置增强方法)

  • 后置通知使用 @After : 通知方法会在目标方法返回或者抛出异常后调用

执行完这个业务之后 , 我再执行后置通知方法

  • 返回之后通知使用 @AfterReturning : 通知方法会在目标方法返回后调用

在 return 之后 , 再通知一下

  • 抛异常后通知使用 @AfterThrowing : 通知方法会在目标方法抛出异常后调用

在抛出异常之后 , 进行通知

  • 环绕通知使用 @Around : 通知包裹了被通知的方法 , 在被通知的方法通知之前和调⽤之后执行自定义的行为

在被通知的方法执行之前或者执行之后执行的通知 , 叫做环绕通知


总结一下 : AOP 基础组成

  1. 切面 : 定义 AOP 业务类型的 (当前 AOP 是干嘛的)
  2. 连接点 : 有可能调用 AOP 的地方就叫做一个连接点
  3. 切点 : 定义 AOP 拦截规则
  4. 通知 / 增强方法 : 定义什么时候干什么事的
    1. 前置通知 : 拦截的目标方法之前执行的通知
    2. 后置通知 : 拦截的目标方法之后执行的通知
    3. 返回之前通知 : 拦截的目标方法返回数据之后通知
    4. 抛出异常之后的通知 : 拦截的目标方法抛出异常之后执行的通知
    5. 环绕通知 : 在拦截方法执行前后都执行的通知

可以再通过这个图片再理解一下
image.png

三 . 实现 Spring AOP

3.1 添加 Spring AOP 框架支持

我们不新创建项目了 , 用之前 MyBatis 的项目即可
把这段内容复制到 pom.xml 中

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

image.png

3.2 定义切面和切点

之前我们编写的登录功能 , 我们就不在每个业务里面去写了 , 我直接就定义一个切面 , 我们执行业务代码的时候 , 他就会先去执行 AOP , 我们就不需要每个地方都去写登录功能了
我们在 demo 包底下新建一个 aop 包 , 再新建一个类 , 叫做 LoginAOP
image.png
image.png
声明切面的注解是 @Aspect , 声明切点的注解是 @Pointcut
然后编写以下代码

package com.example.demo.aop;

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

/**
 * 登录的 AOP 实现代码
 */
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
    // 定义切点(拦截的规则)
    // 括号里面内容先不用管
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    // 写一个返回值为void的空方法,方法名无所谓
    public void pointcut() {
    }
}

image.png

3.3 定义通知

前置通知

前置通知使用 @Before 注解

package com.example.demo.aop;

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

/**
 * 登录的 AOP 实现代码
 */
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
    // 定义切点(拦截的规则)
    // 括号里面内容先不用管
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    // 写一个返回值为void的空方法,方法名无所谓
    public void pointcut() {
    }

    // 前置通知
    // 在调用 UserController 里面的方法之前,先执行的方法
    @Before("pointcut()")
    public void before() {
        System.out.println("执行了前置通知");
    }
}

image.png
我们启动程序 , 通过浏览器查看效果
image.png
image.png
运行之后
image.png
image.png


那么我们把这里改成 TestController
image.png
但是我们访问的是 UserController 里面的方法 , 前置通知还能被打印吗
image.png
编写以下代码

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
    @RequestMapping("hi")
    public String sayHi() {
        return "hi";
    }
}

接下来 , 我们去浏览器里面访问 127.0.0.1:8080/user/getall, 看一看效果
image.png
image.png
那我们访问 127.0.0.1:8080/hi呢 ?
image.png
image.png
把 TestController 改回来

后置通知

后置通知实现 @After 注解

package com.example.demo.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 登录的 AOP 实现代码
 */
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
    // 定义切点(拦截的规则)
    // 括号里面内容先不用管
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    // 写一个返回值为void的空方法,方法名无所谓
    public void pointcut() {
    }

    // 前置通知
    // 在调用 UserController 里面的方法之前,先执行的方法
    @Before("pointcut()")
    public void before() {
        System.out.println("执行了前置通知");
    }

    // 后置通知
    @After("pointcut()")
    public void after() {
        // 后置通知实现的具体业务代码
        System.out.println("执行了后置通知");
    }
}

在浏览器查看一下效果
image.png
image.png

返回通知

使用注解 @AfterReturning

package com.example.demo.aop;

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

/**
 * 登录的 AOP 实现代码
 */
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
    // 定义切点(拦截的规则)
    // 括号里面内容先不用管
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    // 写一个返回值为void的空方法,方法名无所谓
    public void pointcut() {
    }

    // 前置通知
    // 在调用 UserController 里面的方法之前,先执行的方法
    @Before("pointcut()")
    public void before() {
        System.out.println("执行了前置通知");
    }

    // 后置通知
    @After("pointcut()")
    public void after() {
        // 后置通知实现的具体业务代码
        System.out.println("执行了后置通知");
    }

    // 返回通知
    @AfterReturning("pointcut()")
    public void afterReturning() {
        // 返回通知实现的具体业务代码
        System.out.println("执行了返回通知");
    }
}

image.png

异常通知

使用注解 @AfterThrowing

package com.example.demo.aop;

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

/**
 * 登录的 AOP 实现代码
 */
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
    // 定义切点(拦截的规则)
    // 括号里面内容先不用管
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    // 写一个返回值为void的空方法,方法名无所谓
    public void pointcut() {
    }

    // 前置通知
    // 在调用 UserController 里面的方法之前,先执行的方法
    @Before("pointcut()")
    public void before() {
        System.out.println("执行了前置通知");
    }

    // 后置通知
    @After("pointcut()")
    public void after() {
        // 后置通知实现的具体业务代码
        System.out.println("执行了后置通知");
    }

    // 返回通知
    @AfterReturning("pointcut()")
    public void afterReturning() {
        // 返回通知实现的具体业务代码
        System.out.println("执行了返回通知");
    }

    // 异常通知
    @AfterThrowing("pointcut()")
    public void afterThrowing() {
        // 异常通知实现的具体业务代码
        System.out.println("执行了异常通知");
    }
}

image.png

环绕通知

使用注解 @Around
注解部分 : @Around(“pointcut()”) 同上
返回值 : Object , 代表目标方法在执行之后 , 把生成的对象再返回给 Spring 框架 , 因为 Spring 框架执行完一个方法之后 , 可能还会去执行后续操作 , 比如 : 释放资源… , 所以他需要拿到这个对象
固定参数 : ProceedingJoinPoint joinPoint , 代表正在执行的目标方法

Proceeding : 正在加工的
JoinPoint : 连接点
正在加工的连接点 -> 要去拦截的目标方法 -> 把目标方法拦截之后 , 变成了 ProceedingJoinPoint 的对象

接下来 , 我们自己去实现一个环绕通知

package com.example.demo.aop;

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

/**
 * 登录的 AOP 实现代码
 */
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
    // 定义切点(拦截的规则)
    // 括号里面内容先不用管
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    // 写一个返回值为void的空方法,方法名无所谓
    public void pointcut() {
    }

    // 前置通知
    // 在调用 UserController 里面的方法之前,先执行的方法
    @Before("pointcut()")
    public void before() {
        System.out.println("执行了前置通知");
    }

    // 后置通知
    @After("pointcut()")
    public void after() {
        // 后置通知实现的具体业务代码
        System.out.println("执行了后置通知");
    }

    // 返回通知
    @AfterReturning("pointcut()")
    public void afterReturning() {
        // 返回通知实现的具体业务代码
        System.out.println("执行了返回通知");
    }

    // 异常通知
    @AfterThrowing("pointcut()")
    public void afterThrowing() {
        // 异常通知实现的具体业务代码
        System.out.println("执行了异常通知");
    }

    // 环绕通知
    // 注解部分:@Around("pointcut()") 同上
    // 返回值:Object,代表目标方法在执行之后,把生成的对象再返回给 Spring 框架,因为 Spring 框架执行完一个方法之后,还会去执行后续操作,比如:释放资源...,所以他需要拿到这个对象
    // 固定参数:ProceedingJoinPoint joinPoint
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object result = null;
        // 前置业务代码
        System.out.println("环绕通知的前置执行方法");
        // 执行目标方法
        try { 
            // 实际调用的是 UserController 里面的 getUsers 方法
            result = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        // 后置业务代码
        System.out.println("环绕通知的后置执行方法");

        return result;
    }
}

image.png
我们可以猜一下 : 环绕通知出现在前置通知的前还是后 , 环绕通知出现在后置通知的前还是后呢 ?
那么我们运行一下
image.png
image.png
可以发现 , 环绕通知执行顺序比前置通知还要早 , 执行顺序比后置通知还要晚 , 相当于龙头蛇尾 , 全给霸占上了 .
环绕通知最经典的用途就是记录方法执行时间
image.png

3.4 AOP 表达式

固定写法 :
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
image.png

四 . AOP 的实现原理

使用了 AOP 之后 , 我们就实现了一种特定的功能
比如 : 我们的登录验证 , 需要调用所有登录验证的位置 , 都不需要再关注登录验证这个事了 , 因为我们的 AOP 类里面已经做了相关处理了 , 并且我们也指定了拦截路径
image.png
这就代表我们拦截的话 , 就拦截 UserController 文件夹下面所有的方法 , 只要是需要登录验证的功能 , 全部写在 UserController 文件夹下 , 这样就可以实现登录拦截了 , 并且我们写业务的时候也不用关注登录验证功能了 .
那么 AOP 确实好用 , 但是 AOP 是咋回事呢 ?

4.1 AOP 原理

Spring AOP 是构建在动态代理基础上的 , 因此 Spring 对 AOP 的支持局限于方法级别的拦截
那么什么叫动态代理呢 ?

动态代理 静态代理

我们之前在讲 Fiddler 的时候 , 提到过代理

代理就可以理解为代购 , 我们想要去买海外商品就需要通过代购来帮我们购买

在程序执行期间生成的代理 , 就叫做动态代理

代理分为 : 静态代理、动态代理
以新年放烟花举例
静态代理指的是 : 一年四季一直卖烟花的人 , 实际上就是在程序还没执行之前就产生的代理
动态代理指的是 : 赶上正月十五放烟花的人多 , 有的人就进点炮去广场卖 , 卖几天就完事 , 这就指的是在程序运行期间生成的代理

image.png
动态代理也是一种思想 , 他在 Spring AOC 中的具体实现就是 JDK Proxy 和 CGLIB 方式

JDK Proxy 是 Java 官方提供给我们的动态代理
CGLIB 是第三方的动态代理
具体什么时候使用 JDK Proxy , 什么时候使用 CGLIB , 这要看我们的业务场景
假如说你的目标对象实现了接口或者实现了接口的子类 , 那么他就会使用 JDK Proxy
目标对象没有实现接口 , 只是一个普通的类 , 他就会使用 CGLIB

JDK 动态代理实现(了解)

不用具体了解 , 看看即可

package com.example.demo;

import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被代理类必须实现接⼝

public class PayServiceJDKInvocationHandler implements InvocationHandler {

    //⽬标对象即就是被代理对象
    private Object target;

    public PayServiceJDKInvocationHandler(Object target) {
        this.target = target;
    }

    //proxy代理对象
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        //2.记录⽇志
        System.out.println("记录⽇志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //通过反射调⽤被代理类的⽅法
        Object retVal = method.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }

    public static void main(String[] args) {
        PayService target = new AliPayService();
        //⽅法调⽤处理器
        InvocationHandler handler =
                new PayServiceJDKInvocationHandler(target);
        //创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
        PayService proxy = (PayService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{PayService.class},
                handler
        );
        proxy.pay();
    }
}

image.png

CGLIB 动态代理实现(了解)
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;

import java.lang.reflect.Method;

public class PayServiceCGLIBInterceptor implements MethodInterceptor {
    //被代理对象
    private Object target;

    public PayServiceCGLIBInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args,
                            MethodProxy methodProxy) throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        //2.记录⽇志
        System.out.println("记录⽇志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //通过cglib的代理⽅法调⽤
        Object retVal = methodProxy.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }

    public static void main(String[] args) {
        PayService target = new AliPayService();
        PayService proxy = (PayService)
                Enhancer.create(target.getClass(), new
                        PayServiceCGLIBInterceptor(target));
        proxy.pay();
    }
}

也是要实现接口 , 重写里面的 invoke 方法
创建对象也不能去 new , 需要通过其他方式

JDK 和 CGLB 实现的区别(面试题)
  1. JDK 实现 , 要求被代理类必须实现接口 , 之后是通过 InvocationHandler 及 Proxy , 在运行时动态的在内存中生成了代理类对象 , 该代理对象是通过实现同样的接口实现 ( 类似静态代理接口实现的方式 ) , 只是该代理类是在运行期时 , 动态的织入统⼀的业务逻辑字节码来完成
  2. CGLIB 实现 , 被代理类可以不实现接口 , 是通过继承被代理类,在运行时动态的生成代理类对象
  3. CGLIB 实现要比 JDK 实现 性能要高

织入 (Weaving) : 代理的生成时机

织入就是 代理生成的时机
代理生成的时机分为三类 :

  1. 编译期 : 编译时期生成的代理 (运行之前)
  2. 类加载期 : 类加载的时候 , 生成代理 (预运行阶段)
  3. 运行期 : 运行代码的时候生成的代理对象

动态代理生成在代码运行期 , 动态织入到字节码

总结

  1. AOP 是什么 : AOP 是面向切面编程 , 他是对某一方面的功能去进行处理
  2. Spring AOP 是对 AOP 思想的具体实现

分为 : JDK 和 CGLB 两种功能去实现的

  1. 我们自己写的 Spring AOP , 是通过注解实现的 , 而这些注解 , 他来自于 ASpectJ , 而 AspectJ 又是来自于 Spring AOP 的

image.png
如果我们把 import 删除掉
image.png

  1. Spring AOP 实现步骤 :
    1. 添加 AOP 框架支持
    2. 定义切面和切点
    3. 定义通知 : 分为五种
  2. Spring AOP 是通过动态代理的方式 , 在运行期将 AOP 代码织入到程序中的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加勒比海涛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值