Spring Boot 工程化进阶:统一返回 + 全局异常 + AOP 通用工具包

一、前言

前面我们已经学过:

Spring AOP 从原理到实战
Spring AOP 进阶:日志、TraceId、权限、限流

这一篇继续升级。

这篇的目标不是单独讲某个注解,而是把项目中常见的基础能力做成一套可复制的工程模板:

统一返回 Result

业务异常 BizException

全局异常处理 GlobalExceptionHandler

AOP 通知类型工具箱

TraceId 链路追踪

请求日志 AOP

操作日志 AOP

权限校验 AOP

限流 AOP

日志通用工具类

最终效果:

Controller 专注接收请求

Service / Biz 专注业务逻辑

Repository 专注数据访问

shared 负责基础设施能力


二、推荐项目结构

复制代码
project
│
├── modules
│ ├── user
│ ├── order
│ ├── pay
│ ├── ai
│ └── inventory
│
├── gateway
│
├── shared
│ ├── result
│ │ ├── Result.java
│ │ └── ResultCodeEnum.java
│ │
│ ├── exception
│ │ ├── BizException.java
│ │ └── GlobalExceptionHandler.java
│ │
│ ├── annotation
│ │ ├── OperationLog.java
│ │ ├── CheckPermission.java
│ │ └── RateLimit.java
│ │
│ ├── aop
│ │ ├── BeforeLogAspect.java
│ │ ├── AfterReturningLogAspect.java
│ │ ├── AfterThrowingLogAspect.java
│ │ ├── AfterLogAspect.java
│ │ ├── TraceIdAspect.java
│ │ ├── RequestLogAspect.java
│ │ ├── OperationLogAspect.java
│ │ ├── PermissionAspect.java
│ │ └── RateLimitAspect.java
│ │
│ └── util
│ ├── JsonUtils.java
│ ├── WebUtils.java
│ ├── TraceIdUtils.java
│ └── LogUtils.java
│
├── scheduler
├── resources
└── ProjectApplication.java

核心原则:

业务代码放 modules

通用能力放 shared

AOP、异常、返回、工具类都属于 shared


三、统一返回 Result

1. ResultCodeEnum

java 复制代码
package com.xxx.shared.result;

public enum ResultCodeEnum {

SUCCESS(0, "成功"),

PARAM_ERROR(1001, "参数错误"),
USER_NOT_FOUND(1002, "用户不存在"),
USERNAME_EXIST(1003, "用户名已存在"),
NO_PERMISSION(1004, "没有权限"),
RATE_LIMIT(1005, "请求过于频繁"),

SYSTEM_ERROR(5000, "系统异常");

private final Integer code;
private final String message;

ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}

public Integer getCode() {
return code;
}

public String getMessage() {
return message;
}
}

2. Result

java 复制代码
package com.xxx.shared.result;

public class Result<T> {

private Integer code;
private String message;
private T data;

public Result() {
}

private Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}

public static <T> Result<T> success() {
return new Result<>(
ResultCodeEnum.SUCCESS.getCode(),
ResultCodeEnum.SUCCESS.getMessage(),
null
);
}

public static <T> Result<T> success(T data) {
return new Result<>(
ResultCodeEnum.SUCCESS.getCode(),
ResultCodeEnum.SUCCESS.getMessage(),
data
);
}

public static <T> Result<T> fail(ResultCodeEnum codeEnum) {
return new Result<>(
codeEnum.getCode(),
codeEnum.getMessage(),
null
);
}

public static <T> Result<T> fail(ResultCodeEnum codeEnum, String message) {
return new Result<>(
codeEnum.getCode(),
message,
null
);
}

public static <T> Result<T> fail(Integer code, String message) {
return new Result<>(
code,
message,
null
);
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}
}

Controller 中使用:

java 复制代码
@GetMapping("/info/{id}")
public Result<UserResponse> getUser(@PathVariable Long id) {
UserResponse response = userQueryExecutor.getUser(id);
return Result.success(response);
}

统一返回:

