Java-Spring入门指南(十二)SpringAop的三种实现方式

Java-Spring入门指南(十二)SpringAOP的三种实现方式


前言

  • 在前一篇博客中,我们已经掌握了代理模式的核心逻辑,并初步认识了Spring AOP的概念------通过"切面"为业务方法动态添加增强功能,解决横切关注点(如日志、事务)与核心业务的耦合问题。

  • 而在实际开发中,Spring AOP提供了多种灵活的实现方式,适配不同的场景需求。

  • 本篇将聚焦Spring AOP的三种核心实现方式,从基于接口的通知实现、自定义切面实现,到注解驱动实现,逐步拆解代码逻辑,帮助你掌握不同方式的配置细节与适用场景,真正将AOP落地到项目开发中。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的Java-Spring入门指南知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_13040333.html?spm=1001.2014.3001.5482


Spring的官方AOP讲解网站
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop

一、AOP是什么,有什么用,作用是什么?

在正式讲解实现方式前,我们先快速回顾上一篇的核心内容

  • AOP是什么 :全称Aspect Oriented Programming(面向切面编程),是一种编程思想。它将分散在多个业务方法中的"通用逻辑"(如日志、权限校验)抽取为"切面(Aspect)",在指定时机(如方法执行前/后)动态"织入"到目标方法中,无需修改业务代码。

  • AOP有什么用 :解决"横切关注点"问题。横切关注点是指与核心业务无关,但需重复出现在多个方法中的逻辑(如记录每个业务方法的执行日志)。AOP让这些逻辑只需编写一次,即可作用于多个方法,减少代码冗余、降低耦合、便于统一维护

  • AOP的核心作用场景

    • 日志记录:自动记录方法调用信息(参数、返回值、执行时间);
    • 事务管理:方法执行前开启事务,执行成功提交、失败回滚;
    • 权限校验:方法执行前校验用户是否有权限操作;
    • 异常处理:统一捕获方法执行中的异常并处理。
  • AOP核心组件回顾

    • 切面(Aspect):封装横切逻辑的类(如"日志切面");
    • 通知(Advice):切面中的具体增强逻辑(如"方法执行前打印日志");
    • 切入点(Pointcut):通过表达式指定"哪些方法需要被增强";
    • 织入(Weaving):将通知动态植入目标方法的过程(Spring在运行时完成)。

二、AOP的三种实现方式

Spring AOP的实现方式围绕"如何定义切面与通知"展开,核心分为基于Spring接口的实现、自定义切面实现、注解驱动实现三种。

使用Spring AOP需添加aspectjweaver依赖:

xml 复制代码
<!-- AspectJ织入依赖:支持AOP功能的核心依赖 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.9.1</version>
</dependency>

方式一:基于Spring Advice接口实现

这种方式需让"切面类"实现Spring提供的Advice系列接口(如MethodBeforeAdvice前置通知、AfterReturningAdvice后置通知),通过接口方法定义增强逻辑。Spring会自动识别实现类为"通知类",再通过XML配置绑定"通知"与"切入点"。

1. 项目目录结构

先明确代码组织,将该方式的类放在com.niit.aop1包下:

复制代码
spring_aop
└── src
    └── main
        ├── java
        │   └── com.niit
        │       └── aop1          # 方式一的代码包
        │           ├── LogBefore.java       # 切面类(实现MethodBeforeAdvice)
        │           ├── StudentService.java  # 业务接口
        │           └── StudentServiceImpl.java # 业务实现类
        └── resources
            └── applicationContext.xml       # Spring核心配置文件

2. 核心代码实现

(1)业务接口与实现类

首先定义业务逻辑(学生管理的增删改查),这是需要被AOP增强的"目标对象":

  • StudentService.java(业务接口)
java 复制代码
// 学生业务接口:定义核心业务方法
public interface StudentService {
    void add();    // 添加学生
    void del();    // 删除学生
    void update(); // 修改学生
    void query();  // 查询学生
}
  • StudentServiceImpl.java(业务实现类)
java 复制代码
import org.springframework.stereotype.Component;

// 注册为Spring Bean,id为"ssi"(便于后续从容器中获取)
@Component("ssi")
public class StudentServiceImpl implements StudentService {
    // 核心业务逻辑:仅关注"做什么",不关心日志等增强逻辑
    @Override
    public void add() {
        System.out.println("【核心业务】添加学生");
    }

