深入理解 Spring AOP

系列文章目录

Java 面向对象进阶
Java 中的设计模式
深入理解 Spring IOC


前言

提示:这里可以添加本文要记录的大概内容:

当涉及到在 Spring Boot 应用中实现面向切面编程(AOP),我们可以使用 Spring 框架提供的 AOP 功能来将横切关注点(例如日志、事务管理等)与业务逻辑进行解耦,为业务类添加新的功能。AOP 允许我们在程序运行时,通过切面(Aspects)来捕获和处理特定的横切关注点。在本篇博客中,我们将深入探讨 Spring Boot AOP 的基本概念、应用场景和实现方式。


提示:以下是本篇文章正文内容,下面案例可供参考

一、什么是面向切面编程(AOP)?

1.1 AOP 简介

面向切面编程(AOP)是

  • 一种编程范式用于将应用程序的关注点(即横切关注点)从主要业务逻辑中分离出来

通过 AOP,我们可以将与业务逻辑无关的横切关注点(如日志、事务管理、安全性等)模块化,以便更好地管理和维护代码。AOP 在 Spring 框架中得到了很好的支持,使得开发者可以轻松地将切面集成到应用中。

1.2 AOP 应用场景

Spring Boot AOP 可以用于许多不同的应用场景,以下是一些常见的示例:

  • 日志记录: 在方法执行前后记录方法调用、参数和返回值,以便进行日志记录和审计。
  • 事务管理: 在方法执行前后启动和提交事务,确保数据库操作的一致性和完整性。
  • 异常处理: 捕获方法中抛出的异常并进行统一的异常处理和处理逻辑。
  • 权限控制: 验证用户权限,阻止未授权的用户访问特定方法。
  • 性能监控: 在方法执行前后测量方法的执行时间,以进行性能监控和分析。

1.3 AOP 在 spring 中的应用 - 拦截器(Interceptor)、过滤器(Filter)

Spring 拦截器(Interceptor)和过滤器(Filter)是在 Java Web 应用中用于实现横切关注点的两种常见方式。它们都可以用来处理请求和响应,但在实现细节和使用场景上有一些区别。

过滤器(Filter):

  • 位置: 过滤器位于 Servlet 容器中,独立于 Spring 框架。它们在请求到达 Servlet 前后进行处理。
  • 生命周期: 过滤器的生命周期由 Servlet 容器管理,包括初始化、请求处理和销毁阶段。
  • 作用范围: 过滤器可以对整个请求进行处理,包括请求和响应,因此可以用于处理 HTTP 请求的各个阶段
  • 配置: 过滤器在 web.xml 配置文件中进行声明和配置,可以配置多个过滤器,按照声明顺序依次执行。
  • 功能: 过滤器通常用于处理一些通用的、与业务请求无关的任务,如字符编码转换、身份验证、日志记录等。

拦截器(Interceptor):

  • 位置: 拦截器位于 Spring 框架内部,与 Spring MVC 相关。它们在请求进入 Spring 控制器之前或之后进行处理。
  • 生命周期: 拦截器的生命周期由 Spring 容器管理,包括初始化和销毁阶段。
  • 作用范围: 拦截器主要用于 Spring MVC 请求处理流程中,可以在请求处理方法调用前后插入逻辑
  • 配置: 拦截器在 Spring 配置文件中进行声明和配置,可以配置多个拦截器,按照声明顺序依次执行。
  • 功能: 拦截器通常用于处理与请求相关的任务,如日志记录、权限检查、性能监控等

总结:

  • 如果需要在请求处理前后执行一些通用的任务,通常使用过滤器
  • 如果需要在请求进入 Spring MVC 控制器之前或之后执行一些与业务逻辑相关的任务,通常使用拦截器。

二、自定义 Spring AOP

在 Spring Boot 中实现 AOP 需要以下步骤:

  • 创建切面类: 创建一个切面类,它包含要在方法执行前后执行的横切逻辑。切面类通常使用 @Aspect 注解进行标记。
  • 定义切点: 定义切点,它是一个表达式,用于确定哪些方法将被拦截。切点表达式可以使用 @Pointcut 注解进行定义
  • 编写通知: 编写通知,它是实际的横切逻辑。Spring 提供了多种类型的通知,如前置通知、后置通知、环绕通知等。
  • 将切面应用到目标方法: 使用 @Before、@After、@Around 等注解将切面应用到目标方法