{

"code": 0,

"message": "成功",

"data": {

"id": 1,

"username": "admin"

}

}


四、业务异常 BizException

java 复制代码
package com.xxx.shared.exception;

import com.xxx.shared.result.ResultCodeEnum;

public class BizException extends RuntimeException {

private final Integer code;

public BizException(ResultCodeEnum codeEnum) {
super(codeEnum.getMessage());
this.code = codeEnum.getCode();
}

public BizException(ResultCodeEnum codeEnum, String message) {
super(message);
this.code = codeEnum.getCode();
}

public BizException(Integer code, String message) {
super(message);
this.code = code;
}

public Integer getCode() {
return code;
}
}

业务中使用:

java 复制代码
if (user == null) {
throw new BizException(ResultCodeEnum.USER_NOT_FOUND);
}

五、全局异常处理 GlobalExceptionHandler

java 复制代码
package com.xxx.shared.exception;

import com.xxx.shared.result.Result;
import com.xxx.shared.result.ResultCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(BizException.class)
public Result<Void> handleBizException(BizException e) {
log.warn("业务异常:code={}, message={}", e.getCode(), e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult()
.getFieldErrors()
.stream()
.findFirst()
.map(error -> error.getField() + " " + error.getDefaultMessage())
.orElse("参数校验失败");

log.warn("参数校验异常:{}", message);
return Result.fail(ResultCodeEnum.PARAM_ERROR, message);
}

@ExceptionHandler(BindException.class)
public Result<Void> handleBindException(BindException e) {
String message = e.getBindingResult()
.getFieldErrors()
.stream()
.findFirst()
.map(error -> error.getField() + " " + error.getDefaultMessage())
.orElse("参数绑定失败");

log.warn("参数绑定异常:{}", message);
return Result.fail(ResultCodeEnum.PARAM_ERROR, message);
}

@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常:", e);
return Result.fail(ResultCodeEnum.SYSTEM_ERROR);
}
}

这样业务异常会统一变成:

{

"code": 1002,

"message": "用户不存在",

"data": null

}


六、AOP 通知类型工具箱

这一章是补充重点。

AOP 不是只有 @Around,常见通知类型有:

@Before

@AfterReturning

@AfterThrowing

@After

@Around

它们分别适合不同场景。


1. @Before:方法执行前

适合:

  1. 方法进入日志
  2. 简单参数打印
  3. 简单前置检查

代码:

java 复制代码
package com.xxx.shared.aop;

import com.xxx.shared.util.JsonUtils;
import com.xxx.shared.util.LogUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
public class BeforeLogAspect {

@Before("execution(* com.xxx.modules..controller..*(..))")
public void before(JoinPoint joinPoint) {
Object[] args = LogUtils.filterArgs(joinPoint.getArgs());

log.info("Before:方法开始,method={}, args={}",
joinPoint.getSignature().toShortString(),
LogUtils.maskSensitive(JsonUtils.toJson(args)));
}
}

特点:

  1. 能在方法前执行
  2. 不能拿到返回值
  3. 不能统计完整耗时
  4. 不能决定方法是否执行

2. @AfterReturning:成功返回后

适合:

  1. 返回值日志
  2. 成功统计
  3. 成功后埋点

代码:

java 复制代码
package com.xxx.shared.aop;

import com.xxx.shared.util.JsonUtils;
import com.xxx.shared.util.LogUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
public class AfterReturningLogAspect {

@AfterReturning(
pointcut = "execution(* com.xxx.modules..controller..*(..))",
returning = "result"
)
public void afterReturning(JoinPoint joinPoint, Object result) {
log.info("AfterReturning:方法成功,method={}, result={}",
joinPoint.getSignature().toShortString(),
LogUtils.maskSensitive(JsonUtils.toJson(result)));
}
}

特点:

  1. 只在方法成功返回后执行
  2. 异常时不会执行
  3. 不能做异常日志
  4. 不能包住完整流程

3. @AfterThrowing:异常后

适合:

  1. 异常日志
  2. 异常统计
  3. 异常报警触发

代码:

