
告别重复代码!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自动、统一地处理!
四、实战二:优雅实现权限校验
权限校验是另一个典型的横切关注点。
- 定义一个权限注解:
java
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 该注解可以放在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Documented
public @interface PreAuthorize {
String value(); // 用于接收权限字符串,例如 "hasRole('ADMIN')"
}
- 编写权限校验切面:
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);
}
}
}
}
- 在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:
- 一键解耦了日志记录: WebLogAspect
- 优雅实现了权限控制: AuthorizationAspect + 自定义注解@PreAuthorize
- 统一了异常响应格式: GlobalExceptionHandler(与AOP完美互补)
最佳实践:
- 切点表达式: 尽量准确,避免匹配到不需要的方法(如使用具体的包路径execution(* com.xxx.controller..*(..))或自定义注解@annotation)。
- 通知类型选择:
- @Around功能最强劲,可以控制是否执行目标方法,适合日志、性能监控、事务等。
- @Before适合权限校验、参数校验等。
- @AfterReturning适合在方法成功返回后处理返回结果。
- @AfterThrowing适合处理特定异常(但一般全局异常处理器更强劲)。
- @After类似于finally,无论成功失败都会执行。
- 性能: AOP会带来轻微的代理开销,但对于日志、权限等操作,这些开销一般是完全可以接受的。
从此,你的业务代码将只关注业务逻辑(What),而将那些繁琐的通用功能交给切面(How),真正实现了关注点分离,代码可读性和可维护性大幅提升!



谢谢
不错
收藏了,感谢分享