以下是一个简单的示例,演示如何在 Spring Boot 中使用 AOP 记录方法调用和执行时间:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Pointcut("execution(* com.example.myapp.service.*.*(..))")
    private void serviceMethods() {}

    @Before("serviceMethods()")
    public void beforeServiceMethod() {
        System.out.println("Method is about to be executed...");
    }

    @AfterReturning(pointcut = "serviceMethods()", returning = "result")
    public void afterReturningServiceMethod(Object result) {
        System.out.println("Method executed successfully. Result: " + result);
    }

    @Around("serviceMethods()")
    public Object aroundServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        Object result = joinPoint.proceed();

        long endTime = System.currentTimeMillis();
        System.out.println("Method executed in " + (endTime - startTime) + "ms");

        return result;
    }
}

三、Spring AOP 底层原理

3.1 Spring AOP 源码解析

spring boot aop是怎么实现的 AOP 的实现是建立在 Spring 框架的 AOP 模块之上,而 Spring 的 AOP 模块又是基于 AspectJ 库实现的。下面我将简要介绍 Spring Boot AOP 的工作原理:

  • 代理模式: Spring Boot AOP 的核心思想是通过动态代理实现切面逻辑的注入。Spring Boot 使用 JDK 动态代理和CGLIB 动态代理两种方式来创建代理对象。如果目标类实现了接口,Spring 会使用 JDK 动态代理,否则会使用 CGLIB动态代理
  • 切点和切面: 在 Spring Boot AOP中,切点定义了哪些方法需要被拦截切面是一组横切关注点的集合。切点通过切点表达式定义,它可以使用方法的访问修饰符、方法名、参数等条件来匹配方法。切面则是一个Java 类,包含了通知(advice)和切点。
  • 通知: 通知是切面的核心部分,它定义了在何时和如何应用切面逻辑。Spring Boot提供了几种类型的通知,包括前置通知(@Before)、后置通知(@After)、环绕通知(@Around)、返回(@AfterReturning)和异常通知(@AfterThrowing)。每种通知类型都在方法执行的不同阶段触发。
  • 织入织入是将切面逻辑应用到目标方法的过程,它可以在编译时、类加载时或运行时进行。Spring Boot AOP使用运行时织入,即在方法执行时动态将切面逻辑织入到目标方法中。这是通过代理对象实现的,代理对象拦截方法调用并在特定的时机执行切面逻辑。
  • 执行顺序: 在使用多个切面的情况下,Spring Boot AOP 提供了执行顺序控制的机制。您可以通过 @Order 注解或实现Ordered 接口来指定切面的执行顺序,确保切面按照预期的顺序执行。

综上所述,

  • Spring Boot AOP 的实现基于动态代理和切面编程原理。通过定义切点、通知和切面,以及将切面逻辑织入到目标方法中。
  • Spring Boot AOP实现了将横切关注点与主要业务逻辑解耦的目标。这使得应用程序更加模块化、可维护性更强,并且允许开发人员轻松地在应用中应用各种横切逻辑。

3.2 Spring 代理的原理与实现

即然 Spring AOP 使用的动态代理,那么代理是如何实现的呢?

3.2.1 什么是代理

  • 代理模式是一种结构型设计模式它允许一个对象(代理)充当另一个对象(目标)的接口,以控制对目标对象的访问。

在Spring中,代理模式用于实现切面编程,即在方法调用前后注入特定的逻辑,从而实现横切关注点的分离。

3.2.2 Spring 静态代理

静态代理是在编译阶段手动创建代理类的方式,该代理类和目标类都需要实现同一个接口,从而实现代理逻辑的注入。以下是静态代理的主要特点:

  • 代理类预先编写: 在编写代理类时,开发者需要手动实现代理逻辑和目标方法的调用。
  • 目标类和代理类共同接口: 目标类和代理类都必须实现同一个接口,以确保代理对象可以替代目标对象。
  • 编译期间确定代理关系代理关系在编译期间就已确定,无法在运行时动态更改

静态代理示例:

interface UserService {
    void saveUser();
}

class UserServiceImpl implements UserService {
    @Override
    public void saveUser() {
        System.out.println("Saving user...");
    }
}

