使用SpringBoot AOP切面编程的应用

AOP

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容,它通过对既有程序定义一个切入点,然后在其前后切入不同的执行内容,比如常见的有:打开数据库连接/关闭数据库连接、打开事务/关闭事务、记录日志等。基于AOP不会破坏原来程序逻辑,因此它可以很好的对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

为什么要使用 AOP

在实际的开发过程中,我们的应用程序会被分为很多层。通常来讲一个 Java 的 Web 程序会拥有以下几个层次:

  • Web 层:主要是暴露一些 Restful API 供前端调用。
  • 业务层:主要是处理具体的业务逻辑。
  • 数据持久层:主要负责数据库的相关操作(增删改查)。

虽然看起来每一层都做着全然不同的事情,但是实际上总会有一些类似的代码,比如日志打印和安全验证等等相关的代码。如果我们选择在每一层都独立编写这部分代码,那么久而久之代码将变的很难维护。所以我们提供了另外的一种解决方案: AOP。这样可以保证这些通用的代码被聚合在一起维护,而且我们可以灵活的选择何处需要使用这些代码。

AOP 的核心概念

  • 切面(Aspect):通常是一个类,在里面可以定义切入点和通知。
  • 连接点(Joint Point):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截的到的方法,实际上连接点还可以是字段或者构造器。
  • 切入点(Pointcut):对连接点进行拦截的定义。
  • 通知(Advice):拦截到连接点之后所要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。
  • AOP 代理:AOP 框架创建的对象,代理就是目标对象的加强。Spring 中的 AOP 代理可以使 JDK 动态代理,也可以是 CGLIB 代理,前者基于接口,后者基于子类。

Spring AOP

Spring 中的 AOP 代理还是离不开 Spring 的 IOC 容器,代理的生成,管理及其依赖关系都是由 IOC 容器负责,Spring 默认使用 JDK 动态代理,在需要代理类而不是代理接口的时候,Spring 会自动切换为使用 CGLIB 代理,不过现在的项目都是面向接口编程,所以 JDK 动态代理相对来说用的还是多一些。在本文中,我们将以注解结合 AOP 的方式来分别实现 Web 日志处理和分布式锁。

Spring AOP 相关注解

  • @Aspect: 将一个 java 类定义为切面类。
  • @Pointcut:定义一个切入点,可以是一个规则表达式,比如下例中某个 package 下的所有函数,也可以是一个注解等。
  • @Before:在切入点开始处切入内容。
  • @After:在切入点结尾处切入内容。
  • @AfterReturning:在切入点 return 内容之后切入内容(可以用来对处理返回值做一些加工处理)。
  • @Around:在切入点前后切入内容,并自己控制何时执行切入点自身的内容。
  • @AfterThrowing:用来处理当切入内容部分抛出异常之后的处理逻辑。

其中 @Before@After@AfterReturning@Around@AfterThrowing 都属于通知。

AOP 顺序问题

在实际情况下,我们对同一个接口做多个切面,比如日志打印、分布式锁、权限校验等等。这时候我们就会面临一个优先级的问题,这么多的切面该如何告知 Spring 执行顺序呢?这就需要我们定义每个切面的优先级,我们可以使用 @Order(i) 注解来标识切面的优先级, i 的值越小,优先级越高。假设现在我们一共有两个切面,一个 WebLogAspect,我们为其设置@Order(100);而另外一个切面 DistributeLockAspect 设置为 @Order(99),所以 DistributeLockAspect 有更高的优先级,这个时候执行顺序是这样的:在 @Before 中优先执行 @Order(99) 的内容,再执行 @Order(100) 的内容。而在 @After 和 @AfterReturning 中则优先执行 @Order(100) 的内容,再执行 @Order(99) 的内容,可以理解为先进后出的原则。

基于注解的 AOP 配置

使用注解一方面可以减少我们的配置,另一方面注解在编译期间就可以验证正确性,查错相对比较容易,而且配置起来也相当方便。相信大家也都有所了解,我们现在的 Spring 项目里面使用了非常多的注解替代了之前的 xml 配置。而将注解与 AOP 配合使用也是我们最常用的方式,在本文中我们将以这种模式实现 Web 日志统一处理和分布式锁两个注解。下面就让我们从准备工作开始吧。

添加依赖

我们需要添加 Web 依赖和 AOP 相关依赖,只需要在 pom.xml 中添加如下内容即可:

