Spring基于注解实现 AOP 切面功能

前言

复制代码
在Spring AOP(Aspect-Oriented Programming)中,动态代理是常用的技术之一,用于在运行时动态地为目标对象生成代理对象,
并拦截其方法调用。Spring AOP 默认使用两种类型的动态代理机制:JDK 动态代理和 CGLIB 代理。
‌JDK 动态代理‌:
JDK 动态代理是 Java 原生提供的动态代理机制,它只能代理接口。如果你的目标对象实现了某个接口,Spring AOP 会默认使用 JDK 动态代理。
JDK 动态代理机制通过 java.lang.reflect.Proxy 类来创建代理对象,并将方法调用委托给 InvocationHandler 实现。
‌CGLIB 代理‌:
如果目标对象没有实现接口,Spring AOP 会使用 CGLIB(Code Generation Library)来生成代理对象。CGLIB 是一个强大的库,
可以生成目标对象的子类,并覆盖其方法以实现代理功能。通过 CGLIB,Spring AOP 能够代理没有实现接口的类(即具体的类)。
默认代理机制的选择
Spring AOP 在选择使用哪种代理机制时,遵循以下原则:
如果目标对象实现了至少一个接口,则默认使用 JDK 动态代理。
如果目标对象没有实现任何接口,则默认使用 CGLIB 代理。
配置示例
在大多数情况下,你不需要显式地指定使用哪种代理机制,因为 Spring 会自动为你选择。但是,如果你有特殊需求,可以通过配置来强制使用某种代理机制。

****一、****Spring AOP 注解概述

1.Spring 的 AOP 功能除了在配置文件中配置一大堆的配置,比如切入点、表达式、通知等等以外,
使用注解的方式更为方便快捷,特别是 Spring boot 出现以后,基本不再使用原先的 beans.xml 等配置文件了,而都推荐注解编程

|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| @Aspect | 切面声明,标注在类、接口(包括注解类型)或枚举上。 |
| @Pointcut | 切入点声明,即切入到哪些目标类的目标方法。既可以用 execution 切点表达式, 也可以是 annotation 指定拦截拥有指定注解的方法. value 属性指定切入点表达式,默认为 "",用于被通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式 |
| @Before | 前置通知, 在目标方法(切入点)执行之前执行。 value 属性绑定通知的切入点表达式,可以关联切入点声明,也可以直接设置切入点表达式 注意:如果在此回调方法中抛出异常,则目标方法不会再执行,会继续执行后置通知 -> 异常通知。 |
| @After | 后置通知, 在目标方法(切入点)执行之后执行 |
| @AfterReturning | 返回通知, 在目标方法(切入点)返回结果之后执行. pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 "" |
| @AfterThrowing | 异常通知, 在方法抛出异常之后执行, 意味着跳过返回通知 pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 "" 注意:如果目标方法自己 try-catch 了异常,而没有继续往外抛,则不会进入此回调函数 |
| @Around | 环绕通知:目标方法执行前后分别执行一些代码,类似拦截器,可以控制目标方法是否继续执行。 通常用于统计方法耗时,参数校验等等操作。 |

2、上面这些 AOP 注解都是位于 aspectjweaver依赖中;对于习惯了 Spring 全家桶编程的人来说,并不是需要直接引入 aspectjweaver 依赖,因为 spring-boot-starter-aop 组件默认已经引用了 aspectjweaver 来实现 AOP 功能。换句话说 Spring 的 AOP 功能就是依赖的 aspectjweaver !

复制代码
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.1.4.RELEASE</version>
</dependency>

3.AOP 底层是通过 Spring 提供的的动态代理技术实现的,在运行期间动态生成代理对象,代理对象方法执行时进行增强功能的介入,再去调用目标对象的方法,从而完成功能的增强。主要使用 JDK 动态代理Cglib 动态代理;所以如果目标类不是 Spring 组件,则无法拦截,如果是 类名.方法名 方式调用,也无法拦截。

