Spring统一功能处理

一、引言

写接口的时候,我们几乎每天都在重复几件事:把返回值包成 {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 统一功能处理的四件套:

  1. 统一返回结果 Result :统一 {code, msg, data} 结构;
  2. 统一异常处理 :@RestControllerAdvice + @ExceptionHandler,业务异常、参数校验异常、兜底异常分级处理;
  3. 统一响应体包装 :ResponseBodyAdvice 自动包装返回值(注意 String 类型需手动转 JSON);
  4. 请求日志拦截器 :HandlerInterceptor 统一记录路径与耗时。

这套组合拳一旦搭好,团队里所有人的接口风格天然一致,前端对接成本大幅下降。下一篇我们将介绍 Spring AOP,用更优雅的方式实现日志、耗时统计等横切逻辑。

相关推荐
SimonKing9 小时前
震惊:怎么回事,opencode一轮对话耗尽免费额度
java·后端·程序员
装不满的克莱因瓶9 小时前
【SpringAI Alibaba】ReactAgent响应SSE实时流式传输案例实战
java·人工智能·ai·sse·springai
杨凯凡9 小时前
Spring Boot 文件上传怎么做才安全?流式落盘、路径校验与数据库一致性实战
java·spring boot·文件上传·后端安全·数据库一致性
这个昵称不容易9 小时前
旅游管理系统的设计与实现(SpringBoot + MySQL + Vue 前后端分离)
java·springboot·毕设
Rain的Java大神之路9 小时前
LeetCode 37 解数独面试攻略:回溯 + 位掩码 + MRV,从暴力到毫秒级求解
java·后端·leetcode
严肃小白兔10 小时前
TanStack Router 更新构建工具链:Node.js 兼容性、路由分割与前端工程实践
前端·node.js·状态模式
howard200510 小时前
Spring Boot 版成绩统计分析
spring boot·成绩统计分析
天天被压力10 小时前
【A股分钟级高频数据完全手册 #11】券商实时行情数据接口:实时交易(券商源)接入与字段
java·人工智能·python
天天被压力11 小时前
【A股分钟级高频数据完全手册 #12】股票五档盘口数据接口:买卖五档实时盘口解析
java·人工智能·python
Wang's Blog20 小时前
Java 项目实战: 交友项目 - FastDFS:入门案例
java·开发语言·交友