spring05-Spring核心:AOP面向切面编程

一、什么是 AOP?

AOP 是为了解决“横切关注点”问题的一种编程范式。 

在一个项目中,有很多功能不是业务核心逻辑,但又会反复出现在多个地方,例如:

  • 日志记录

  • 权限校验

  • 登录状态检查

  • 统计耗时

  • 异常处理

这些逻辑与“业务方法”不在一个维度上,但又必须“附着在”业务方法上。AOP 就是用来把这些“通用功能”抽出来,统一管理和复用的。

1、案例背景

有一个登录流程,希望在不修改源代码的情况下,添加权限判断模块,使得用户在校验用户名、密码成功以后,做一个权限的判断,然后再进入到主页面。

spring05-Spring核心:AOP面向切面编程_第1张图片

不通过修改源代码的方式,在主干功能里添加新功能。降低了耦合度! 

 总结:为什么用 AOP?

优势 举例
解耦 权限判断逻辑不写在 login 方法里
复用 一次定义,多个方法可用
可插拔 加一个注解或切点表达式就生效

二、AOP 底层原理

AOP允许你在不改变原始业务代码的前提下,在方法前后加“额外逻辑”(例如日志、事务、安全控制等)。

AOP 是怎么做到的?

本质:Spring AOP 通过“动态代理”技术来实现对目标方法的增强。

2-1、动态代理的两种方式 

Spring 根据目标类是否实现接口,选择不同的方式生成代理对象

有两种情况动态代理
第一种 有接口情况,使用 JDK 动态代理
第二种 没有接口情况,使用 CGLIB 动态代理

1. 有接口 → 使用 JDK 动态代理 

spring05-Spring核心:AOP面向切面编程_第2张图片

目的:想要在实现类调login()方法的前后,添加新的功能。

特点:
  • JDK 动态代理只能为接口创建代理对象。

  • Spring 会为接口生成代理类,拦截接口方法,织入切面逻辑。

2. 没有接口 → 使用 CGLIB 动态代理

spring05-Spring核心:AOP面向切面编程_第3张图片

目的:在user类的add方法上增强新的方法。

特点:
  • 如果目标类没有实现接口,Spring 会使用 CGLIB(Code Generation Library)生成目标类的子类。

  • 代理类是目标类的“子类”,重写方法,加入增强逻辑。

2-2、代码实现

1、有接口:使用JDK动态代理

public interface UserService {
    void addUser();
}

public class UserServiceImpl implements UserService {
    public void addUser() {
        System.out.println("执行 addUser 方法");
    }
}

 使用Proxy类里面的方法:newProxyInstance(),创建代理对象。

public static Object newProxyInstance(
    ClassLoader loader,
    Class[] interfaces,
    InvocationHandler h)

这个方法是 java.lang.reflect.Proxy 类中的 静态方法,用于创建一个 代理对象


newProxyInstance方法,三个参数含义

1. ClassLoader loader

  • 作用:指定当前代理类由哪个类加载器来加载。

  • 一般写法:传入目标对象的类加载器,例如:

    target.getClass().getClassLoader()
    
  • 为什么需要这个?
    因为 Java 在运行时会动态生成一个代理类,这个代理类也需要被加载器加载进 JVM 才能使用。


2. Class[] interfaces

  • 作用:指定代理对象要实现的一组接口。

  • 这些接口是目标对象实现的接口,也是你希望代理对象对外暴露的行为

  • 一般写法

    target.getClass().getInterfaces()
    
  • 注意JDK 动态代理只能代理接口,不能代理普通类(如没有接口的目标类需要用 CGLIB)。


3. InvocationHandler h

  • 作用:代理逻辑的核心,是一个接口,你要提供它的实现类

  • 当你调用代理对象的方法时,实际执行的是 InvocationHandler invoke() 方法

接口定义如下:

public interface InvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

你可以在 invoke() 方法中定义:

  • 方法前后要干什么(增强)

  • 是否真的调用目标对象的方法


2、举个完整例子

