Spring框架:AOP

在软件开发的世界里,我们不断追求着代码的高内聚、低耦合和可维护性。当系统变得复杂时,一些与核心业务逻辑无关但又必不可少的功能,如日志记录、事务管理、权限校验等,会不可避免地散落在各个业务模块中,形成大量重复代码,这给维护带来了巨大挑战。

Spring AOP(面向切面编程)正是为解决这一问题而生的利器。它如同一位技艺高超的裁缝,能够在不改动衣服(业务代码)本身的情况下,为其巧妙地缝上精美的补丁(横切逻辑)。本文将带你深入理解 AOP 的概念、原理,并通过 XML 和注解两种方式,手把手教你如何在 Spring 中应用 AOP。

一、AOP概念的引入

首先我们来看一下登录的原理

如上图所示这是一个基本的登录原理图,但是如果我们想要在这个登录之上添加一些新的功能,比如权限校验

那么我们能想到的就有两种方法:

①:通过对源代码的修改实现。

②:不通过修改源代码方式添加新的功能 (AOP)

AOP 的解决方案(横向抽取): AOP 提供了一种全新的思路:在不修改 源代码的前提下,动态地为其添加功能。我们将日志和权限校验逻辑抽离出来,形成独立的 "切面",然后在程序运行时,由框架将这些切面 "织入" 到目标方法的执行流程中。

这就是 AOP 的核心思想:将横切关注点(Cross-cutting Concerns)与业务逻辑分离

AOP的优势:

运行期间,不修改源代码的情况下对已有的方法进行增强

  1. 减少重复的代码

  2. 提供开发的效率

  3. 维护方便

二、AOP的核心概念

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程

AOP是一种编程范式,隶属于软工范畴,指导开发者如何组织程序结构

AOP最早由AOP联盟的组织提出的,制定了一套规范.Spring将AOP思想引入到框架中,必须遵守AOP联盟的规范

通过预编译方式或者运行期动态代理实现程序功能的统一维护的一种技术

AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型

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

AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码(事务管理、安全检查、缓存)

为什么要学习AOP,可以在不修改源代码的前提下,对程序进行增强!!

三、AOP的底层逻辑

3.1 JDK的动态代理技术

1、为接口创建代理类的字节码文件

2、使用ClassLoader将字节码文件加载到JVM

3、创建代理类实例对象,执行对象的目标方法

3.2 CGLIB 代理技术

为类生成代理对象,被代理类有没有接口都无所谓,底层是生成子类,继承被代理类

四、Spring AOP 的两种实现方式

4.1 基于 XML 的配置

4.1.1 AOP相关的术语

Joinpoint(连接点) 类里面有哪些方法可以增强这些方法称为连接点

Pointcut(切入点) -- 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义

Advice(通知/增强)-- 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)

Aspect(切面)-- 是 切入点+通知 的结合,以后咱们自己来编写和配置的

4.1.2 基本准备工作

AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,AspectJ实际上是对AOP编程思想的一个实践。

4.1.3 AOP配置文件方式的入门

创建maven项目,坐标依赖、

复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!--AOP联盟-->
    <dependency>
        <groupId>aopalliance</groupId>
        <artifactId>aopalliance</artifactId>
        <version>1.0</version>
    </dependency>
    <!--Spring Aspects-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--aspectj-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.3</version>
    </dependency>
</dependencies>

创建被增强的类

复制代码
// 被增强的类
public class User {
    //连接点/切入点
    public void add(){
        System.out.println("add......");
    }
    public void update(){
        System.out.println("update......");
    }
}

将目标类配置到Spring中

复制代码
<bean id="user" class="com.aopImpl.User"></bean>

定义切面类

复制代码
public class UserProxy {
    //增强/通知  ---》前置通知
    public void before(){
        System.out.println("before.............");
    }
}

在配置文件中定义切面类

复制代码
<bean id="userProxy" class="com.aopImpl.UserProxy"></bean>

在配置文件中完成AOP的配置

复制代码
<!--配置切面-->
<aop:config>
    <!--配置切面 = 切入点 + 通知组成-->
    <aop:aspect ref="userProxy">
        <!--前置通知:UserServiceImpl的save方法执行前,会增强-->
        <!--pointcut:后边是切入点表达式,作用是知道对对面的那个方法进行增强-->
        <aop:before method="before" pointcut="execution(public void com.aopImpl.User.add())"/>
    </aop:aspect>
</aop:config>

完成测试

复制代码
public class DemoTest {
    @Test
    public void aopTest1(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = (User) applicationContext.getBean("user");
        user.add();
    }
}

4.1.4 切入点的表达式

再配置切入点的时候,需要定义表达式,具体展开如下:

切入点表达式的格式如下:

execution([修饰符] [返回值类型] [类全路径] [方法名 ( [参数] )])

修饰符可以省略不写,不是必须要出现的。

返回值类型是不能省略不写的,根据你的方法来编写返回值,可以使用 * 代替。

包名,类名,方法名,参数的规则如下:

例如:com.qcby.demo3.BookDaoImpl.save()

首先包名,类名,方法名是不能省略不写的,但是可以使用 * 代替

中间的包名可以使用 * 号代替

类名也可以使用 * 号代替,也有类似的写法:*DaoImpl

方法也可以使用 * 号代替

参数如果是一个参数可以使用 * 号代替,如果想代表任意参数使用 ..