清单 1. 添加 web 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

清单 2. 添加 AOP 相关依赖 

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

确保Spring AOP已被启用,通过@EnableAspectJAutoProxy注解在启动类中启用它。 

应用场景1:WEB日志切面

Web日志美化

在实际的开发过程中,我们会需要将接口的出请求参数、返回数据甚至接口的消耗时间都以日志的形式打印出来以便排查问题,有些比较重要的接口甚至还需要将这些信息写入到数据库。而这部分代码相对来讲比较相似,为了提高代码的复用率,我们可以以 AOP 的方式将这种类似的代码封装起来。

import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * @author hudy
 * @date 2025/8/19
 */
@Aspect
@Component
@Profile("dev") // 只在dev环境下生效
public class WebLogAspect {

  private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

  private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();

  /**
   * 以 controller 包下定义的所有请求为切入点
   */
  @Pointcut("execution(public * com.test.custom.controller..*.*(..))")
  public void webLog() {
  }

  @Before("webLog()")
  public void doBefore(JoinPoint joinPoint) {
    // 接收到请求,记录请求内容
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if (attributes == null) {
      return;
    }

    HttpServletRequest request = attributes.getRequest();

    START_TIME.set(System.currentTimeMillis());

    // 记录请求内容
    logger.info("URL : {}", request.getRequestURL().toString());
    logger.info("HTTP_METHOD : {}", request.getMethod());
    logger.info("IP : {}", request.getRemoteAddr());
    logger.info("CLASS_METHOD : {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
    logger.info("参数 : {}", Arrays.toString(joinPoint.getArgs()));
  }

  @AfterReturning(returning = "ret", pointcut = "webLog()")
  public void doAfterReturning(Object ret) {
    try {
      // 处理完请求,返回内容
      long spendTime = System.currentTimeMillis() - START_TIME.get();
      logger.info("RESPONSE : {}", ret);
      logger.info("消耗时间: {}ms", spendTime);
    } finally {
      // 清理 ThreadLocal 避免内存泄漏
      START_TIME.remove();
    }
  }
}

应用场景2:Redis的分布式锁

为什么要使用分布式锁

我们程序中多多少少会有一些共享的资源或者数据,在某些时候我们需要保证同一时间只能有一个线程访问或者操作它们。在传统的单机部署的情况下,我们简单的使用 Java 提供的并发相关的 API 处理即可。但是现在大多数服务都采用分布式的部署方式,我们就需要提供一个跨进程的互斥机制来控制共享资源的访问,这种互斥机制就是我们所说的分布式锁。

代码实现:SPRING BOOT 动态定时任务_springboot 动态定时任务-优快云博客

应用场景3:字符串首尾空格去除

我们的程序接口,提交的参数列表需要去除字符串首尾空格。

切面实现

包配置类

package com.hworld.custom.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * HTTP参数trim配置类 用于配置需要进行参数trim处理的包前缀和控制器切点表达式
 */
@Component
@ConfigurationProperties(prefix = "hworld.param-trim")
public class HttpParamTrimProperties {

  /**
   * 需要进行参数trim处理的包前缀列表 默认包含"com.hworld"包前缀
   */
  private List<String> packagePrefixes = List.of("com.hworld");

  /**
   * 控制器切点表达式 用于指定哪些控制器方法需要应用参数trim处理 默认值为匹配com.hworld.custom.controller 匹配包及其子包中的所有方法
   */
  private String controllerPointcut = "execution(* com.hworld.custom.controller..*(..))";

  /**
   * 定义系统类包名前缀列表
   */
  private static final String[] systemPackagePrefixes = {
      "java.",
      "javax.",
      "sun.",
      "com.sun.",
      "org.springframework",
      "com.fasterxml",
      "org.apache",
      "org.jboss",
      "com.google",
      "org.slf4j",
      "ch.qos.logback",
      "org.junit",
      "junit.",
      "org.mockito",
      "org.aspectj",
      "io.netty",
      "org.hibernate",
      "org.mybatis",
      "com.alibaba",
      "org.yaml",
      "org.dom4j",
      "org.xml",
      "org.json",
      "org.jsoup",
      "javax.servlet",
      "jakarta.servlet"
  };

  public String[] getSystemPackagePrefixes() {
    return systemPackagePrefixes;
  }