    @Override
    public void del() {
        System.out.println("【核心业务】删除学生");
    }

    @Override
    public void update() {
        System.out.println("【核心业务】修改学生");
    }

    @Override
    public void query() {
        System.out.println("【核心业务】查询学生");
    }
}
(2)切面类(实现Advice接口)

创建LogBefore类,实现MethodBeforeAdvice接口(Spring提供的"前置通知"接口),在before方法中定义"方法执行前"的增强逻辑(打印日志):

  • LogBefore.java(切面类)
java 复制代码
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

// 注册为Spring Bean(让Spring管理切面类)
@Component
// 实现MethodBeforeAdvice:代表"目标方法执行前"的增强
public class LogBefore implements MethodBeforeAdvice {

    /**
     * 前置通知的核心逻辑:目标方法执行前会自动调用此方法
     * @param method 被增强的目标方法(如add()、del())
     * @param args   目标方法的参数(本例中无参数,为null)
     * @param target 被增强的目标对象(如StudentServiceImpl实例)
     */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        // 增强逻辑:打印"哪个类的哪个方法即将执行"
        System.out.println("【AOP前置增强】" + 
                           "类:" + target.getClass().getName() + 
                           ",方法:" + method.getName() + " 即将执行");
    }
}
(3)Spring配置文件(applicationContext.xml)

通过XML配置"切入点"(指定增强哪些方法)和"通知绑定"(将LogBefore的增强逻辑织入切入点):

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <!-- 1. 组件扫描:扫描com.niit包下的@Component注解,注册Bean -->
    <context:component-scan base-package="com.niit"/>

    <!-- 2. AOP核心配置:方式一(基于Advice接口) -->
    <aop:config proxy-target-class="false">
        <aop:pointcut id="pointcut" expression="execution(* com.niit.aop1.*.add())"/>
        <aop:advisor advice-ref="logBefore" pointcut-ref="pointcut"></aop:advisor>
    </aop:config>

</beans>
  • 核心代码解析

1. 根标签 aop:config

  • 作用:是Spring AOP XML配置的根标签,所有AOP相关的配置(切入点、通知、切面关联等)都需要定义在该标签内部。
  • 属性 proxy-target-class
    用于指定Spring AOP生成代理的方式:
    • proxy-target-class="false"(默认值):使用JDK动态代理,要求目标类必须实现接口,代理对象是接口的实现类。
    • proxy-target-class="true":使用CGLIB代理,可以代理没有实现接口的类(通过继承目标类生成代理子类)。

2. 切入点配置 aop:pointcut

  • 作用:定义"切入点"(Pointcut),即AOP要拦截的方法(哪些方法需要被增强)。
  • 属性详解
    • id="pointcut":给切入点起一个唯一标识,方便后续引用(如在<aop:advisor>中关联)。
    • expression:切入点表达式,用于精确匹配需要拦截的方法,核心语法是execution()(最常用的表达式类型)。

切入点表达式 execution(* com.niit.aop1.*.add()) 解析

execution() 用于匹配方法执行的连接点,语法结构为:
execution(修饰符 返回值类型 包名.类名.方法名(参数列表))

  • *:第一个*表示"任意返回值类型"(如void、int、Object等)。
  • com.niit.aop1.*com.niit.aop1是包名,后面的*表示"该包下的所有类"。
  • .add():表示匹配类中的add()方法,且无参数 (如果有参数,需写成add(参数类型),如add(int))。

整体含义:拦截com.niit.aop1包下所有类中的add()无参方法。

3. 切面关联 aop:advisor

  • 作用 :将"通知(Advice)"和"切入点(Pointcut)"关联起来,形成一个完整的"切面(Aspect)"。

    (这里的advisor是Spring AOP中基于Advice接口的特殊切面形式,一个advisor只能关联一个通知和一个切入点)

  • 属性详解

    • advice-ref="logBefore":引用一个"通知Bean"(id为logBefore),该Bean需实现Spring的Advice接口(如MethodBeforeAdvice前置通知、AfterReturningAdvice后置通知等)。
    • pointcut-ref="pointcut":引用前面定义的切入点(id为pointcut),表示"通知"要作用在这个切入点匹配的方法上。