java 复制代码
package com.xxx.shared.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
public class AfterThrowingLogAspect {

@AfterThrowing(
pointcut = "execution(* com.xxx.modules..controller..*(..))",
throwing = "e"
)
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
log.error("AfterThrowing:方法异常,method={}",
joinPoint.getSignature().toShortString(),
e);
}
}

特点:

  1. 只在异常时执行
  2. 适合记录异常
  3. 不能处理成功返回

4. @After:最终执行

适合:

  1. 资源清理
  2. ThreadLocal 清理
  3. MDC 清理
  4. 无论成功失败都要执行的逻辑

代码:

java 复制代码
package com.xxx.shared.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
public class AfterLogAspect {

@After("execution(* com.xxx.modules..controller..*(..))")
public void after(JoinPoint joinPoint) {
log.info("After:方法结束,method={}",
joinPoint.getSignature().toShortString());
}
}

特点:

  1. 无论成功失败都会执行
  2. 拿不到返回值
  3. 不知道是成功还是异常

5. @Around:完整包裹

适合:

  1. 请求日志
  2. 耗时统计
  3. 权限拦截
  4. 限流
  5. 重试
  6. 降级
  7. TraceId

代码:

java 复制代码
package com.xxx.shared.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
public class AroundDemoAspect {

@Around("execution(* com.xxx.modules..controller..*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();

try {
Object result = joinPoint.proceed();

long cost = System.currentTimeMillis() - start;

log.info("Around:方法成功,method={}, cost={}ms",
joinPoint.getSignature().toShortString(),
cost);

return result;

} catch (Throwable e) {
log.error("Around:方法异常,method={}",
joinPoint.getSignature().toShortString(),
e);
throw e;
}
}
}

特点:

  1. 最强
  2. 可以控制方法是否执行
  3. 可以拿到参数
  4. 可以拿到返回值
  5. 可以捕获异常
  6. 可以统计耗时
  7. 可以修改返回结果

6. 怎么选择通知类型

只想在执行前做点事

→ @Before

只关心成功返回

→ @AfterReturning

只关心异常

→ @AfterThrowing

无论成功失败都要收尾

→ @After

要控制完整流程

→ @Around

真实项目中:

请求日志、权限、限流、TraceId

优先用 @Around

原因是:

它能覆盖一次方法调用的完整生命周期


七、AOP 通用注解

tips:

注解(Annotation)

1. OperationLog

java 复制代码
package com.xxx.shared.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {

String value() default "";

String module() default "";

String action() default "";
}

2. CheckPermission

java 复制代码
package com.xxx.shared.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckPermission {

String value();
}

3. RateLimit

java 复制代码
package com.xxx.shared.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

int limit() default 10;
}

八、AOP 通用工具类

1. TraceIdUtils

java 复制代码
package com.xxx.shared.util;

import java.util.UUID;

public class TraceIdUtils {

public static final String TRACE_ID = "traceId";

private TraceIdUtils() {
}

public static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
}

2. JsonUtils

java 复制代码
package com.xxx.shared.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonUtils {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private JsonUtils() {
}

public static String toJson(Object obj) {
if (obj == null) {
return "null";
}

try {
return OBJECT_MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
return String.valueOf(obj);
}
}
}

3. WebUtils

java 复制代码
package com.xxx.shared.util;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class WebUtils {

private WebUtils() {
}

public static HttpServletRequest getRequest() {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

if (attributes == null) {
return null;
}

return attributes.getRequest();
}

public static String getRequestUri() {
HttpServletRequest request = getRequest();
return request == null ? "" : request.getRequestURI();
}

public static String getMethod() {
HttpServletRequest request = getRequest();
return request == null ? "" : request.getMethod();
}

public static String getIp() {
HttpServletRequest request = getRequest();

if (request == null) {
return "";
}

String ip = request.getHeader("X-Forwarded-For");

if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip.split(",")[0];
}

ip = request.getHeader("X-Real-IP");

if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}

return request.getRemoteAddr();
}
}

注意:

Spring Boot 3 使用 jakarta.servlet