假设你有一个接口 UserDao 和一个实现类:

public interface UserDao {

    public int add(int a, int b);
    public String update(String id);

}

public class UserDaoImpl implements UserDao {

    @Override
    public int add(int a, int b) {
        System.out.println("add方法执行了......");
        return a+b;
    }

    @Override
    public String update(String id) {
        System.out.println("update方法执行了......");
        return id;
    }
}

现在你想通过 JDK 动态代理加一个日志功能:

public class JDKProxy {

    public static void main(String[] args) {
        // 创建接口的实现类的代理对象
        Class[] interfaces = {UserDao.class};
        /*Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new InvocationHandler() {
            // 方法一:匿名内部类
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return null;
            }
        });*/

        // 方法二:创建接口:InvocationHandler的实现类
        UserDaoImpl userDaoImpl = new UserDaoImpl();
        // 创建接口实现类的代理对象
        UserDao userDao = (UserDao) Proxy.newProxyInstance(userDaoImpl.getClass().getClassLoader(), interfaces, new UserProxy(userDaoImpl));
        int sum = userDao.add(1, 2);
        System.out.println("result: " + sum);
    }

}

// 创建代理对象代码
class UserProxy implements InvocationHandler{

    // 创建的是谁的代理对象,就把谁传递过来
    // 有参构造
    private Object obj;
    public UserProxy(Object obj){
        this.obj = obj;
    }

    // 增强的逻辑
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 方法之前
        System.out.println("方法之前执行....." + method.getName() + ": 传递的参数..." + Arrays.toString(args));

        // 被增强的方法的执行
        Object res = method.invoke(obj, args);

        // 方法之后
        System.out.println("方法之后执行......" + obj);

        return res;
    }
}

输出:

spring05-Spring核心:AOP面向切面编程_第4张图片


3、总结记忆

参数 作用 常见写法
ClassLoader 加载代理类 target.getClass().getClassLoader()
Class[] 代理哪些接口 target.getClass().getInterfaces()
InvocationHandler 方法增强逻辑 自定义实现(如日志、事务)

三、Spring AOP 核心术语

public class User {
    public void add() { ... }
    public void update() { ... }
    public void select() { ... }
    public void delete() { ... }
}

3-1、连接点(Join Point)

连接点:程序中所有可以被增强的位置,在 Spring AOP 中就是方法

User 类来说:

add()、update()、select()、delete() 这四个方法都是连接点

3-2、切入点(Pointcut)

切入点:你“真正选中”的、打算增强的方法

  • 所有方法是“连接点”,但你不一定要增强全部。

  • 你可以用表达式挑出感兴趣的方法,比如:只增强 add 和 delete 方法

类比:

  • 连接点是“候选池”,切入点是“真正入选”。

示例表达式(用来定义切入点):

@Pointcut("execution(* com.example.service.User.add(..))")

上面这句意思是:

“我要增强 com.example.service.User 类的 add 方法”


3-3、通知(Advice)