  /**
   * 获取需要进行参数trim处理的包前缀列表
   *
   * @return 包前缀列表
   */
  public List<String> getPackagePrefixes() {
    return packagePrefixes;
  }

  /**
   * 设置需要进行参数trim处理的包前缀列表
   *
   * @param packagePrefixes 包前缀列表
   */
  public void setPackagePrefixes(List<String> packagePrefixes) {
    this.packagePrefixes = packagePrefixes;
  }

  /**
   * 获取控制器切点表达式
   *
   * @return 控制器切点表达式
   */
  public String getControllerPointcut() {
    return controllerPointcut;
  }

  /**
   * 设置控制器切点表达式
   *
   * @param controllerPointcut 控制器切点表达式
   */
  public void setControllerPointcut(String controllerPointcut) {
    this.controllerPointcut = controllerPointcut;
  }
}

@ConfigurationProperties将配置项会被自动注入到 HttpParamTrimProperties 类的 packagePrefixes 和 controllerPointcut 字段中

yaml

hworld:
  param-trim:
    package-prefixes:
      - com.example
    controller-pointcut: execution(* com.hworld.custom.controller..*(..))
package com.hworld.custom.config;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * HTTP请求参数处理切面类
 * <p>
 * 该切面用于自动去除HTTP请求参数中字符串类型的首尾空格, 包括@RequestParam参数和@RequestBody对象中的字符串字段。
 * </p>
 *
 * @author hudy
 * @since 2025-07-08
 */
@Aspect
@Component
@Slf4j
public class HttpParamTrimAspect {

  @Autowired
  private HttpParamTrimProperties properties;

  /**
   * 简单类型集合,用于区分简单类型和复杂对象
   */
  private static final Set<Class<?>> SIMPLE_TYPES = new HashSet<>(Arrays.asList(
      String.class, Boolean.class, Character.class, Byte.class, Short.class,
      Integer.class, Long.class, Float.class, Double.class, Void.class,
      boolean.class, char.class, byte.class, short.class, int.class,
      long.class, float.class, double.class, void.class
  ));

  /**
   * 定义系统类包名前缀列表
   */
  private static final String[] SYSTEM_PACKAGE_PREFIXES = {
      "java.",
      "javax.",
      "sun.",
      "com.sun.",
      "org.springframework",
      "com.fasterxml",
      "org.apache",
      "org.jboss",
      "com.google",
      "org.slf4j",
      "ch.qos.logback",
      "org.junit",
      "junit.",
      "org.mockito",
      "org.aspectj",
      "io.netty",
      "org.hibernate",
      "org.mybatis",
      "com.alibaba",
      "org.yaml",
      "org.dom4j",
      "org.xml",
      "org.json",
      "org.jsoup",
      "javax.servlet",
      "jakarta.servlet"
  };

  /**
   * 缓存字段信息以提高性能,避免重复反射操作
   * Key: Class对象, Value: 该类中所有声明的字段
   * 使用LRU策略限制缓存大小,避免内存泄漏
   */
  private static final Map<Class<?>, Field[]> FIELDS_CACHE = new ConcurrentHashMap<>();

  /**
   * 缓存最大大小
   */
  private static final int MAX_CACHE_SIZE = 1000;

  /**
   * 拦截指定Controller包下的所有方法,对请求参数进行处理
   *
   * @param joinPoint 连接点对象
   * @return 原方法执行结果
   * @throws Throwable 可能抛出的异常
   */
  @Around("controllerPointcut()")
  public Object trimRequestParams(ProceedingJoinPoint joinPoint) throws Throwable {
    return processRequestParams(joinPoint);
  }

  /**
   * 虽然注解中写的是固定的表达式,但实际上它会返回通过配置来改变实际的切点
   */
  @Pointcut("execution(* net.icsoc.ark.api.controller..*(..))")
  private void controllerPointcut() {
    // 方法体为空,仅用于定义切点
  }

