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,用更优雅的方式实现日志、耗时统计等横切逻辑。

相关推荐
SL_staff1 小时前
从钉钉日报到动态数据看板:面向业务侧的低代码BI实践路径
java·数据分析·数据可视化
TDengine (老段)1 小时前
TDengine TSDB 实战排障四(升级与兼容)
android·java·大数据·数据库·物联网·时序数据库·tdengine
nhdh1 小时前
SpringAI与SpringAIAlibaba:标准与生态的完美互补
java·人工智能·spring
不会写DN2 小时前
Go日志库工程选型与逃逸分析评测报告
java·服务器·golang
SL_staff2 小时前
合规性折旧:当知识资产因主权缺位在审计中‘功能性清零’
java·设计模式·开源
wzq11_6662 小时前
Kubernetes集群——基础篇(基础知识与搭建步骤一遍过!!!)
java·容器·kubernetes
Dreams_l2 小时前
基于SpringBoot实现的抽奖系统(一)
java·spring boot·后端
落魄实习生2 小时前
Agent Scope Java 2.x 系列【1】核心架构
java·开发语言·架构
鬼手点金3 小时前
Claude Code示范案例-常用快捷命令
java·服务器·前端·计算机视觉·前向传播