Spring Boot 2 使用 javax.servlet


4. LogUtils

java 复制代码
package com.xxx.shared.util;

import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.springframework.web.multipart.MultipartFile;

import java.util.Arrays;

public class LogUtils {

private LogUtils() {
}

public static Object[] filterArgs(Object[] args) {
if (args == null) {
return new Object[0];
}

return Arrays.stream(args)
.filter(arg -> !(arg instanceof ServletRequest))
.filter(arg -> !(arg instanceof ServletResponse))
.filter(arg -> !(arg instanceof MultipartFile))
.toArray();
}

public static String maskSensitive(String text) {
if (text == null) {
return null;
}

return text
.replaceAll("(\"password\"\\s*:\\s*\").*?(\")", "$1******$2")
.replaceAll("(\"token\"\\s*:\\s*\").*?(\")", "$1******$2")
.replaceAll("(\"authorization\"\\s*:\\s*\").*?(\")", "$1******$2");
}
}

九、TraceIdAspect

java 复制代码
package com.xxx.shared.aop;

import com.xxx.shared.util.TraceIdUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1)
public class TraceIdAspect {

@Around("execution(* com.xxx.modules..controller..*(..))")
public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {

String traceId = TraceIdUtils.generateTraceId();
MDC.put(TraceIdUtils.TRACE_ID, traceId);

try {
return joinPoint.proceed();
} finally {
MDC.remove(TraceIdUtils.TRACE_ID);
}
}
}

说明:

TraceIdAspect 用 @Around

因为它需要:

  1. 方法执行前放入 traceId

  2. 方法执行后清理 traceId


十、RequestLogAspect

java 复制代码
package com.xxx.shared.aop;

import com.xxx.shared.util.JsonUtils;
import com.xxx.shared.util.LogUtils;
import com.xxx.shared.util.WebUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
@Order(2)
public class RequestLogAspect {

@Around("execution(* com.xxx.modules..controller..*(..))")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {

long start = System.currentTimeMillis();

String uri = WebUtils.getRequestUri();
String httpMethod = WebUtils.getMethod();
String ip = WebUtils.getIp();
String javaMethod = joinPoint.getSignature().toShortString();

Object[] filteredArgs = LogUtils.filterArgs(joinPoint.getArgs());
String argsJson = LogUtils.maskSensitive(JsonUtils.toJson(filteredArgs));

log.info("请求开始:uri={}, method={}, ip={}, javaMethod={}, args={}",
uri, httpMethod, ip, javaMethod, argsJson);

try {
Object result = joinPoint.proceed();

long cost = System.currentTimeMillis() - start;
String resultJson = LogUtils.maskSensitive(JsonUtils.toJson(result));

log.info("请求结束:uri={}, javaMethod={}, cost={}ms, result={}",
uri, javaMethod, cost, resultJson);

return result;

} catch (Throwable e) {
long cost = System.currentTimeMillis() - start;

log.error("请求异常:uri={}, javaMethod={}, cost={}ms",
uri, javaMethod, cost, e);

throw e;
}
}
}

说明:

  1. 请求日志用 @Around 最合适
  2. 因为它需要同时处理:
  3. 请求前
  4. 请求后
  5. 耗时
  6. 返回值
  7. 异常

十一、OperationLogAspect

java 复制代码
package com.xxx.shared.aop;

import com.xxx.shared.annotation.OperationLog;
import com.xxx.shared.util.WebUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
@Order(3)
public class OperationLogAspect {

@Around("@annotation(operationLog)")
public Object operationLog(ProceedingJoinPoint joinPoint,
OperationLog operationLog) throws Throwable {

long start = System.currentTimeMillis();

String module = operationLog.module();
String action = operationLog.action();
String value = operationLog.value();

try {
Object result = joinPoint.proceed();

long cost = System.currentTimeMillis() - start;

log.info("操作日志:module={}, action={}, value={}, uri={}, cost={}ms",
module, action, value, WebUtils.getRequestUri(), cost);

return result;

} catch (Throwable e) {
long cost = System.currentTimeMillis() - start;

log.error("操作异常:module={}, action={}, value={}, uri={}, cost={}ms",
module, action, value, WebUtils.getRequestUri(), cost, e);

throw e;
}
}
}