  /**
   * 实际处理请求参数的方法
   *
   * @param joinPoint 连接点对象
   * @return 原方法执行结果
   * @throws Throwable 可能抛出的异常
   */
  private Object processRequestParams(ProceedingJoinPoint joinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    String methodSignature = "unknown";

    try {
      methodSignature = joinPoint.getSignature().toShortString();
      log.debug("开始处理请求参数,方法: {}", methodSignature);

      Object[] args = joinPoint.getArgs();

      if (args == null || args.length == 0) {
        log.debug("方法 {} 无参数,直接执行", methodSignature);
        Object result = joinPoint.proceed();
        logExecutionTime(methodSignature, startTime);
        return result;
      }

      // 获取方法参数注解
      Annotation[][] parameterAnnotations = getParameterAnnotations(joinPoint);

      // 处理每个参数
      int processedCount = 0;
      for (int i = 0; i < args.length; i++) {
        try {
          Object arg = args[i];
          if (arg != null) {
            // 获取当前参数的注解
            Annotation[] paramAnnotations =
                (parameterAnnotations != null && i < parameterAnnotations.length)
                    ? parameterAnnotations[i]
                    : new Annotation[0];

            // 处理参数
            Object processedArg = processParameter(arg, paramAnnotations);
            if (processedArg != arg) {
              args[i] = processedArg;
              processedCount++;
              log.debug("处理了参数[{}],类型: {}", i, arg.getClass().getSimpleName());
            }
          }
        } catch (Exception e) {
          log.warn("处理参数[{}]时发生异常: {}", i, e.getMessage(), e);
          // 发生异常时不中断流程,继续处理其他参数
        } catch (Throwable t) {
          log.error("处理参数[{}]时发生严重错误: {}", i, t.getMessage(), t);
          // 对于严重错误,仍然不中断流程
        }
      }

      if (processedCount > 0) {
        log.debug("共处理了 {} 个参数", processedCount);
      }

      // 继续执行原方法
      Object result = joinPoint.proceed(args);
      logExecutionTime(methodSignature, startTime);
      return result;

    } catch (Exception e) {
      log.error("请求参数处理切面执行异常,方法: {}", methodSignature, e);
      // 切面执行异常时,直接执行原方法
      Object result = joinPoint.proceed();
      logExecutionTime(methodSignature, startTime);
      return result;
    } catch (Throwable t) {
      log.error("请求参数处理切面执行严重错误,方法: {}", methodSignature, t);
      // 对于严重错误,也尝试执行原方法
      try {
        Object result = joinPoint.proceed();
        logExecutionTime(methodSignature, startTime);
        return result;
      } catch (Throwable innerThrowable) {
        log.error("执行原方法也发生错误,方法: {}", methodSignature, innerThrowable);
        throw innerThrowable;
      }
    }
  }

  /**
   * 记录方法执行时间
   *
   * @param methodSignature 方法签名
   * @param startTime       开始时间
   */
  private void logExecutionTime(String methodSignature, long startTime) {
    long executionTime = System.currentTimeMillis() - startTime;
    if (executionTime > 100) {
      log.warn("方法 {} 执行时间较长: {}ms", methodSignature, executionTime);
    } else if (log.isDebugEnabled()) {
      log.debug("方法 {} 执行完成,耗时: {}ms", methodSignature, executionTime);
    }
  }

  /**
   * 获取方法参数注解数组
   *
   * @param joinPoint 连接点对象
   * @return 参数注解数组
   */
  private Annotation[][] getParameterAnnotations(ProceedingJoinPoint joinPoint) {
    try {
      MethodSignature signature = (MethodSignature) joinPoint.getSignature();
      Annotation[][] annotations = signature.getMethod().getParameterAnnotations();
      log.debug("成功获取方法 {} 的参数注解", signature.toShortString());
      return annotations;
    } catch (Exception e) {
      log.warn("获取方法参数注解失败: {}", e.getMessage());
      return new Annotation[0][0]; // 返回空数组而不是null,避免null检查
    } catch (Throwable t) {
      log.error("获取方法参数注解时发生严重错误: {}", t.getMessage(), t);
      return new Annotation[0][0];
    }
  }

