一、前言
前面我们已经学过:
这一篇继续升级。
这篇的目标不是单独讲某个注解,而是把项目中常见的基础能力做成一套可复制的工程模板:
统一返回 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:方法执行前
适合:
- 方法进入日志
- 简单参数打印
- 简单前置检查
代码:
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)));
}
}
特点:
- 能在方法前执行
- 不能拿到返回值
- 不能统计完整耗时
- 不能决定方法是否执行
2. @AfterReturning:成功返回后
适合:
- 返回值日志
- 成功统计
- 成功后埋点
代码:
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)));
}
}
特点:
- 只在方法成功返回后执行
- 异常时不会执行
- 不能做异常日志
- 不能包住完整流程
3. @AfterThrowing:异常后
适合:
- 异常日志
- 异常统计
- 异常报警触发
代码:
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);
}
}
特点:
- 只在异常时执行
- 适合记录异常
- 不能处理成功返回
4. @After:最终执行
适合:
- 资源清理
- ThreadLocal 清理
- MDC 清理
- 无论成功失败都要执行的逻辑
代码:
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());
}
}
特点:
- 无论成功失败都会执行
- 拿不到返回值
- 不知道是成功还是异常
5. @Around:完整包裹
适合:
- 请求日志
- 耗时统计
- 权限拦截
- 限流
- 重试
- 降级
- 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;
}
}
}
特点:
- 最强
- 可以控制方法是否执行
- 可以拿到参数
- 可以拿到返回值
- 可以捕获异常
- 可以统计耗时
- 可以修改返回结果
6. 怎么选择通知类型
只想在执行前做点事
→ @Before
只关心成功返回
→ @AfterReturning
只关心异常
→ @AfterThrowing
无论成功失败都要收尾
→ @After
要控制完整流程
→ @Around
真实项目中:
请求日志、权限、限流、TraceId
优先用 @Around
原因是:
它能覆盖一次方法调用的完整生命周期
七、AOP 通用注解
tips:
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
因为它需要:
方法执行前放入 traceId
方法执行后清理 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;
}
}
}
说明:
- 请求日志用 @Around 最合适
- 因为它需要同时处理:
- 请求前
- 请求后
- 耗时
- 返回值
- 异常
十一、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
升级成:有企业工程结构的项目
这就是中级后端必须具备的工程化意识。