使用:

java 复制代码
@OperationLog(module = "用户模块", action = "创建用户")
@PostMapping("/create")
public Result<Void> createUser(@RequestBody CreateUserRequest request) {
userFacade.createUser(request);
return Result.success();
}

十二、PermissionAspect

java 复制代码
package com.xxx.shared.aop;

import com.xxx.shared.annotation.CheckPermission;
import com.xxx.shared.exception.BizException;
import com.xxx.shared.result.ResultCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
@Order(0)
public class PermissionAspect {

@Around("@annotation(checkPermission)")
public Object check(ProceedingJoinPoint joinPoint,
CheckPermission checkPermission) throws Throwable {

String permission = checkPermission.value();

if (!hasPermission(permission)) {
log.warn("权限校验失败:permission={}", permission);
throw new BizException(ResultCodeEnum.NO_PERMISSION);
}

return joinPoint.proceed();
}

private boolean hasPermission(String permission) {
// 这里先写死 true
// 后面可以从 JWT / Redis / 当前登录用户上下文中获取权限
return true;
}
}

为什么必须用 @Around

因为权限校验需要决定:

业务方法到底要不要执行


十三、RateLimitAspect

java 复制代码
package com.xxx.shared.aop;

import com.xxx.shared.annotation.RateLimit;
import com.xxx.shared.exception.BizException;
import com.xxx.shared.result.ResultCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Aspect
@Component
@Slf4j
@Order(0)
public class RateLimitAspect {

private final Map<String, AtomicInteger> counter = new ConcurrentHashMap<>();

@Around("@annotation(rateLimit)")
public Object limit(ProceedingJoinPoint joinPoint,
RateLimit rateLimit) throws Throwable {

String key = joinPoint.getSignature().toShortString();

counter.putIfAbsent(key, new AtomicInteger(0));

int count = counter.get(key).incrementAndGet();

if (count > rateLimit.limit()) {
log.warn("接口限流:key={}, count={}, limit={}",
key, count, rateLimit.limit());

throw new BizException(ResultCodeEnum.RATE_LIMIT);
}

return joinPoint.proceed();
}
}

为什么用 @Around

因为限流也要决定:

超过阈值时,业务方法不执行

注意:

这个是演示版

真实生产一般用 Redis + Lua / Sentinel / 网关限流


十四、logback 配置 traceId

路径:

resources/logback-spring.xml

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"/>

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>

</configuration>

日志效果:

2026-04-25 10:00:01.123 [http-nio-8080-exec-1] [9f8a7b6c] INFO RequestLogAspect - 请求开始...

2026-04-25 10:00:01.156 [http-nio-8080-exec-1] [9f8a7b6c] INFO RequestLogAspect - 请求结束...


十五、Controller 示例

java 复制代码
package com.xxx.modules.user.controller;

import com.xxx.shared.annotation.CheckPermission;
import com.xxx.shared.annotation.OperationLog;
import com.xxx.shared.annotation.RateLimit;
import com.xxx.shared.result.Result;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

@OperationLog(module = "用户模块", action = "查询用户")
@GetMapping("/info/{id}")
public Result<String> getUser(@PathVariable Long id) {
return Result.success("用户ID:" + id);
}

@CheckPermission("user:create")
@OperationLog(module = "用户模块", action = "创建用户")
@PostMapping("/create")
public Result<Void> createUser() {
return Result.success();
}

@RateLimit(limit = 5)
@GetMapping("/test-limit")
public Result<String> testLimit() {
return Result.success("ok");
}
}

十六、执行顺序说明

当多个 AOP 同时存在时,可以用 @Order 控制顺序。

@Order 数字越小,优先级越高

建议顺序:

java 复制代码
权限 / 限流
↓
TraceId
↓
请求日志
↓
操作日志
↓
业务方法

示例:

