告别重复代码,Spring Boot AOP 实战:一键搞定日志、权限与全局异常

告别重复代码,Spring Boot AOP 实战:一键搞定日志、权限与全局异常

告别重复代码!Spring Boot AOP 实战:一键搞定日志、权限与全局异常

你是否曾在Spring Boot项目中写过这样的代码?

java

@RestController
public class UserController {

    @GetMapping("/user/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        // 1. 打印入参日志
        log.info("GET /user/{} called", id);
        try {
            // 2. 权限校验(重复出目前每个方法里)
            if (!hasPermission()) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
            }
            // 3. 核心业务逻辑
            User user = userService.findById(id);
            // 4. 打印出参日志
            log.info("GET /user/{} response: {}", id, user);
            return ResponseEntity.ok(user);
        } catch (Exception e) {
            // 5. 异常处理(又是重复代码)
            log.error("Error getting user by id: {}", id, e);
            return ResponseEntity.internalServerError().build();
        }
    }

    // 其他方法里充斥着同样的日志、权限、异常代码...
    @PostMapping("/user")
    public ResponseEntity<User> createUser(@RequestBody User user) {
        log.info("POST /user called with body: {}", user);
        try {
            if (!hasPermission()) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
            }
            User savedUser = userService.save(user);
            log.info("POST /user response: {}", savedUser);
            return ResponseEntity.ok(savedUser);
        } catch (Exception e) {
            log.error("Error creating user", e);
            return ResponseEntity.internalServerError().build();
        }
    }

    private boolean hasPermission() {
        // 模拟权限校验
        return true;
    }
}

上面的代码存在典型的代码重复职责不清问题。控制器本该只关注处理HTTP请求和响应,目前却混杂了日志、权限、异常处理等横切关注点,导致代码臃肿、难以维护。

如何优雅地解决? 答案就是 AOP(面向切面编程)

一、什么是AOP?为什么是Spring AOP?

AOP(Aspect-Oriented Programming) 是一种编程范式,旨在将那些分散在多个类或方法中的横切关注点(如日志、事务、安全等)从业务逻辑中分离出来,实现关注点的分离,从而让代码更加清爽、可维护。

Spring AOP 是Spring框架对AOP的实现,它通过代理模式在运行期为目标对象生成代理,从而将切面织入到目标方法中。它超级适用于处理Spring管理的Bean(如@Controller, @Service等)。

二、实战准备:添加依赖与基本概念

第一,在pom.xml中添加Spring Boot AOP依赖(一般已被包含在spring-boot-starter-web中,但显式声明是个好习惯):

xml

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

核心概念速览:

  • Aspect(切面): 一个模块化的横切关注点,即我们将要编写的包含通知和切点的类(用@Aspect注解标注)。
  • Advice(通知): 切面在特定连接点上执行的动作(如@Before, @After, @Around等)。
  • Pointcut(切点): 一个匹配连接点的表达式,定义了通知何时被触发。
  • Join Point(连接点): 程序执行过程中的一个点,如方法调用或异常抛出。在Spring AOP中,它总是代表一个方法的执行。

三、实战一:一键统一日志记录

让我们用AOP重构上面繁琐的日志代码。

java

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.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

@Aspect // 标记这是一个切面类
@Component // 让Spring管理这个Bean
@Slf4j
public class WebLogAspect {

    // 定义切点:匹配所有Controller包下的所有方法
    @Pointcut("execution(* com.yourproject.controller..*.*(..))")
    public void webLog() {}

    // 环绕通知:在目标方法执行前后都执行
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取当前请求对象(可选,用于记录URL、IP等)
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 记录请求信息
        log.info("URL: {}", request.getRequestURL().toString());
        log.info("HTTP Method: {}", request.getMethod());
        log.info("Class Method: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        log.info("IP: {}", request.getRemoteAddr());
        log.info("Request Args: {}", Arrays.toString(joinPoint.getArgs()));

        long startTime = System.currentTimeMillis();
        // 执行目标方法,并获取返回值
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();

        // 记录响应信息和方法耗时
        log.info("Response: {}", result);
        log.info("Time Cost: {} ms", (endTime - startTime));

        return result; // 返回目标方法的执行结果
    }
}