class UserServiceProxy implements UserService {
    private final UserService target;

	// 构造函数把被代理对象传进来
    public UserServiceProxy(UserService target) {
        this.target = target;
    }

    @Override
    public void saveUser() {
        System.out.println("Before saving user...");
        target.saveUser();
        System.out.println("After saving user...");
    }
}

3.2.3 Spring 动态代理

动态代理是在运行时通过反射机制创建代理类的方式,无需手动编写代理类,代理逻辑可以动态注入。

关于Java反射机制参考文章:

Spring框架主要使用JDK动态代理和CGLIB动态代理两种方式来实现动态代理。以下是动态代理的主要特点:

  • 运行时生成代理类: 代理类是在程序运行时通过反射机制动态生成的。
  • 代理对象实现接口或继承类: JDK动态代理要求目标类实现接口,而CGLIB动态代理则可以代理非接口的类。
  • 动态注入代理逻辑: 代理逻辑可以在运行时动态注入,实现切面逻辑的无侵入性注入。

JDK动态代理

  • JDK动态代理是基于Java标准库中的java.lang.reflect.Proxy类实现的。它要求目标类必须实现一个或多个接口。以下是JDK动态代理的实现过程:
  • 创建一个实现InvocationHandler接口的类,该接口包含一个invoke方法,用于拦截方法调用并执行切面逻辑。
  • 使用Proxy.newProxyInstance(ClassLoader, interfaces,InvocationHandler)方法创建代理对象。该方法需要传入类加载器、目标接口数组和InvocationHandler实例。
  • 当代理对象的方法被调用时,实际调用会传递到invoke方法,在该方法中可以执行切面逻辑,然后再调用目标方法。

CGLIB动态代理

  • CGLIB(Code Generation Library)是一个用于在运行时生成字节码并创建类的开源Java库与JDK动态代理不同,CGLIB动态代理可以代理没有实现接口的类。以下是CGLIB动态代理的实现过程:
  • 创建一个类,该类继承自目标类,并重写需要被代理的方法
  • 使用CGLIB库生成代理类的字节码,并加载到内存中,创建代理对象实例

实例演示
假设我们有一个业务接口UserService和其实现类UserServiceImpl,我们希望在调用UserServiceImpl的方法时记录日志。以下是使用JDK动态代理实现的示例:

public interface UserService {
    void saveUser();
}

public class UserServiceImpl implements UserService {
    @Override
    public void saveUser() {
        System.out.println("Saving user...");
    }
}

public class MyInvocationHandler implements InvocationHandler {
    private final Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before saving user...");
        Object result = method.invoke(target, args);
        System.out.println("After saving user...");
        return result;
    }
}

public class Main {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();
        InvocationHandler handler = new MyInvocationHandler(userService);

        UserService proxy = (UserService) Proxy.newProxyInstance(
            userService.getClass().getClassLoader(),
            userService.getClass().getInterfaces(),
            handler
        );

        proxy.saveUser();
    }
}

在Spring中,选择使用JDK动态代理还是CGLIB动态代理取决于目标类是否实现了接口。

  • 如果目标类实现了接口,Spring会使用JDK动态代理
  • 如果目标类没有实现接口,Spring会使用CGLIB动态代理

注: 为什么JDK 动态代理 不能代理没有实现接口的类

  • JDK动态代理的限制是由其实现机制决定的,它是基于Java标准库中的java.lang.reflect.Proxy类来创建代理对象的,且代理对象的生成是基于接口的动态生成字节码实现的

  • JDK动态代理的工作流程如下:创建一个实现InvocationHandler接口的类,该接口包含一个invoke方法,用于拦截方法调用并执行切面逻辑。使用Proxy.newProxyInstance(ClassLoader, interfaces,InvocationHandler)方法创建代理对象。该方法需要传入类加载器、目标接口数组和InvocationHandler实例。

  • 代理对象的字节码是在运行时通过接口来动态生成的,所以目标类必须实现接口,以便代理对象可以按照接口的规范进行方法调用。如果目标类没有实现接口,JDK动态代理就无法生成符合接口规范的代理对象。

为了解决这个限制,Spring框架引入了CGLIB动态代理,它可以代理没有实现接口的类。

  • CGLIB会在运行时生成目标类的子类作为代理类,从而绕过了接口的限制。

总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值