  /**
   * 处理单个参数,根据参数类型和注解类型进行不同的处理
   *
   * @param arg              参数对象
   * @param paramAnnotations 参数注解数组
   * @return 处理后的参数对象
   */
  private Object processParameter(Object arg, Annotation[] paramAnnotations) {
    try {
      // 检查是否有@RequestParam注解
      boolean hasRequestParam = hasAnnotation(paramAnnotations, RequestParam.class);
      // 检查是否有@RequestBody注解
      boolean hasRequestBody = hasAnnotation(paramAnnotations, RequestBody.class);

      if (arg instanceof String) {
        // 处理字符串参数(包括@RequestParam)
        String original = (String) arg;
        String trimmed = trimString(original);
        if (!original.equals(trimmed)) {
          log.debug("字符串参数已去除首尾空格: '{}' -> '{}'", original, trimmed);
        }
        return trimmed;
      } else if (hasRequestBody || (!hasRequestParam && isComplexObject(arg))) {
        // 处理@RequestBody对象或未注解的复杂对象
        log.debug("处理复杂对象类型: {}", arg.getClass().getSimpleName());
        trimStringFields(arg);
        return arg;
      } else {
        // 其他情况不处理
        log.debug("跳过处理简单类型参数: {}", arg.getClass().getSimpleName());
        return arg;
      }
    } catch (Exception e) {
      log.warn("处理参数时发生异常,参数类型: {}", arg != null ? arg.getClass().getSimpleName() : "null", e);
      // 发生异常时返回原参数
      return arg;
    } catch (Throwable t) {
      log.error("处理参数时发生严重错误,参数类型: {}", arg != null ? arg.getClass().getSimpleName() : "null", t);
      return arg;
    }
  }

  /**
   * 检查注解数组中是否包含指定注解类型
   *
   * @param annotations     注解数组
   * @param annotationClass 目标注解类型
   * @return 是否包含指定注解
   */
  private boolean hasAnnotation(Annotation[] annotations,
      Class<? extends Annotation> annotationClass) {
    if (annotations == null || annotations.length == 0) {
      return false;
    }

    try {
      for (Annotation annotation : annotations) {
        if (annotation == null) {
          continue; // 跳过null注解
        }
        if (annotationClass.isInstance(annotation)) {
          return true;
        }
      }
    } catch (Exception e) {
      log.warn("检查注解时发生异常: {}", e.getMessage());
    } catch (Throwable t) {
      log.error("检查注解时发生严重错误: {}", t.getMessage(), t);
    }
    return false;
  }

  /**
   * 判断是否为复杂对象(非简单类型)
   *
   * @param obj 待判断的对象
   * @return 是否为复杂对象
   */
  private boolean isComplexObject(Object obj) {
    if (obj == null) {
      return false;
    }

    try {
      Class<?> clazz = obj.getClass();

      // 检查是否为简单类型
      if (SIMPLE_TYPES.contains(clazz)) {
        return false;
      }

      // 检查是否为枚举
      if (clazz.isEnum()) {
        return false;
      }

      // 检查是否为数组且元素为简单类型
      if (clazz.isArray()) {
        Class<?> componentType = clazz.getComponentType();
        return !SIMPLE_TYPES.contains(componentType) && !componentType.isEnum();
      }

      // 其他情况视为复杂对象
      return true;
    } catch (Exception e) {
      log.warn("判断对象类型时发生异常: {}", e.getMessage());
      return false;
    } catch (Throwable t) {
      log.error("判断对象类型时发生严重错误: {}", t.getMessage(), t);
      return false;
    }
  }

  /**
   * 去除字符串首尾空格
   *
   * @param str 原始字符串
   * @return 去除首尾空格后的字符串
   */
  private String trimString(String str) {
    try {
      if (str == null) {
        return null;
      }

      // 允许空字符串,只去除首尾空格
      String trimmed = str.trim();

      // 记录处理日志(仅在需要处理时)
      if (!trimmed.equals(str)) {
        log.trace("字符串去除空格: '{}' -> '{}'", str, trimmed);
      }

      return trimmed;
    } catch (Exception e) {
      log.warn("处理字符串时发生异常: {}", e.getMessage());
      // 发生异常时返回原字符串
      return str;
    } catch (Throwable t) {
      log.error("处理字符串时发生严重错误: {}", t.getMessage(), t);
      return str;
    }
  }