通知:你想“插进去”的代码逻辑(也叫增强代码

也就是你实际想在方法前后做的事,比如:

  • 打印日志

  • 开启事务

  • 校验权限

  • 报错时写日志

1、通知有五种类型:

通知类型 作用时间点 示例含义
前置通知 @Before 方法执行前 比如:打日志、权限验证
后置通知 @AfterReturning 方法执行后(成功返回) 比如:打印返回结果
环绕通知 @Around 方法执行前后都包住 最强,可以控制是否继续执行原方法
异常通知 @AfterThrowing 方法抛出异常时触发 比如:记录异常日志
最终通知 @After 方法执行完一定会执行(无论成功或异常) 类似 finally

2、示例代码:

@Before("execution(* com.example.service.User.add(..))")
public void logBefore() {
    System.out.println("方法开始前记录日志");
}

这就是一个前置通知,目标是 add() 方法。


3、类比总结:

概念 类比解释 在你的例子中举例
连接点 所有方法都是“可以增强的点” add()update()
切入点 你选择要增强的那些点 只增强 add()
通知 插入的逻辑代码 add() 前打印日志

4、完整示例:

你定义一个切面类(Aspect):

@Aspect
@Component
public class LogAspect {

    @Pointcut("execution(* com.example.User.add(..))")
    public void addPointcut() {}

    @Before("addPointcut()")
    public void logBefore() {
        System.out.println("调用 add() 前打印日志");
    }
}

Spring 会自动为 User.add() 方法生成代理对象,在调用时:

logBefore() → add() 原方法 → (可能还有后置/最终通知)

3-4、切面(Aspect)

1、什么是“切面”?

切面(Aspect)= 通知(增强逻辑) + 切入点(在哪增强) 的组合体

用通俗话说:

  • 切面就是你定义的一整套增强规则:你想增强什么地方、增强什么逻辑

  • 在 Spring 中,通常是一个 普通 Java 类,加上 @Aspect 注解,就成了一个切面类。

切面是一个动作,把通知应用到切入点的过程。 

2、举个例子来理解“切面”

@Aspect
@Component
public class LogAspect {

    // 切入点:指定要增强哪些方法
    @Pointcut("execution(* com.example.service.UserService.*(..))")
    public void userServiceMethods() {}

    // 通知:在方法前记录日志
    @Before("userServiceMethods()")
    public void beforeLog() {
        System.out.println("【Log】方法调用前");
    }

    // 通知:方法执行后记录
    @After("userServiceMethods()")
    public void afterLog() {
        System.out.println("【Log】方法调用后");
    }
}
  • LogAspect 就是一个切面类

  • 它定义了:

    • 切入点:UserService 中的所有方法

    • 通知:在这些方法执行前/后执行打印日志


3、在 Spring 中如何定义一个切面?

只要满足两件事:

步骤 说明
1 类上加 @Aspect 注解(表示这是一个切面类)
2 类上加 @Component,让 Spring 扫描到它
3 在类中定义切入点 + 通知(@Pointcut + @Before / @After等)

四、AOP 操作

Spring 框架一般都是基于 AspectJ 实现 AOP 操作

什么是 AspectJ?

AspectJ 不是 Spring 组成部分,独立 AOP 框架,一般把 AspectJ 和 Spring 框架一起使用,进行 AOP 操作。

基于 AspectJ 实现 AOP 操作
(1)基于 xml 配置文件实现
(2)基于注解方式实现(使用)

4-1、添加AOP相关的依赖


    
    
        org.springframework
        spring-context
        5.3.33
    

    
    
        org.springframework
        spring-aspects
        5.3.33
    

4-2、切入点表达式

作用:知道对哪个类里面的哪个方法进行增强。 

语法的结构:excution([权限修饰符][返回值类型][类的全类名][方法名称][(参数列表)])

  • 权限修饰符:*表示任意权限;
  • 返回值类型:省略;
  • 参数列表:用..代替。 

示例1:对com.wsbazinga.dao.BookDao类里面的add进行增强。

excution(* com.wsbazinga.dao.BookDao.add(..))

示例1:对com.wsbazinga.dao.BookDao类里面的所有方法进行增强。

 excution(* com.wsbazinga.dao.BookDao.*(..))

示例1:对com.wsbazinga.dao包里面的所有类,以及类里面的所有方法进行增强。 

 excution(* com.wsbazinga.dao.*.*(..))

4-3、基于注解方式实现AOP操作 

1、创建普通类,定义方法

目标类(User.java

@Component(value = "userPr")
public class User {

    public void add(){
        System.out.println("User add method.....");
    }

}

2、创建增强类,编写增强逻辑 

增强类(UserProxy.java

@Component  // 注册为 Spring 管理的 Bean
@Aspect     // 声明为切面类
public class UserProxy {

    // 前置通知
    @Before("execution(* com.example.User.add(..))")
    public void before() {
        System.out.println("before......");
    }
}

3、 Spring 配置文件(applicationContext.xml

添加aop命名空间;

添加:开启注解扫描;开启基于注解的 AOP 自动代理




    
    

    
    

 4、测试类

@Test
    public void testXmlAnnotation(){
        // 加载spring配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");

        User user = applicationContext.getBean("userPr", User.class);
        user.add();
    }

输出结果:

spring05-Spring核心:AOP面向切面编程_第5张图片

4-4、通知类型一览

// 增强类
@Aspect
@Component
public class UserProxy {

    // 前置通知
    @Before("execution(* aopAnnotation.User.add(..))")
    public void before(){
        System.out.println("before......");
    }

    // 最终通知
    @After("execution(* aopAnnotation.User.add(..))")
    public void after(){
        System.out.println("after......");
    }

    // 返回通知
    @AfterReturning("execution(* aopAnnotation.User.add(..))")
    public void afterReturning(){
        System.out.println("afterReturning......");
    }

    @AfterThrowing("execution(* aopAnnotation.User.add(..))")
    public void afterThrowing(){
        System.out.println("afterThrowing......");
    }

    @Around("execution(* aopAnnotation.User.add(..))")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕执行前......");

        // 被增强的方法
        proceedingJoinPoint.proceed();

        System.out.println("环绕执行后......");
    }

}
通知类型 注解 说明
前置通知 @Before 在目标方法执行之前执行
环绕通知 @Around 包围目标方法,最强大的通知,可控制是否执行目标方法
返回通知(成功) @AfterReturning 在目标方法成功执行之后执行
异常通知(抛异常时) @AfterThrowing 在目标方法抛出异常时执行
最终通知(无论成功或异常) @After 在方法执行结束后一定执行(类似 finally)

1、执行顺序图解

1. @Around(环绕通知 - 前)
2. @Before(前置通知)
3. —— 执行目标方法(User.add()) ——
4. @AfterReturning(返回通知)
5. @After(最终通知)
6. @Around(环绕通知 - 后)

如果目标方法抛出异常,比如:

public void add() {
    System.out.println("add...");
    int a = 1 / 0; // 模拟异常
}

 此时的执行顺序变为:

1. @Around(环绕通知 - 前)
2. @Before(前置通知)
3. —— 执行目标方法(报错) ——
4. @AfterThrowing(异常通知)
5. @After(最终通知)
6. @Around(环绕通知 - 后) (注意是否还继续执行由异常处理方式决定

2、总结一下顺序:

情况 执行顺序
正常返回 Around前 → Before → 目标方法 → AfterReturning → After → Around后
异常抛出 Around前 → Before → 目标方法 → AfterThrowing → After → Around后

3、关于 @Around前后行为

@Around 的本质是你包裹整个方法执行的代码,比如:

@Around("execution(* aopAnnotation.User.add(..))")
public void around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("环绕前");

    pjp.proceed();  // 方法执行处,可能会抛异常

    System.out.println("环绕后");  // ⚠️ 如果上面抛出异常,这里不会执行
}

所以当 proceed() 抛异常时:

  • proceed() 后面的代码不会执行;

  • 除非你自己用 try-catch 把异常捕获住:

正确写法(如果你想环绕后也执行):

@Around("execution(* aopAnnotation.User.add(..))")
public void around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("环绕前");

    try {
        pjp.proceed();  // 目标方法执行
    } catch (Throwable e) {
        System.out.println("捕获异常:" + e.getMessage());
        throw e; // 继续抛出,供 @AfterThrowing 使用
    }

    System.out.println("环绕后");  // ✅ 只有在不抛出或被 catch 时才会执行
}

4-5、相同切入点的抽取

这是 Spring AOP 中一个 代码复用 + 可读性提升 的技巧。


1、什么是“相同切入点的抽取”?

在实际开发中,你可能会在多个通知上使用完全相同的切入点表达式,例如:

@Before("execution(* com.example.User.add(..))")
public void before() { ... }

@After("execution(* com.example.User.add(..))")
public void after() { ... }

@AfterReturning("execution(* com.example.User.add(..))")
public void afterReturning() { ... }

这样写虽然功能没问题,但重复太多,如果某天方法签名改了,你要到多个地方同步修改,非常麻烦。


2、怎么抽取?

Spring AOP 允许你使用 @Pointcut 注解来将切入点表达式提取成一个方法,然后在通知里复用它。


示例:切入点抽取的写法

@Aspect
@Component
public class UserProxy {

    // ① 抽取切入点表达式:不需要方法体
    @Pointcut("execution(* com.example.User.add(..))")
    public void userAddPointcut() {}

    // ② 使用切入点方法名作为引用
    @Before("userAddPointcut()")
    public void before() {
        System.out.println("before...");
    }

    @After("userAddPointcut()")
    public void after() {
        System.out.println("after...");
    }

    @AfterReturning("userAddPointcut()")
    public void afterReturning() {
        System.out.println("afterReturning...");
    }

    @AfterThrowing("userAddPointcut()")
    public void afterThrowing() {
        System.out.println("afterThrowing...");
    }

    @Around("userAddPointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕前...");
        joinPoint.proceed();
        System.out.println("环绕后...");
    }
}

3、高级一点:组合切入点

@Pointcut("execution(* com.example.User.add(..))")
public void addPointcut() {}

@Pointcut("execution(* com.example.User.update(..))")
public void updatePointcut() {}

@Pointcut("addPointcut() || updatePointcut()")
public void addOrUpdatePointcut() {}

4、总结一句话:

相同切入点的抽取,就是使用 @Pointcut 注解将切入点表达式封装为方法,然后在多个通知中统一引用,以提高代码复用性和可维护性。

4-6、设置增强类的执行顺序

在 Spring AOP 中,多个增强类(切面)对同一个方法进行增强时是有执行顺序的,并且这个顺序可以通过 设置优先级 来明确控制。


1、默认行为:不指定优先级时

如果你有多个 @Aspect 类,Spring 默认不会保证它们的执行顺序

例如:

@Aspect
@Component
public class LogAspect {
    @Before("execution(* com.example.User.add(..))")
    public void log() {
        System.out.println("日志记录...");
    }
}

@Aspect
@Component
public class SecurityAspect {
    @Before("execution(* com.example.User.add(..))")
    public void check() {
        System.out.println("权限检查...");
    }
}

执行顺序是不确定的,可能先日志,也可能先权限。


2、如何设置优先级?

Spring 提供了使用 @Order 注解的方式来设置切面的执行顺序:

@Aspect
@Component
@Order(1)  // 数字越小,优先级越高,最先执行
public class LogAspect {
    @Before("execution(* com.example.User.add(..))")
    public void log() {
        System.out.println("日志记录...");
    }
}

@Aspect
@Component
@Order(2)
public class SecurityAspect {
    @Before("execution(* com.example.User.add(..))")
    public void check() {
        System.out.println("权限检查...");
    }
}

执行顺序:

@Order(1)@Order(2) → 目标方法执行

即:

日志记录...
权限检查...
User add method...

3、注意点

事项 说明
数字越小越先执行 比如 @Order(1) 先于 @Order(5)
@Around 特别重要 因为它包裹整个流程,外层的 @Around 最先执行、最后结束

4、结合环绕通知的顺序图解

假设你有两个切面都有 @Around,并都设置了 @Order

@Aspect
@Order(1)
public class OuterAspect {
    @Around("execution(* com.example.User.add(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("Outer 环绕前");
        Object result = pjp.proceed();
        System.out.println("Outer 环绕后");
        return result;
    }
}

@Aspect
@Order(2)
public class InnerAspect {
    @Around("execution(* com.example.User.add(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("Inner 环绕前");
        Object result = pjp.proceed();
        System.out.println("Inner 环绕后");
        return result;
    }
}

输出顺序为:

Outer 环绕前
Inner 环绕前
User add method...
Inner 环绕后
Outer 环绕后

5、总结

多个切面对同一方法增强时,可以用 @Order 设置优先级。数字越小,越先执行。如果有 @Around,它最先进入、最后退出,像一层层的“洋葱”。

4-6、基于xml配置文件设置AOP(了解)

spring05-Spring核心:AOP面向切面编程_第6张图片

这段 XML 的功能是:

com.atguigu.spring5.aopxml.Book 类的 buy() 方法,执行一个“前置通知”,该通知由 BookProxy 类中的 before() 方法实现。

4-7、 Spring AOP 完全基于注解的方式

一、什么是完全注解方式?

完全注解方式指的是:

不使用 applicationContext.xml,所有的配置都通过 @Configuration@Component@Aspect@EnableAspectJAutoProxy 等注解完成。


二、关键注解一览

注解 作用
@Configuration 声明一个 Java 配置类,相当于 XML 配置
@ComponentScan 扫描指定包下的类,注册为 Bean
@EnableAspectJAutoProxy 开启 Spring 的 AOP 支持
@Aspect 声明一个切面类
@Before / @After / @Around / @AfterReturning / @AfterThrowing 声明各种类型的通知

【注意】:

所有配置类你只需要加上 @Configuration 即可不需要再加 @Component,因为:

  • @Configuration@Component:它本身就是一种特殊的 @Component

  • Spring 会自动把 @Configuration 注解的类注册为 Bean。

  • 如果这个类被扫描到了,Spring 就会自动识别它是配置类并处理其中的 @Bean 方法。

 只要 配置类 在你的 @ComponentScan 范围内,它就能被自动加载。


三、完整示例

我们写一个示例,对 UserService.add() 方法进行 AOP 增强。

1、目标类:UserService.java

package com.example.service;

import org.springframework.stereotype.Component;

@Component
public class UserService {
    public void add() {
        System.out.println("UserService add() method...");
    }
}

2、切面类:LogAspect.java

package com.example.aspect;

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

@Aspect
@Component
public class LogAspect {

    @Before("execution(* com.example.service.UserService.add(..))")
    public void before() {
        System.out.println("前置通知:before...");
    }

    @After("execution(* com.example.service.UserService.add(..))")
    public void after() {
        System.out.println("最终通知:after...");
    }

    @AfterReturning("execution(* com.example.service.UserService.add(..))")
    public void afterReturning() {
        System.out.println("返回通知:afterReturning...");
    }

    @AfterThrowing("execution(* com.example.service.UserService.add(..))")
    public void afterThrowing() {
        System.out.println("异常通知:afterThrowing...");
    }

    @Around("execution(* com.example.service.UserService.add(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("环绕前...");
        Object result = pjp.proceed(); // 调用目标方法
        System.out.println("环绕后...");
        return result;
    }
}

3、配置类:AppConfig.java

package com.example.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration                      // 声明配置类
@ComponentScan("com.example")       // 扫描所有组件(含 service 和 aspect)
@EnableAspectJAutoProxy            // 开启 AOP 注解自动代理
public class AppConfig {
}

4、测试类:MainApp.java

package com.example;

import com.example.config.AppConfig;
import com.example.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MainApp {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig.class);

        UserService userService = context.getBean(UserService.class);
        userService.add();  // 执行方法,触发 AOP

        context.close();
    }
}

四、执行结果(正常情况)

环绕前...
前置通知:before...
UserService add() method...
返回通知:afterReturning...
最终通知:after...
环绕后...

如果在 add() 方法中抛异常:

public void add() {
    System.out.println("UserService add() method...");
    int a = 1 / 0;  // 模拟异常
}

输出变为:

环绕前...
前置通知:before...
UserService add() method...
异常通知:afterThrowing...
最终通知:after...
环绕后... (是否输出视是否捕获异常而定)

五、总结

步骤 内容
1 @Component 注解目标类和切面类
2 切面类加上 @Aspect 注解并写好各种通知方法
3 写一个 @Configuration 配置类,开启组件扫描和 AOP 功能
4 使用 AnnotationConfigApplicationContext 启动上下文

你可能感兴趣的:(spring,spring)