核心知识点总结

  1. AOP核心概念

    • 切入点(Pointcut):要拦截的方法(通过表达式定义)。
    • 通知(Advice):拦截方法后要执行的逻辑(如日志、事务、权限校验等),需实现Advice接口。
    • 切面(Aspect):切入点 + 通知的组合(这里通过<aop:advisor>实现)。
  2. 基于Advice接口的特点

    这是Spring早期的AOP配置方式,通知必须严格实现Spring提供的Advice相关接口(如MethodBeforeAdviceAfterReturningAdvice),灵活性较低。现在更多使用基于AspectJ的注解方式(如@Before@After)。

  3. 代理方式选择

    • JDK代理(默认):依赖接口,性能较好。
    • CGLIB代理:不依赖接口,可代理任意类,但会生成子类,性能略低。

3. 测试代码与结果

编写Junit测试类,从Spring容器中获取业务Bean,调用方法验证AOP是否生效:

  • 测试类代码
java 复制代码
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AopTest {

    @Test
    public void test1() {
        // 1. 加载Spring配置文件,初始化容器
        ClassPathXmlApplicationContext ac = 
                new ClassPathXmlApplicationContext("applicationContext.xml");

        // 2. 获取业务Bean:注意!若proxy-target-class=false(JDK代理),需按接口获取
        StudentService studentService = (StudentService) ac.getBean("ssi");
        
        // 3. 调用方法:验证增强效果
        System.out.println("-----调用add()方法(会被增强)-----");
        studentService.add();   // add()在切入点中,会触发前置通知
        System.out.println("-----调用del()方法(不被增强)-----");
        studentService.del();   // del()不在切入点中,无增强逻辑
        System.out.println("-----调用query()方法(不被增强)-----");
        studentService.query(); // query()不在切入点中,无增强逻辑

        // 4. 关闭容器
        ac.close();
    }
}
  • 测试结果

结果分析 :仅add()方法触发了前置增强(打印日志),del()query()未被增强,符合切入点表达式的配置,说明AOP生效。

方式一总结

  • 核心逻辑 :切面类实现Spring的Advice接口,通过<aop:advisor>绑定通知与切入点;
  • 代理方式 :支持JDK(基于接口)和CGLIB(基于子类),由proxy-target-class控制;
  • 优缺点:实现简单,适合简单的通知场景;但需依赖Spring接口,灵活性较低(若需多个通知,需实现多个接口)。

方式二:自定义切面实现(不依赖Spring接口)

这种方式无需实现Spring的Advice接口,而是自定义切面类(包含before、after等增强方法),再通过XML配置将自定义方法指定为"通知",灵活性更高。

1. 项目目录结构

将该方式的类放在com.niit.aop2包下,与方式一隔离:

复制代码
spring_aop
└── src
    └── main
        ├── java
        │   └── com.niit
        │       └── aop2          # 方式二的代码包
        │           ├── DiyPointCut.java     # 自定义切面类(含增强方法)
        │           ├── StudentService.java  # 业务接口(与方式一一致)
        │           └── StudentServiceImpl.java # 业务实现类
        └── resources
            └── applicationContext.xml       # Spring配置文件

2. 核心代码实现

(1)业务接口与实现类

与方式一完全一致,仅Bean id改为"ssi2"(避免与方式一的Bean重名):

  • StudentServiceImpl.java
java 复制代码
package com.niit.aop2;

import org.springframework.stereotype.Component;

// Bean id改为"ssi2",与方式一的"ssi"区分
@Component("ssi2")
public class StudentServiceImpl implements StudentService {
    @Override
    public void add() {
        System.out.println("【核心业务】添加学生");
    }

    @Override
    public void del() {
        System.out.println("【核心业务】删除学生");
    }

    @Override
    public void update() {
        System.out.println("【核心业务】修改学生");
    }

    @Override
    public void query() {
        System.out.println("【核心业务】查询学生");
    }
}


(2)自定义切面类(DiyPointCut)

自定义类,直接编写before(前置增强)、after(后置增强)方法,无需实现任何接口:

  • DiyPointCut.java
java 复制代码
import org.springframework.stereotype.Component;

// 注册为Spring Bean
@Component
// 自定义切面类:包含多个增强方法(before、after)
public class DiyPointCut {

    // 前置增强方法:目标方法执行前调用
    public void before() {
        System.out.println("【自定义前置增强】方法即将执行,准备参数校验...");
    }