**比较通用的表达式:**execution(* com.qcby.*.ServiceImpl.save(..))

举例2:com.qcby.demo3.BookDaoImpl当中所有的方法进行增强

execution(* com.qcby.*.ServiceImpl.*(..))

举例3:com.qcby.demo3包当中所有的方法进行增强

execution(* com.qcby.*.*.*(..))

复制代码
<!--配置切面-->
<aop:config>
    <!--配置切面 = 切入点 + 通知组成-->
    <aop:aspect ref="userProxy">
        <!--切入点的表达式
        execution() 固定的写法
        public 是可以省略不写的
        方法的返回值 int String 通用的写法,可以编写 * 不能省略不写的
        包名+类名 不能省略不写的,编写 * com.*
        方法名称 add() 可以写 *
        参数列表 (..) 表示任意类型和个数的参数
        比较通用的表达式:execution(* com.*.User.add(..))-->
        <aop:before method="before" pointcut="execution(* com.*.User.add(..))"/>
    </aop:aspect>
</aop:config>

4.1.5 AOP的通知类型

**1. 前置通知:**目标方法执行前,进行增强。

如上配置案例就是前置通知

**2. 环绕通知:**目标方法执行前后,都可以进行增强。目标对象的方法需要手动执行。

复制代码
// 环绕通知
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    System.out.println("before.............");
    //  执行被增强的方法
    proceedingJoinPoint.proceed();
    
    System.out.println("after.............");
}

xml配置:

复制代码
<aop:around method="around" pointcut="execution(* com.*.User.add(..))"/>

**3. 最终通知:**目标方法执行成功或者失败,进行增强。

复制代码
// 最终通知
public void after() {
    System.out.println("after.............");
}

xml配置:

复制代码
<aop:after method="after" pointcut="execution(* com.*.User.add(..))"/>

**4. 后置通知:**目标方法执行成功后,进行增强。

复制代码
//后置通知
public void afterReturning() {
    System.out.println("afterReturning.............");
}

xml配置:

复制代码
<aop:after-returning method="afterReturning" pointcut="execution(public void com.aopImpl.User.add())"/>

**5. 异常通知:**目标方法执行失败后,进行增强。(发生异常的时候才会执行,否则不执行)

复制代码
//异常通知
public void afterThrowing() {

    System.out.println("afterThrowing.............");
}

需要改动一下切点:

复制代码
//连接点/切入点
public void add(){
    int a = 10 / 0;
    System.out.println("add......");
}

xml配置:

复制代码
<aop:after-throwing method="afterThrowing" pointcut="execution(public void com.aopImpl.User.add())"/>

4.2 基于注解的配置

创建maven工程,导入坐标。编写接口,完成IOC的操作。步骤略。

编写切面类

给切面类添加注解 @Aspect,编写增强的方法,使用通知类型注解声明

1.配置xml扫描注解

复制代码
<?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.aopImpl"></context:component-scan>
    
</beans>

2.配置注解

复制代码
@Component
public class User {
    //连接点/切入点
    public void add(){
        System.out.println("add......");
    }
}

给切面类添加注解 @Aspect,编写增强的方法,使用通知类型注解声明

复制代码
@Component
@Aspect  //生成代理对象
public class UserProxy {

}

3.配置文件中开启自动代理

复制代码
<?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.aopImpl"></context:component-scan>
    <!--开启Aspect生成代理对象-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>

4.通知类型注解

@Before -- 前置通知

@AfterReturing -- 后置通知

@Around -- 环绕通知(目标对象方法默认不执行的,需要手动执行)

@After -- 最终通知

@AfterThrowing -- 异常抛出通知

复制代码
@Component
@Aspect  //生成代理对象
public class UserProxy {
    //增强/通知  ---》前置通知
    @Before(value = "execution(* com.*.User.add(..))")
    public void before(){
        System.out.println("before.............");

    }

    // 环绕通知
    @Around(value = "execution(* com.*.User.add(..))")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("before.............");
        //  执行被增强的方法
        proceedingJoinPoint.proceed();
        System.out.println("after.............");
    }

    // 最终通知
    @After(value = "execution(* com.*.User.add(..))")
    public void after() {

        System.out.println("after.............");
    }

    //异常通知
    @AfterThrowing(value = "execution(* com.*.User.add(..))")
    public void afterThrowing() {

        System.out.println("afterThrowing.............");
    }

    //后置通知
    @AfterReturning(value = "execution(* com.*.User.add(..))")
    public void afterReturning() {

        System.out.println("afterReturning.............");
    }
}

5.测试类

复制代码
@Test
public void aopTest1(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    
    user.add();
}
相关推荐
GetcharZp几秒前
告别 TCP 握手延迟!让你的 Go 服务瞬间拥抱 HTTP/3 时代
后端
花花鱼3 分钟前
Spring Security 与 Spring MVC
java·spring·mvc
oak隔壁找我18 分钟前
SpringBoot 将项目打包成 Fat JAR(肥包),核心原理
后端
言慢行善1 小时前
sqlserver模糊查询问题
java·数据库·sqlserver
专吃海绵宝宝菠萝屋的派大星1 小时前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟1 小时前
操作系统之虚拟内存
java·服务器·网络
Tong Z1 小时前
常见的限流算法和实现原理
java·开发语言
凭君语未可1 小时前
Java 中的实现类是什么
java·开发语言
He少年1 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python