java 复制代码
@Order(0)
public class PermissionAspect {
}

@Order(1)
public class TraceIdAspect {
}

@Order(2)
public class RequestLogAspect {
}

十七、什么时候用哪个

1. @Before

简单前置动作

参数打印

方法进入日志

不适合:

权限拦截

限流

完整耗时统计


2. @AfterReturning

成功返回日志

成功统计

成功埋点

不适合:

异常处理

完整链路日志


3. @AfterThrowing

异常日志

异常统计

报警触发

不适合:

成功返回

控制业务流程


4. @After

最终清理

ThreadLocal 清理

MDC 清理

不适合:

返回值处理

异常分类处理


5. @Around

请求日志

耗时统计

权限拦截

限流

TraceId

重试

降级

真实项目里最常用。


十八、这套代码的价值

这套工程化代码解决的是:

  • 接口返回不统一
  • 异常处理混乱
  • 日志不好查
  • 没有请求链路
  • 权限判断散落
  • 限流逻辑侵入业务
  • AOP 通知类型不知道怎么选

完成后,项目结构会更清晰:

Controller:接请求

Facade / Executor:编排业务

Biz / Service:处理业务规则

Repository:处理数据

shared:处理基础设施能力


十九、面试怎么讲

可以这样说:

我在项目里做了一套基础工程化能力。

首先,接口统一使用 Result 返回结构,包含 code、message、data,方便前端统一处理。

其次,业务异常统一使用 BizException 抛出,再通过 @RestControllerAdvice 进行全局异常处理,把异常转换成统一返回。

然后,我用 AOP 做了请求日志和 TraceId。TraceId 通过 MDC 放入日志上下文,请求日志记录接口路径、请求方式、IP、参数、返回值、耗时和异常。

另外,我也梳理了 AOP 的几种通知类型:@Before 适合前置日志,@AfterReturning 适合成功返回统计,@AfterThrowing 适合异常日志,@After 适合清理资源,@Around 适合完整流程控制。

在实际项目里,权限校验、限流、请求日志、TraceId 这类需要控制完整流程的场景,我会优先使用 @Around。


二十、总结

这一篇的核心是:把基础能力工程化

几个核心关系:

java 复制代码
Result
= 统一接口返回

BizException
= 主动抛业务异常

GlobalExceptionHandler
= 统一处理异常

@Before / @AfterReturning / @AfterThrowing / @After
= AOP 分段增强

@Around
= AOP 完整流程控制

TraceId
= 串起一次请求的日志

RequestLogAspect
= 统一请求日志

OperationLogAspect
= 关键业务操作日志

PermissionAspect
= 权限校验

RateLimitAspect
= 限流保护

最终形成:

请求进来

权限 / 限流

TraceId

请求日志

Controller

Biz / Service

异常统一处理

Result 统一返回

到这里,Spring Boot 项目就从:能跑的 demo

升级成:有企业工程结构的项目

这就是中级后端必须具备的工程化意识。

最后一篇(补充):

Spring AOP 切点设计实战:execution vs @annotation

相关推荐
NE_STOP1 小时前
Redis--发布订阅命令和Redis事务
java
PAC_3Dame1 小时前
记一次真实的线上OOM
java
SunnyDays10112 小时前
如何在Java中将Word文档转换为图像(JPEG、PNG或SVG)
java
追风筝的人er2 小时前
SpringBoot+Vue3 企业考勤如何处理法定假期?节假日方案、调休补班与工作日判断链路拆解
前端·vue.js·后端
Lumos_7772 小时前
Linux -- 线程
java·jvm·算法
知兀2 小时前
【MybatisPlus】后端用枚举类,数据库用tinyint,存在枚举类型转换
java
StockTV2 小时前
印度股票实时数据 NSE和BSE的实时行情、K 线及指数数据
java·开发语言·spring boot·python
User_芊芊君子2 小时前
【OpenAI 把 AI 玩明白了】:自主推理 + 动态知识图谱,这 4 个技术突破要颠覆行业
java·人工智能·知识图谱