    // 后置增强方法:目标方法执行后调用(无论是否抛出异常都会执行)
    public void after() {
        System.out.println("【自定义后置增强】方法执行完成,记录操作日志...");
    }

    // 环绕增强方法(可选):包裹目标方法,可在执行前后添加逻辑
    public void around() {
        System.out.println("【自定义环绕增强】方法执行前的准备工作...");
        // 注意:环绕增强若需执行目标方法,需配合ProceedingJoinPoint(后续方式三会演示)
        System.out.println("【自定义环绕增强】方法执行后的清理工作...");
    }
}
(3)Spring配置文件(applicationContext.xml)

重点使用<aop:aspect>标签配置自定义切面,将切面中的方法绑定为"通知":

xml 复制代码
<!-- 继续在applicationContext.xml中添加方式二的配置 -->
<!--    方式二-->
    <aop:config proxy-target-class="true"><!-- 这里用CGLIB代理(true),可直接按类获取Bean,false默认接口 -->
        <aop:aspect ref="diyPointCut"> <!-- 1. 定义切面:ref指向自定义切面类(DiyPointCut的Bean id)diyPointCut是DiyPointCut类的默认Bean id(首字母小写) -->
            <!-- 2. 定义切入点:增强com.niit.aop2包下的del()方法 -->
            <aop:pointcut id="mydiyPointCut" expression="execution(* com.niit.aop2.*.del())"/>
            <aop:after method="after" pointcut-ref="mydiyPointCut"></aop:after>
            <aop:before method="before" pointcut-ref="mydiyPointCut"></aop:before> <!-- 3. 绑定增强方法与切入点:指定切面中的方法作为通知 -->
        </aop:aspect>
    </aop:config>

3. 测试代码与结果

  • 测试类代码
java 复制代码
@Test
public void test2() {
    ClassPathXmlApplicationContext ac = 
            new ClassPathXmlApplicationContext("applicationContext.xml");

    // 因proxy-target-class=true(CGLIB代理),可直接按类获取Bean
    StudentServiceImpl studentService = 	ac.getBean(StudentServiceImpl.class);

    System.out.println("-----调用del()方法(会被增强)-----");
    studentService.del();   // del()在切入点中,触发前置+后置增强
    System.out.println("-----调用add()方法(不被增强)-----");
    studentService.add();   // add()不在切入点中,无增强

    ac.close();
}
  • 测试结果

方式三:注解驱动实现(@Aspect注解)

这是实际开发中最常用的方式------通过@Aspect@Before@After等注解直接在切面类中定义"切面"和"通知",无需复杂的XML配置,仅需开启注解驱动即可。

1. 项目目录结构

将该方式的类放在com.niit.aop3包下:

复制代码
spring_aop
└── src
    └── main
        ├── java
        │   └── com.niit
        │       └── aop3          # 方式三的代码包
        │           ├── AnnotationPointCut.java # 注解式切面类
        │           ├── StudentService.java     # 业务接口
        │           └── StudentServiceImpl.java  # 业务实现类
        └── resources
            └── applicationContext.xml          # Spring配置文件

2. 核心代码实现

(1)业务接口与实现类

Bean id改为"ssi3",避免重名:

  • StudentServiceImpl.java
java 复制代码
package com.niit.aop3;

import org.springframework.stereotype.Component;

@Component("ssi3")
public class StudentServiceImpl implements StudentService {
    @Override
    public void add() {
        System.out.println("【核心业务】添加学生");
    }

    @Override
    public void del() {
        System.out.println("【核心业务】删除学生");
    }

    @Override
    public void update() {
        System.out.println("【核心业务】修改学生");
    }

    @Override
    public void query() {
        System.out.println("【核心业务】查询学生");
    }
}
(2)注解式切面类(AnnotationPointCut)

使用@Aspect声明切面,@Before@After@Around等注解定义通知,execution表达式直接写在注解中:

  • AnnotationPointCut.java
java 复制代码
package com.niit.aop3;

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

// 1. 注册为Spring Bean
@Component
// 2. @Aspect:声明当前类是"切面类"
@Aspect
public class AnnotationPointCut {