  /**
   * 递归处理对象中的String类型字段,去除首尾空格
   *
   * @param obj 需要处理的对象
   */
  private void trimStringFields(Object obj) {
    if (obj == null) {
      return;
    }

    try {
      Class<?> clazz = obj.getClass();

      // 只处理自定义的DTO类,避免处理JDK内置类和第三方库类
      if (!shouldProcessClass(clazz)) {
        log.debug("跳过处理类: {}", clazz.getName());
        return;
      }

      // 从缓存中获取字段,避免重复反射操作
      Field[] fields = FIELDS_CACHE.computeIfAbsent(clazz, k -> {
        // 如果缓存已满,清理最早的一些条目
        if (FIELDS_CACHE.size() > MAX_CACHE_SIZE) {
          FIELDS_CACHE.entrySet().iterator().remove();
        }
        return k.getDeclaredFields();
      });
      int processedFieldCount = 0;

      for (Field field : fields) {
        try {
          if (field.getType() == String.class) {
            boolean processed = processStringField(obj, field);
            if (processed) {
              processedFieldCount++;
            }
          }
        } catch (Exception e) {
          log.warn("处理字段[{}]时发生异常: {}", field.getName(), e.getMessage(), e);
          // 继续处理其他字段
        } catch (Throwable t) {
          log.error("处理字段[{}]时发生严重错误: {}", field.getName(), t.getMessage(), t);
        }
      }

      if (processedFieldCount > 0 && log.isDebugEnabled()) {
        log.debug("处理了 {} 个字符串字段,对象类型: {}", processedFieldCount, clazz.getSimpleName());
      }
    } catch (Exception e) {
      log.warn("处理对象字符串字段时发生异常,对象类型: {}", obj.getClass().getSimpleName(), e);
    } catch (Throwable t) {
      log.error("处理对象字符串字段时发生严重错误,对象类型: {}", obj.getClass().getSimpleName(), t);
    }
  }

  /**
   * 判断是否应该处理该类的字符串字段
   *
   * @param clazz 类对象
   * @return 是否应该处理
   */
  private boolean shouldProcessClass(Class<?> clazz) {
    try {
      // 空包名检查
      Package pkg = clazz.getPackage();
      if (pkg == null) {
        if (log.isTraceEnabled()) {
          log.trace("类 {} 无包名,跳过处理", clazz.getName());
        }
        return false;
      }

      String packageName = pkg.getName();
      // 检查是否为系统类
      for (String prefix : SYSTEM_PACKAGE_PREFIXES) {
        if (packageName.startsWith(prefix)) {
          return false;
        }
      }

      // 使用配置的包前缀列表判断是否应该处理
      List<String> packagePrefixes = properties.getPackagePrefixes();
      boolean shouldProcess = packagePrefixes.stream()
          .anyMatch(prefix -> packageName.startsWith(prefix.trim()));

      // 同时保留原有的包含特定关键词的判断逻辑
      shouldProcess = shouldProcess
          || packageName.contains("dto")
          || packageName.contains("vo")
          || packageName.contains("model")
          || packageName.contains("entity");

      if (!shouldProcess) {
        if (log.isTraceEnabled()) {
          log.trace("类 {} 不在处理范围内", clazz.getName());
        }
      }

      return shouldProcess;
    } catch (Exception e) {
      log.warn("判断类是否需要处理时发生异常: {}", e.getMessage());
      return false;
    } catch (Throwable t) {
      log.error("判断类是否需要处理时发生严重错误: {}", t.getMessage(), t);
      return false;
    }
  }

  /**
   * 处理对象中的字符串字段,去除首尾空格
   *
   * @param obj   对象实例
   * @param field 字段对象
   * @return 是否进行了处理
   */
  private boolean processStringField(Object obj, Field field) {
    boolean originalAccessible = field.isAccessible();
    try {
      field.setAccessible(true);
      Object value = field.get(obj);

      if (value instanceof String) {
        String strValue = (String) value;
        String trimmedValue = trimString(strValue);

        // 只有在值发生变化时才设置新值
        if (trimmedValue != strValue && (trimmedValue == null || !trimmedValue.equals(strValue))) {
          field.set(obj, trimmedValue);
          if (log.isTraceEnabled()) {
            log.trace("字段 {} 已去除首尾空格: '{}' -> '{}'", field.getName(), strValue, trimmedValue);
          }
          return true;
        }
      }

      return false;
    } catch (IllegalAccessException e) {
      log.warn("无法访问字段: {}", field.getName(), e);
      return false;
    } catch (Exception e) {
      log.warn("处理字符串字段[{}]时发生异常: {}", field.getName(), e.getMessage(), e);
      return false;
    } catch (Throwable t) {
      log.error("处理字符串字段[{}]时发生严重错误: {}", field.getName(), t.getMessage(), t);
      return false;
    } finally {
      // 恢复原始访问状态,避免内存泄漏
      field.setAccessible(originalAccessible);
    }
  }
}

上述代码中,切面会拦截 com.hworld.custom.controller 包下所有类的所有方法,无论这些方法映射到哪种 HTTP 方法(GET、POST、PUT、DELETE等)对字符串参数进行 trim 处理

应用场景4:数据源自动切换

不同的功能模块读取不同的数据库,比如订单模块读取订单数据库。用户中心模块读取用户中心数据库。

SpringBoot多数据源配置_多数据源配置springboot-优快云博客

应用场景5:权限校验切面

对需要特定权限的接口进行统一权限校验

自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequirePermission {
    String value() default "";
}

切面实现

@Aspect
@Component
@Slf4j
public class PermissionAspect {