二、@Aspect 快速入门

复制代码
1、@Aspect 常见用于记录日志、异常集中处理、权限验证、Web 参数校验、事务处理等等

2、要想把一个类变成切面类,只需3步:

  1)在类上使用 @Aspect 注解使之成为切面类

  2)切面类需要交由 Spring 容器管理,所以类上还需要有 @Service、
     @Repository、@Controller、@Component 等注解
  2)在切面类中自定义方法接收通知

3、AOP 的含义就不再累述了,下面直接上示例:
复制代码
/**
 * 切面类,用于处理日志、参数校验等
 *
 * @author songwp
 * @date 2020-04-27
 */
@Aspect
@Component
@Slf4j
public class HandleAspect {

    /**
     * @Pointcut :切入点声明,即切入到哪些目标方法。value 属性指定切入点表达式,默认为 ""。
     * 用于被下面的通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式
     * <p>
     * 切入点表达式常用格式举例如下:
     * - * com.songwp.aspect.EmpService.*(..)):表示 com.songwp.aspect.EmpService 类中的任意方法
     * - * com.songwp.aspect.*.*(..)):表示 com.songwp.aspect 包(不含子包)下任意类中的任意方法
     * - * com.songwp.aspect..*.*(..)):表示 com.songwp.aspect 包及其子包下任意类中的任意方法
     * </p>
     * value 的 execution 可以有多个,使用 || 隔开.
     */
    @Pointcut("execution(public * com.songwp.controller.*.*(..))")
    public void aopPointCut() {}


    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容。
     * value:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
     * <br/>
     * * @param joinPoint:提供对连接点处可用状态和有关它的静态信息的反射访问<br/> <p>
     * * * Object[] getArgs():返回此连接点处(目标方法)的参数,目标方法无参数时,返回空数组
     * * * Signature getSignature():返回连接点处的签名。
     * * * Object getTarget():返回目标对象
     * * * Object getThis():返回当前正在执行的对象
     * * * StaticPart getStaticPart():返回一个封装此连接点的静态部分的对象。
     * * * SourceLocation getSourceLocation():返回与连接点对应的源位置
     * * * String toLongString():返回连接点的扩展字符串表示形式。
     * * * String toShortString():返回连接点的缩写字符串表示形式。
     * * * String getKind():返回表示连接点类型的字符串
     * * * </p>
     */
    @Before("aopPointCut()")
    public void beforeAdvice() {
        System.out.println("前置通知执行");
    }

    /**
     * 后置通知:目标方法执行之后执行以下方法体的内容,不管目标方法是否发生异常。
     * value:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
     */
    @After("aopPointCut()")
    public void afterAdvice() {
        System.out.println("后置通知执行");
    }

    /**
     * 返回通知:目标方法返回后执行以下代码
     * value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
     * pointcut 属性:绑定通知的切入点表达式,优先级高于 value,默认为 ""
     * returning 属性:通知签名中要将返回值绑定到的参数的名称,默认为 ""
     *
     * @param joinPoint :提供对连接点处可用状态和有关它的静态信息的反射访问
     */
    @AfterReturning("execution(* com.songwp.service.impl.OperateLogServiceImpl.*(..))")
    public void logAfterReturning(JoinPoint joinPoint) {
        System.out.println("返回后通知: " + joinPoint.getSignature().getName());
    }

