【Spring 】了解Spring AOP

目录

[一、什么是Spring AOP](#一、什么是Spring AOP)

二、AOP的使用场景

三、AOP组成

[四、Spring AOP的实现](#四、Spring AOP的实现)

[1、添加Spring AOP依赖](#1、添加Spring AOP依赖)

2、定义切面和切点

3、定义相关通知

[五、 AOP的实现原理](#五、 AOP的实现原理)

1、什么是动态代理

[2、 JDK代理和CGLIB代理的区别](#2、 JDK代理和CGLIB代理的区别)


一、什么是Spring AOP

AOP(Aspect Oriented Programming),直译过来就是面向切面编程,AOP是一种编程思想,是面向对象编程(OOP)的一种补充。Spring AOP是AOP思想的一种实现,就像DI一样是IoC的一种实现。

AOP的主要作用就是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点,减少对业务代码的侵入。增强代码的可读性和可维护性。简单来所,AOP的作用就是保证开发者在不修改业务代码的前提下,位系统中的业务组件添加某种通用功能。

就比如实现一个用户登录权限的校验功能,就比如我们使用的博客,在要进入博客编辑的页面时,需要对你是否登录进行校验,如果已经登录,那么就可以进入编辑页,如果没有那么就需要在登录页面登录之后在进入。像这样需要登录校验的页面,我们使用AOP思想,只需要在某一处配置以下,所有的需要判断用户登录的页面就可以实现用户登录验证了。这样每个页面就只关注具体的业务逻辑了。

二、AOP的使用场景

就像上面举的例子,当你的程序中实现的页面越来越多,那么你要 写的登录验证也越来越多,⽽这些⽅法⼜是相同的,这么多的⽅法就会代码修改和维护的成本我们对这种功能统一,并且使用地方较多的功能,就可以考虑使用AOP来统一处理。当然AOP可以使用的场景还有很多。

  • 统一日志记录
  • 统一方法执行时间统计
  • 统一的返回格式设置
  • 统一的异常处理
  • 事务的开启和提交等

如果没有使用AOP思想来写代码,用户发送的请求直接被业务代码控制层接收到,请求访问的页面来校验用户是否登录。使用了AOP思想的代码,用户发送方的请求被AOP这里的代码先进行登录校验,如果登录,将请求传给控制层,如果没有登录就会被拦截。

三、AOP组成

1️⃣切面(Aspect):表示当前AOP是针对那些事件做处理的,用来登录的还是记录日志的。切面就是通知和切点的结合,通知和切点共同定义了切面的全部内容,他是干什么的,什么时候在哪里执行。通常以类的形式表示。

2️⃣切点(Pointcut):表示定义具体规则。切点其实就是筛选出的连接点,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点作为切点,如果说通知定义了切面的动作后者执行时机的话,切点则定义了执行的地点。

3️⃣通知(Advice):AOP执行的具体方法。有的地方叫增强。

4️⃣连接点(Join point):就是有可能触发切点的所有点。应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,异常抛出时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

四、Spring AOP的实现

1、添加Spring AOP依赖

在创建好的Spring Boot项目的pom.xml中添加Spring AOP的依赖,我们可以从中央仓库中下载

XML 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后点击刷新,触发下载。

2、定义切面和切点

这里使用注解@Aspect表示定义切面,即UserAserAspect类为切面,使用@Component注解表示让切面随着框架的启动而启动,这样切面中的切点定义的拦截规则才能生效。

java 复制代码
package com.example.demo.common;

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

@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
    //定义切点,@Pointcut注解的参数中定义了具体的拦截规则。参数中使用AspectJ表达式语法
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void pointcut(){}
}

上述代码中,pointcut方法为空方法,它不需要又方法体,此方法名就是起到一个"标识"的作用,标识下面的通知方法具体指的是那个切点。因为一个切面中有很多切点。

上述pointcut方法上添加的**@Pointcut注解的参数** 中使用切点表达式定义了具体的拦截规则

java 复制代码
execution(* com.example.demo.controller.UserController.*(..))

切点表达的意思是 :拦截UserContrller类中的所有方法其参数为任意参数并且返回值为任意类型的返回值。

  • execution表示的意思为执行,执行的是后面跟的()中的规则。
  • *表示的多个部分组成的,有修饰符和返回值类型。
  • com.example.demo.controller.UserController表示要拦截com.example.demo.controller包中的UserController类
  • 类后面跟的*表示UserController类中的所有方法。
  • ..表示的不定式传参

切点表达式由切点函数组成,其中execution()是最常见的切点函数用来匹配方法,语法为:

execution(<修饰符><返回值类型><包.类.方法(参数)><异常>)

常见表达式示例

  • execution(* com.example.demo.User.*(..)):匹配User类中的所有方法。
  • execution(* com.example.demo.User+.*(..)):匹配该类的子类包括该类的所有方法
  • execution(* com.example.*.*(..)):匹配com.example包下的所有类的所有方法
  • execution(* com.example..*.*(..)):匹配com.example包下,子孙包下所有类的所有方法
  • execution(* addUser(String,int)):匹配addUser方法,其第一个参数类型是String,第二个参数类型是int。

创建UserController类,这个类中的方法哪一个要被执行(目标方法)哪一个就是连接点

java 复制代码
package com.example.demo.controller;

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

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/getuser")
    public String getUser(){
        System.out.println("do getUser");
        return "get user";
    }
    @RequestMapping("/deluser")
    public String delUser(){
        System.out.println("do delUser");
        return "del user";
    }
}

3、定义相关通知

通知定义的是被拦截的方法具体要执行的业务。比如用户登录权限验证方法就是具体要执行的业务。

Spring AOP中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:

  • 前置通知 使用@Before:通知方法会在目标方法(连接点)调用之前执行
  • 后置通知 使用@After:通知方法会在目标方法(连接点)返回或者抛出异常后调用
  • 返回之后通知 使用@AfterReturning:通知方法会在目标方法(连接点)返回后调用
  • 抛异常后通知 使用@AfterThrowing:通知方法会在目标方法(连接点)抛出异常后调用
  • 环绕通知 使用@Around:通知包裹了被通知的方法,在被通知的方法之前和调用之后执行自定义的行为。

1️⃣前置通知和后置通知的实现

java 复制代码
package com.example.demo.common;

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;

@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
    //定义切点,@Pointcut注解的参数中定义了具体的拦截规则
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void pointcut(){}

    //定义前置通知
    @Before("pointcut()")//表示这个通知是针对pointcut方法的
    public void doBefore(){
        System.out.println("执行了前置通知");
    }

    //定义后置通知
    @After("pointcut()")
    public void doAfter(){
        System.out.println("执行了后置通知");
    }
}

当我们在前端页面中访问UserController类的方法时,后端程序的控制台上每次出现的结果是先执行前置通知,在执行目标方法(连接点),然后执行后置通知。

2️⃣环绕通知的具体实现

环绕通知方法是具有Object类型的返回值,需要把方法执行结果返回给框架,框架拿到对象继续执行。

java 复制代码
package com.example.demo.common;

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

@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
    //定义切点,@Pointcut注解的参数中定义了具体的拦截规则
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void pointcut(){}

    //定义前置通知
    @Before("pointcut()")//表示这个通知是针对pointcut方法的
    public void doBefore(){
        System.out.println("执行了前置通知");
    }

    //定义后置通知
    @After("pointcut()")
    public void doAfter(){
        System.out.println("执行了后置通知");
    }

    //定义环绕通知
    @Around("pointcut()")
    //环绕通知方法的参数为要执行的连接点,也就是我们在前端访问的目标方法
    public Object doAround(ProceedingJoinPoint joinPoint){
        System.out.println("环绕通知之前");
        Object result = null;
        try {
            //执行目标方法,它的目标方法就是我们在前端访问的方法
            result = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("环绕通知之后");
        return result;
    }
}

从执行结果中可以看到环绕通知的执行范围,可以环绕执行通知是最先执行的,然后是执行前置通知,然后再执行目标方法,然后执行后置通知,最后所有的方法执行完成了,环绕通知方法才会执行完成。

五、 AOP的实现原理

Spring AOP是建立再动态代理的基础上的,Spring对AOP的支持局限于方法级别的拦截。

Spring AOP使用两种混合的实现方式:JDK动态代理和CGLib动态代理。

  • JDK动态代理:如果目标对象实现了InvocationHandler接口,Spring将使用JDK动态代理来创建代理对象。
  • CGLib动态代理 :如果目标对象没有实现InvocationHandler接口,Spring将使用CGLib代理,通过继承目标对象来创建代理对象。

1、什么是动态代理

代理可以看作是对调用目标的一个包装,这样我们对目标代理的调用不是直接发生的,而是通过代理完成。

当想要给实现了某个接口的类中的方法,加一些额外的处理,比如加日志,加事务等。可以给这个类创建一个代理,也就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新方法,这个代理类并不是定义好的,而是动态生成的,具有解耦意义,灵活、扩展性强。

在Java中,动态代理通常使用Java.lang.reflect.Proxy类和Java.lang.reflect.InvocationHandler接口来实现。

2、 JDK代理和CGLIB代理的区别

  • 接口要求:JDK动态代理只能对实现了接口的类生成代理;而CGLIB代理可以没有实现接口的类,是通过继承被代理类,在运行时动态的生成代理对象。
  • 生成方式:JDK代理使用Java的反射机制来完成代理对象,而CGLIB代理使用CGLIB库生成代理对象,通过修改目标类的字节码来实现。所以该类不能被final修饰

如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

相关推荐
一元咖啡41 分钟前
SpringCloud Gateway转发请求到同一个服务的不同端口
spring·spring cloud·gateway
儿时可乖了1 小时前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol1 小时前
java基础概念37:正则表达式2-爬虫
java
Iced_Sheep1 小时前
干掉 if else 之策略模式
后端·设计模式
xmh-sxh-13141 小时前
jdk各个版本介绍
java
XINGTECODE1 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露