    @Autowired
    private HotelJobService hotelJobService;

    @Pointcut("@annotation(requirePermission)")
    public void permissionCheck(RequirePermission requirePermission) {}

    @Around("permissionCheck(requirePermission)")
    public Object checkPermission(ProceedingJoinPoint joinPoint, RequirePermission requirePermission) throws Throwable {
        BasicUserInfo userInfo = (BasicUserInfo) SecurityUtils.getUserPrincipal();
        if (userInfo != null && !"admin".equals(userInfo.getAdmin())) {
            List<HotelVo> hotelVos = hotelJobService.getPermissionHotelInfos(null);
            if (CollectionUtils.isEmpty(hotelVos)) {
                String errorMessage = "没有权限访问酒店列表";
                log.info(errorMessage);
                return WebResponse.builder()
                        .code(WebResponse.WebResponseCode.HOTEL_AUTH_EXCEPTION.getCode())
                        .message(errorMessage)
                        .data(null)
                        .build();
            }
        }
        
        return joinPoint.proceed();
    }
}

使用方法

@PostMapping("/export")
@RequirePermission("cdr_export")
@Operation(summary = "导出通话记录", description = "导出通话记录 Excel 文件")
public void export(@RequestBody BillCdrTrunkListDTO dto, HttpServletResponse response) {
    // 方法实现
}

应用场景6:接口幂等性切面

防止重复提交,特别是导出等操作

自定义注解

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 接口幂等性切面
 *
 * @author hudy
 * @date 2025/8/7
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {

  String value() default "";
}

切面实现

@Aspect
@Component
@Slf4j
public class IdempotentAspect {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Around("@annotation(idempotent)")
    public Object handleIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        BasicUserInfo userInfo = (BasicUserInfo) SecurityUtils.getUserPrincipal();
        String requestId = RequestContextHolder.currentRequestAttributes()
                .getAttribute("requestId", RequestAttributes.SCOPE_REQUEST).toString();
        
        String key = "idempotent:" + userInfo.getId() + ":" + requestId;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
            throw new BussinessException("请求处理中,请勿重复提交");
        }
        
        try {
            // 设置防重标识,5分钟过期
            redisTemplate.opsForValue().set(key, "1", 5, TimeUnit.MINUTES);
            return joinPoint.proceed();
        } finally {
            // 可选:在处理完成后删除标识
            // redisTemplate.delete(key);
        }
    }
}

使用示例

@PostMapping("/asyncExport")
@Idempotent
public WebResponse<Object> asyncExport(@RequestBody BillCdrTrunkListDTO dto) {
    // 方法实现
}

应用场景7:限流切面

防止接口被频繁调用

自定义注解

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author hudy
 * @date 2025/8/7
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {

  int limit() default 10; // 默认每分钟10次

  int timeout() default 60; // 时间窗口,单位秒
}

切面实现

@Aspect
@Component
@Slf4j
public class RateLimiterAspect {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    @Before("@annotation(rateLimiter)")
    public void checkRateLimit(JoinPoint joinPoint, RateLimiter rateLimiter) {
        BasicUserInfo userInfo = (BasicUserInfo) SecurityUtils.getUserPrincipal();
        if (userInfo == null) return;

        String key = "rate_limit:" + userInfo.getId() + ":" + 
                    joinPoint.getSignature().getDeclaringType().getSimpleName() + 
                    ":" + joinPoint.getSignature().getName();
        
        Long current = redisTemplate.boundValueOps(key).increment();
        if (current != null && current == 1) {
            redisTemplate.expire(key, rateLimiter.timeout(), TimeUnit.SECONDS);
        }
        
        if (current != null && current > rateLimiter.limit()) {
            throw new BussinessException("操作过于频繁,请稍后再试");
        }
    }
}

使用例子

// 限制每个用户每分钟最多调用20次列表查询
@RateLimiter(limit = 20, timeout = 60)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值