一、引言
写接口的时候,我们几乎每天都在重复几件事:把返回值包成 {code, msg, data} 的统一结构、写 try-catch 处理异常、打印请求日志。如果每个 Controller 都各写一遍,代码会变得又臭又长,而且风格很难统一。
SpringBoot 提供了一整套「统一功能处理」的机制,让我们把这些横切逻辑集中到一处。本文就带你实现四件套:统一返回结果、统一异常处理、统一响应体包装、请求日志拦截器。
二、为什么需要统一功能处理
举一个最直接的例子:前端要对接 10 个接口,如果每个接口的返回结构都不一样,有的返回 {data: ...},有的直接返回字符串,有的异常时返回 {error: ...},前端就得为每个接口写不同的解析逻辑,苦不堪言。
统一功能处理的核心目标就是:
- 统一返回结构 :所有接口返回
Result结构; - 统一异常兜底:异常不再抛给前端一堆堆栈,而是转成友好提示;
- 统一日志:请求参数、耗时、异常集中记录。
三、统一返回结果 Result
先定义一个所有接口共用的返回结构:
java
java
package com.example.unify.common;
import lombok.Data;
/**
* 统一返回结果
*/
@Data
public class Result<T> {
private Integer code; // 状态码:200 成功,其它表示失败
private String msg; // 提示信息
private T data; // 业务数据
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.setCode(200);
r.setMsg("success");
r.setData(data);
return r;
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> error(String msg) {
return error(500, msg);
}
public static <T> Result<T> error(Integer code, String msg) {
Result<T> r = new Result<>();
r.setCode(code);
r.setMsg(msg);
return r;
}
}
四、统一异常处理:@ControllerAdvice + @ExceptionHandler
异常处理是统一功能里最重要的一环。核心是两个注解:
@ControllerAdvice(或@RestControllerAdvice):声明一个全局的异常处理类;@ExceptionHandler:指定该方法处理哪种异常。
先定义一个自定义业务异常:
java
java
package com.example.unify.common;
/**
* 自定义业务异常
*/
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(String message) {
this(500, message);
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
}
再写全局异常处理器:
java
java
package com.example.unify.handler;
import com.example.unify.common.BusinessException;
import com.example.unify.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/** 处理自定义业务异常 */
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常:{}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/** 处理参数校验异常(配合 @Validated 使用) */
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError() != null
? e.getBindingResult().getFieldError().getDefaultMessage()
: "参数校验失败";
return Result.error(400, msg);
}
/** 兜底:处理其它所有异常 */
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常:", e);
return Result.error(500, "系统繁忙,请稍后重试");
}
}
要点:
@ExceptionHandler越具体的异常要写在上面,Exception.class作为兜底写在最后,Spring 会优先匹配最精确的处理器。
五、统一响应体处理:ResponseBodyAdvice
有了 Result 和全局异常处理后,还差一步:让 Controller 直接返回业务对象,框架自动包成 Result,而不是每个接口手动写 Result.success(...)。
实现 ResponseBodyAdvice 接口即可:
java
java
package com.example.unify.handler;
import com.example.unify.common.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 统一响应体处理:把 Controller 返回值自动包装成 Result
*/
@RestControllerAdvice(basePackages = "com.example.unify.controller")
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
private final ObjectMapper objectMapper = new ObjectMapper();
/** 是否需要包装:已经是 Result 的就不重复包装 */
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.getParameterType().equals(Result.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
if (body instanceof String) {
try {
// 字符串类型需手动转 JSON,否则 StringHttpMessageConverter 会报类型转换错误
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return Result.success(body);
}
}
六、请求日志与拦截器:HandlerInterceptor
统一记录每个请求的路径、耗时,可以用拦截器实现。
拦截器类:
java
java
package com.example.unify.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 请求日志拦截器
*/
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
long start = System.currentTimeMillis();
request.setAttribute("startTime", start);
log.info("请求开始:{} {},来源 IP:{}",
request.getMethod(), request.getRequestURI(), request.getRemoteAddr());
return true; // 返回 true 放行,false 则中断请求
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
Long start = (Long) request.getAttribute("startTime");
long cost = System.currentTimeMillis() - start;
log.info("请求结束:{} {},耗时:{} ms",
request.getMethod(), request.getRequestURI(), cost);
}
}
注册拦截器:
java
java
package com.example.unify.config;
import com.example.unify.interceptor.LogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC 配置:注册拦截器
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login", "/static/**"); // 排除登录和静态资源
}
}
配套 Controller 示例,验证整套链路:
java
java
package com.example.unify.controller;
import com.example.unify.common.BusinessException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
/** 正常返回:返回值会被 GlobalResponseAdvice 自动包装成 Result */
@GetMapping("/{id}")
public String getUser(@PathVariable Long id) {
if (id <= 0) {
throw new BusinessException(400, "用户 ID 不合法");
}
return "用户信息:id = " + id;
}
/** 异常返回:异常会被 GlobalExceptionHandler 捕获 */
@GetMapping("/demoError")
public String demoError() {
throw new BusinessException(500, "演示业务异常");
}
}
七、总结
本文实现了 SpringBoot 统一功能处理的四件套:
- 统一返回结果
Result:统一{code, msg, data}结构; - 统一异常处理 :
@RestControllerAdvice+@ExceptionHandler,业务异常、参数校验异常、兜底异常分级处理; - 统一响应体包装 :
ResponseBodyAdvice自动包装返回值(注意 String 类型需手动转 JSON); - 请求日志拦截器 :
HandlerInterceptor统一记录路径与耗时。
这套组合拳一旦搭好,团队里所有人的接口风格天然一致,前端对接成本大幅下降。下一篇我们将介绍 Spring AOP,用更优雅的方式实现日志、耗时统计等横切逻辑。