    // 3. @Before:前置通知,增强com.niit.aop3包下的add()方法
    @Before(value = "execution(* com.niit.aop3.*.add())")
    public void before() {
        System.out.println("【注解前置通知】add()方法即将执行");
    }

    // 4. @After:后置通知(目标方法执行后,无论是否异常)
    @After(value = "execution(* com.niit.aop3.*.add())")
    public void after() {
        System.out.println("【注解后置通知】add()方法执行完成");
    }

    // 5. @Around:环绕通知(最灵活,包裹目标方法,需手动执行目标方法)
    @Around(value = "execution(* com.niit.aop3.*.add())")
    public void around(ProceedingJoinPoint point) throws Throwable {
        System.out.println("【注解环绕通知】方法执行前:开启事务");
        // 关键:调用point.proceed()执行目标方法(add())
        point.proceed();
        System.out.println("【注解环绕通知】方法执行后:提交事务");
    }

    // 6. @AfterReturning:返回通知(目标方法正常执行完成后触发)
    @AfterReturning(value = "execution(* com.niit.aop3.*.add())")
    public void afterReturning() {
        System.out.println("【注解返回通知】add()方法正常返回,无异常");
    }
}

关键说明

  • @Around通知必须传入ProceedingJoinPoint参数,通过point.proceed()手动触发目标方法执行;
  • 通知执行顺序:环绕前 → 前置 → 目标方法 → 后置 → 环绕后 → 返回通知(若无异常)。
(3)Spring配置文件(applicationContext.xml)

无需复杂的<aop:config>配置,仅需开启注解驱动的AOP即可:

xml 复制代码
<!-- 方式三:注解驱动AOP -->
<!-- 1. 组件扫描:扫描com.niit包下的@Component和@Aspect注解 -->
<context:component-scan base-package="com.niit"/>

<!-- 2. 开启AspectJ注解驱动:让Spring识别@Aspect、@Before等注解 -->
<aop:aspectj-autoproxy  proxy-target-class="true"/> <!-- 可选,默认false(JDK代理),true(CGLIB代理) -->

3. 测试代码与结果

  • 测试类代码
java 复制代码
import com.niit.aop3.StudentService;
@Test
public void test3() {
    ClassPathXmlApplicationContext ac = 
            new ClassPathXmlApplicationContext("applicationContext.xml");

    StudentService studentService = (StudentService) ac.getBean("ssi3");

    System.out.println("-----调用add()方法(触发所有注解通知)-----");
    studentService.add();

    ac.close();
}
  • 测试结果

三种实现方式对比

为了更清晰地选择合适的方式,我们通过表格总结核心差异:

实现方式 依赖接口 配置方式 灵活性 推荐度 适用场景
基于Spring Advice接口 需实现Advice系列接口 XML配置 ★★☆☆☆ 简单通知场景(仅需单一通知)
自定义切面实现 无需接口 XML配置 ★★★☆☆ 需多个通知,但不想用注解
注解驱动实现(@Aspect) 无需接口 注解+少量XML ★★★★★ 企业级开发(推荐首选)

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的Java-Spring入门指南知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_13040333.html?spm=1001.2014.3001.5482

|--------------------|
| 非常感谢您的阅读,喜欢的话记得三连哦 |

相关推荐
做运维的阿瑞2 小时前
使用 Python 打造一个轻量级系统信息查看器
开发语言·后端·python·系统架构
nbsaas-boot2 小时前
使用 DuckDB 构建高性能 OLAP 分析平台
java·服务器·数据库
Yeats_Liao2 小时前
Java网络编程(七):NIO实战构建高性能Socket服务器
java·网络·nio
磨十三2 小时前
C++ 中的类型双关、union 与类型双关:让一块内存有多个“名字”
开发语言·c++
chao_7892 小时前
Union 和 Optional 区别
开发语言·数据结构·python·fastapi
hsjkdhs2 小时前
C++之类的组合
开发语言·c++·算法
疯狂的Alex2 小时前
【C#避坑实战系列文章16】性能优化(CPU / 内存占用过高问题解决)
开发语言·性能优化·c#
象骑士Hack3 小时前
dev c++工具下载 dev c++安装包下载 dev c++软件网盘资源分享
开发语言·c++
迎風吹頭髮3 小时前
UNIX下C语言编程与实践15-UNIX 文件系统三级结构:目录、i 节点、数据块的协同工作机制
java·c语言·unix