目前,你的Controller可以变得无比纯净:

java

@RestController
public class UserController {

    @GetMapping("/user/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        // 只需关注核心业务
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }

    @PostMapping("/user")
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User savedUser = userService.save(user);
        return ResponseEntity.ok(savedUser);
    }
}

所有日志都会由WebLogAspect自动、统一地处理!

四、实战二:优雅实现权限校验

权限校验是另一个典型的横切关注点。

  1. 定义一个权限注解

java

import java.lang.annotation.*;

@Target(ElementType.METHOD) // 该注解可以放在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Documented
public @interface PreAuthorize {
    String value(); // 用于接收权限字符串,例如 "hasRole('ADMIN')"
}
  1. 编写权限校验切面

java

@Aspect
@Component
public class AuthorizationAspect {

    @Autowired
    private YourAuthService authService; // 你自定义的权限校验服务

    // 定义切点:匹配所有被@PreAuthorize注解标记的方法
    @Pointcut("@annotation(com.yourproject.annotation.PreAuthorize)")
    public void authorizePointcut() {}

    @Before("authorizePointcut()") // 在目标方法执行前进行权限校验
    public void doBefore(JoinPoint joinPoint) {
        // 从方法上获取注解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);

        if (preAuthorize != null) {
            String permission = preAuthorize.value();
            // 调用你的权限校验服务
            if (!authService.checkPermission(permission)) {
                // 如果没有权限,直接抛出异常,由全局异常处理器接管
                throw new UnauthorizedException("Permission Denied: " + permission);
            }
        }
    }
}
  1. 在Controller方法上使用

java

@RestController
public class UserController {

    @PreAuthorize("hasRole('ADMIN')") // 只需一个注解
    @DeleteMapping("/user/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
        return ResponseEntity.ok().build();
    }
}

五、实战三:强化全局异常处理

虽然@RestControllerAdvice本身不是AOP(它基于@ExceptionHandler),但它与AOP的理念一致,都是聚焦处理横切关注点。结合AOP抛出的异常,可以构建强劲的错误处理机制。

java

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice // 这是一个增强的全局异常处理组件
public class GlobalExceptionHandler {

    // 处理权限切面抛出的异常
    @ExceptionHandler(UnauthorizedException.class)
    public ResponseEntity<String> handleUnauthorizedException(UnauthorizedException e) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage());
    }

    // 处理所有其他未明确处理的异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGlobalException(Exception e) {
        // 这里可以记录详细的错误日志
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server Internal Error");
    }
}

目前,权限校验失败会抛出UnauthorizedException,然后被GlobalExceptionHandler捕获并返回统一的403格式,无需在每个Controller方法中写try-catch

总结与最佳实践

通过以上三个实战,我们成功地用Spring Boot AOP:

  1. 一键解耦了日志记录WebLogAspect
  2. 优雅实现了权限控制AuthorizationAspect + 自定义注解@PreAuthorize
  3. 统一了异常响应格式GlobalExceptionHandler(与AOP完美互补)

最佳实践:

  • 切点表达式: 尽量准确,避免匹配到不需要的方法(如使用具体的包路径execution(* com.xxx.controller..*(..))或自定义注解@annotation)。
  • 通知类型选择
    • @Around功能最强劲,可以控制是否执行目标方法,适合日志、性能监控、事务等。
    • @Before适合权限校验、参数校验等。
    • @AfterReturning适合在方法成功返回后处理返回结果。
    • @AfterThrowing适合处理特定异常(但一般全局异常处理器更强劲)。
    • @After类似于finally,无论成功失败都会执行。
  • 性能: AOP会带来轻微的代理开销,但对于日志、权限等操作,这些开销一般是完全可以接受的。

从此,你的业务代码将只关注业务逻辑(What),而将那些繁琐的通用功能交给切面(How),真正实现了关注点分离,代码可读性和可维护性大幅提升!

© 版权声明

相关文章

3 条评论

您必须登录才能参与评论!
立即登录