    /**
     * 异常通知:目标方法发生异常的时候执行以下代码,此时返回通知不会再触发
     * value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
     * pointcut 属性:绑定通知的切入点表达式,优先级高于 value,默认为 ""
     * throwing 属性:与方法中的异常参数名称一致,
     *
     * @param ex:捕获的异常对象,名称与 throwing 属性值一致
     */
    @AfterThrowing(pointcut = "execution(* com.songwp.service.impl.OperateLogServiceImpl.*(..))", throwing = "ex")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
        System.out.println("异常后通知: " + joinPoint.getSignature().getName() + ", Exception: " + ex);
    }

    /**
     * 环绕通知
     * 1、@Around 的 value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
     * 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.
     * 3、假如目标方法是控制层接口,则本方法的异常捕获与否都不会影响目标方法的事务回滚
     * 4、假如目标方法是控制层接口,本方法 try-catch 了异常后没有继续往外抛,则全局异常处理 @RestControllerAdvice 中不会再触发
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.songwp.service.impl.OperateLogServiceImpl.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        this.checkRequestParam(joinPoint);
        System.out.println("环绕通知: " + joinPoint.getSignature().getName());
        // 继续执行方法
        Object result = joinPoint.proceed();
        System.out.println("环绕通知: " + joinPoint.getSignature().getName());
        return result;
    }

    /**
     * 参数校验,防止 SQL 注入
     *
     * @param joinPoint
     */
    private void checkRequestParam(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length <= 0) {
            return;
        }
        String params = Arrays.toString(joinPoint.getArgs()).toUpperCase();
        String[] keywords = {"DELETE ", "UPDATE ", "SELECT ", "INSERT ", "SET ", "SUBSTR(", "COUNT(", "DROP ",
                "TRUNCATE ", "INTO ", "DECLARE ", "EXEC ", "EXECUTE ", " AND ", " OR ", "--"};
        for (String keyword : keywords) {
            if (params.contains(keyword)) {
                log.error("参数存在SQL注入风险,其中包含非法字符 {}.", keyword);
                throw new RuntimeException("参数存在SQL注入风险:params=" + params);
            }
        }
    }
}

三、@Aspect 切面不生效原因

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 确保切面类被Spring管理‌:在切面类上添加 @Service、@Repository、@Controller、@Component 等注解 |
| 检查路径设置‌:确保切面类被 @ComponentScan 注解扫描到。即有没有被Spring容器管理。可以使用 @PostConstruct注解测试。 |
| 检查切面表达式‌:确保切面表达式正确无误,能够匹配到目标方法。 |
| 特别注意: 比如定义了一个 AOP 切面(@Pointcut)拦截 ServiceA 中的方法 B,当从其他类调用方法 B 时(比如 Controller 层),会正常切入拦截,而从本类其他方法中调用方法 B 时,无法切入拦截,因为此时默认并不是通过代理对象调用的,而是直接通过 this 对象来调的。可以参考@EnableAspectJAutoProxy注解。 |

总结:AOP的高级特性使得开发者能够以声明式的方式处理复杂的应用场景。通过灵活使用切入点表达式和正则表达式,可以在Spring AOP中实现精确的连接点匹配。此外,AOP在性能监控、日志记录、事务管理等场景中的应用,展示了其在提高代码模块化和可维护性方面的强大能力。

相关推荐
To Be Clean Coder1 小时前
【Spring源码】getBean源码实战(二)
java·后端·spring
0和1的舞者3 小时前
SpringAOP详解(二)
学习·spring·切面·代理·知识·springaop
廋到被风吹走3 小时前
【Spring】Spring Cache 深度解析
java·后端·spring
七夜zippoe4 小时前
响应式编程基石 Project Reactor源码解读
java·spring·flux·响应式编程·mono·订阅机制
IT 行者4 小时前
Spring Framework 6.x 异常国际化完全指南:让错误信息“说“多国语言
java·后端·spring·异常处理·problemdetail·国际化i18n
鱼跃鹰飞5 小时前
面试题:Spring事务失效的八大场景
数据库·mysql·spring
wa的一声哭了5 小时前
内积空间 内积空间二
java·开发语言·python·spring·java-ee·django·maven
cike_y5 小时前
Spring使用注解开发
java·后端·spring·jdk1.8
wanghowie6 小时前
01.03 Spring核心|事务管理实战
java·后端·spring
微爱帮监所写信寄信6 小时前
微爱帮监狱寄信写信小程序PHP底层优化框架
java·开发语言·数据库·spring·微信·php·mybatis