老蒋的知识库

  • 首页
  • 文章归档
  • 关于页面

  • 搜索

Spring boot、Swagger 3 统一接口响应,处理异常统一返回。

发表于 2025-02-18 | 分类于 Java | 0 | 阅读次数 38
  1. 统一配置
  2. 定义统一响应类 Result<T>
  3. WebMvc配置
    3.1. 安装依赖,添加配置
    3.2. 自定义@RestControllerAdvice + ResponseBodyAdvice自动Result<T>包装返回值
  4. WebFlux配置
    4.1. 安装依赖,添加配置
    4.2. 自定义GlobalResponseHandler自动Result<T>包装返回值
    4.3. 自定义WebFluxConfiguration并注入GlobalResponseHandler
  5. 自定义GlobalExceptionHandler监听全局异常WebMvc、WebFlux通用
  6. 自定义ModelConverter修改Swagger Ui 显示Api文档,使其匹配Result<T>包装的返回值
  7. 同步Spring Boot和Swagger Ui的ObjectMapper,统一修改返回值驼峰格式为蛇形格式(下划线)

1. 安装依赖

<!-- 通过注解,添加语法糖	-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

添加配置到bootstrap.yml


springdoc:
  api-docs:
    enabled: true    # 是否开启 OpenAPI JSON 接口
    path: /v3/api-docs # 默认路径
  swagger-ui:
    enabled: true
    path: /index.html # 自定义 Swagger UI 路径,访问此路径会重定向到/swagger-ui/index.html
    disable-swagger-default-url: true # 禁用 swagger 默认自带的示例 ui
  webjars:
    prefix: # 修改默认的 /webjars 路径为空  

此时启动服务可以访问http://127.0.0.1:8080/swagger-ui/index.html

2. 定义统一响应类 Result<T>,http状态HttpStatus

坑点:Schema 泛型不能使用name属性,swagger-ui通过name区分唯一,会导致多个不同泛型api文档一样


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jagger.ai.common.core.constant.HttpStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Mono;

@Slf4j
@Data
@Schema(description = "Http统一响应") // 统一响应类(泛型) 不能使用 @Schema(name = ""),swagger-ui通过name区分,导致多个泛型api文档一样
public class Result<T> {
    @Schema(description = "状态码")
    private int code;
    @Schema(description = "消息")
    private String msg;
    @Schema(description = "数据")
    private T data;

    public Result(HttpStatus httpStatus, String msg, T data) {
        this.code = httpStatus.value();
        this.msg = msg;
        this.data = data;
    }

    // 成功响应(无数据)
    public static <T> Result<T> success() {
        return Result.success(null);
    }

    // 成功响应(带数据)
    public static <T> Result<T> success(T data) {
        return new Result<>(HttpStatus.OK, HttpStatus.OK.getReasonPhrase(), data);
    }

    // 错误响应
    public static <T> Result<T> error(HttpStatus httpStatus) {
        return Result.error(httpStatus, httpStatus.getReasonPhrase());
    }

    public static <T> Result<T> error(HttpStatus httpStatus, String msg) {
        return Result.error(httpStatus, msg, null);
    }

    public static <T> Result<T> error(HttpStatus httpStatus, String msg, T data) {
        return new Result<>(httpStatus, msg, data);
    }

    /***
     * webflux 专用
     */
    public Mono<Void> toMono(ServerHttpResponse response, ObjectMapper objectMapper) {
        response.setStatusCode(HttpStatusCode.valueOf(code));
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

        byte[] bytes = null;
        try {
            bytes = objectMapper.writeValueAsBytes(this);
        } catch (JsonProcessingException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        }
        DataBuffer buffer = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(buffer));
    }
}

HttpStatus 定义返回值


import org.springframework.lang.Nullable;

/***
 * 抄 {@link org.springframework.http.HttpStatus},改汉化的
 */
public enum HttpStatus {

    CONTINUE(100, HttpStatus.Series.INFORMATIONAL, "继续"),
    SWITCHING_PROTOCOLS(101, HttpStatus.Series.INFORMATIONAL, "切换协议"),
    PROCESSING(102, HttpStatus.Series.INFORMATIONAL, "处理中"),
    EARLY_HINTS(103, HttpStatus.Series.INFORMATIONAL, "早期提示"),
    OK(200, HttpStatus.Series.SUCCESSFUL, "成功"),
    CREATED(201, HttpStatus.Series.SUCCESSFUL, "已创建"),
    ACCEPTED(202, HttpStatus.Series.SUCCESSFUL, "已接受"),
    NON_AUTHORITATIVE_INFORMATION(203, HttpStatus.Series.SUCCESSFUL, "非权威信息"),
    NO_CONTENT(204, HttpStatus.Series.SUCCESSFUL, "无内容"),
    RESET_CONTENT(205, HttpStatus.Series.SUCCESSFUL, "重置内容"),
    PARTIAL_CONTENT(206, HttpStatus.Series.SUCCESSFUL, "部分内容"),
    MULTI_STATUS(207, HttpStatus.Series.SUCCESSFUL, "多状态"),
    ALREADY_REPORTED(208, HttpStatus.Series.SUCCESSFUL, "已报告"),
    IM_USED(226, HttpStatus.Series.SUCCESSFUL, "IM 已使用"),
    MULTIPLE_CHOICES(300, HttpStatus.Series.REDIRECTION, "多种选择"),
    MOVED_PERMANENTLY(301, HttpStatus.Series.REDIRECTION, "永久移动"),
    FOUND(302, HttpStatus.Series.REDIRECTION, "已找到"),
    /** @deprecated */
    @Deprecated
    MOVED_TEMPORARILY(302, HttpStatus.Series.REDIRECTION, "临时移动"),
    SEE_OTHER(303, HttpStatus.Series.REDIRECTION, "查看其他"),
    NOT_MODIFIED(304, HttpStatus.Series.REDIRECTION, "未修改"),
    /** @deprecated */
    @Deprecated
    USE_PROXY(305, HttpStatus.Series.REDIRECTION, "使用代理"),
    TEMPORARY_REDIRECT(307, HttpStatus.Series.REDIRECTION, "临时重定向"),
    PERMANENT_REDIRECT(308, HttpStatus.Series.REDIRECTION, "永久重定向"),
    BAD_REQUEST(400, HttpStatus.Series.CLIENT_ERROR, "错误请求"),
    UNAUTHORIZED(401, HttpStatus.Series.CLIENT_ERROR, "鉴权不通过"),
    PAYMENT_REQUIRED(402, HttpStatus.Series.CLIENT_ERROR, "需要付款"),
    FORBIDDEN(403, HttpStatus.Series.CLIENT_ERROR, "禁止访问"),
    NOT_FOUND(404, HttpStatus.Series.CLIENT_ERROR, "未找到"),
    METHOD_NOT_ALLOWED(405, HttpStatus.Series.CLIENT_ERROR, "方法不允许"),
    NOT_ACCEPTABLE(406, HttpStatus.Series.CLIENT_ERROR, "不可接受"),
    PROXY_AUTHENTICATION_REQUIRED(407, HttpStatus.Series.CLIENT_ERROR, "需要代理认证"),
    REQUEST_TIMEOUT(408, HttpStatus.Series.CLIENT_ERROR, "请求超时"),
    CONFLICT(409, HttpStatus.Series.CLIENT_ERROR, "冲突"),
    GONE(410, HttpStatus.Series.CLIENT_ERROR, "已删除"),
    LENGTH_REQUIRED(411, HttpStatus.Series.CLIENT_ERROR, "需要长度"),
    PRECONDITION_FAILED(412, HttpStatus.Series.CLIENT_ERROR, "前提条件失败"),
    PAYLOAD_TOO_LARGE(413, HttpStatus.Series.CLIENT_ERROR, "负载过大"),
    /** @deprecated */
    @Deprecated
    REQUEST_ENTITY_TOO_LARGE(413, HttpStatus.Series.CLIENT_ERROR, "请求实体过大"),
    URI_TOO_LONG(414, HttpStatus.Series.CLIENT_ERROR, "URI 过长"),
    /** @deprecated */
    @Deprecated
    REQUEST_URI_TOO_LONG(414, HttpStatus.Series.CLIENT_ERROR, "请求 URI 过长"),
    UNSUPPORTED_MEDIA_TYPE(415, HttpStatus.Series.CLIENT_ERROR, "不支持的媒体类型"),
    REQUESTED_RANGE_NOT_SATISFIABLE(416, HttpStatus.Series.CLIENT_ERROR, "请求范围无法满足"),
    EXPECTATION_FAILED(417, HttpStatus.Series.CLIENT_ERROR, "期望失败"),
    I_AM_A_TEAPOT(418, HttpStatus.Series.CLIENT_ERROR, "我是一个茶壶"),
    /** @deprecated */
    @Deprecated
    INSUFFICIENT_SPACE_ON_RESOURCE(419, HttpStatus.Series.CLIENT_ERROR, "资源空间不足"),
    /** @deprecated */
    @Deprecated
    METHOD_FAILURE(420, HttpStatus.Series.CLIENT_ERROR, "方法失败"),
    /** @deprecated */
    @Deprecated
    DESTINATION_LOCKED(421, HttpStatus.Series.CLIENT_ERROR, "目标锁定"),
    UNPROCESSABLE_ENTITY(422, HttpStatus.Series.CLIENT_ERROR, "无法处理的实体"),
    LOCKED(423, HttpStatus.Series.CLIENT_ERROR, "锁定"),
    FAILED_DEPENDENCY(424, HttpStatus.Series.CLIENT_ERROR, "依赖失败"),
    TOO_EARLY(425, HttpStatus.Series.CLIENT_ERROR, "过早"),
    UPGRADE_REQUIRED(426, HttpStatus.Series.CLIENT_ERROR, "需要升级"),
    PRECONDITION_REQUIRED(428, HttpStatus.Series.CLIENT_ERROR, "需要前提条件"),
    TOO_MANY_REQUESTS(429, HttpStatus.Series.CLIENT_ERROR, "请求过多"),
    REQUEST_HEADER_FIELDS_TOO_LARGE(431, HttpStatus.Series.CLIENT_ERROR, "请求头字段过大"),
    UNAVAILABLE_FOR_LEGAL_REASONS(451, HttpStatus.Series.CLIENT_ERROR, "因法律原因不可用"),
    INTERNAL_SERVER_ERROR(500, HttpStatus.Series.SERVER_ERROR, "服务器错误"),
    NOT_IMPLEMENTED(501, HttpStatus.Series.SERVER_ERROR, "未实现"),
    BAD_GATEWAY(502, HttpStatus.Series.SERVER_ERROR, "网关错误"),
    SERVICE_UNAVAILABLE(503, HttpStatus.Series.SERVER_ERROR, "服务不可用"),
    GATEWAY_TIMEOUT(504, HttpStatus.Series.SERVER_ERROR, "网关超时"),
    HTTP_VERSION_NOT_SUPPORTED(505, HttpStatus.Series.SERVER_ERROR, "HTTP 版本不支持"),
    VARIANT_ALSO_NEGOTIATES(506, HttpStatus.Series.SERVER_ERROR, "变体也协商"),
    INSUFFICIENT_STORAGE(507, HttpStatus.Series.SERVER_ERROR, "存储不足"),
    LOOP_DETECTED(508, HttpStatus.Series.SERVER_ERROR, "检测到循环"),
    BANDWIDTH_LIMIT_EXCEEDED(509, HttpStatus.Series.SERVER_ERROR, "带宽限制超出"),
    NOT_EXTENDED(510, HttpStatus.Series.SERVER_ERROR, "未扩展"),
    NETWORK_AUTHENTICATION_REQUIRED(511, HttpStatus.Series.SERVER_ERROR, "需要网络认证");

    private static final HttpStatus[] VALUES = values();
    private final int value;
    private final HttpStatus.Series series;
    private final String reasonPhrase;

    private HttpStatus(int value, HttpStatus.Series series, String reasonPhrase) {
        this.value = value;
        this.series = series;
        this.reasonPhrase = reasonPhrase;
    }

    public int value() {
        return this.value;
    }

    public HttpStatus.Series series() {
        return this.series;
    }

    public String getReasonPhrase() {
        return this.reasonPhrase;
    }

    /***
     * 转 org.springframework.http.HttpStatus
     * @return
     */
    public org.springframework.http.HttpStatus toSpringHttpStatus(){
        return org.springframework.http.HttpStatus.valueOf(this.value);
    }
    public boolean is1xxInformational() {
        return this.series() == HttpStatus.Series.INFORMATIONAL;
    }

    public boolean is2xxSuccessful() {
        return this.series() == HttpStatus.Series.SUCCESSFUL;
    }

    public boolean is3xxRedirection() {
        return this.series() == HttpStatus.Series.REDIRECTION;
    }

    public boolean is4xxClientError() {
        return this.series() == HttpStatus.Series.CLIENT_ERROR;
    }

    public boolean is5xxServerError() {
        return this.series() == HttpStatus.Series.SERVER_ERROR;
    }

    public boolean isError() {
        return this.is4xxClientError() || this.is5xxServerError();
    }

    public String toString() {
        int var10000 = this.value;
        return var10000 + " " + this.name();
    }

    public static HttpStatus valueOf(int statusCode) {
        HttpStatus status = resolve(statusCode);
        if (status == null) {
            throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
        } else {
            return status;
        }
    }

    @Nullable
    public static HttpStatus resolve(int statusCode) {
        for(HttpStatus status : VALUES) {
            if (status.value == statusCode) {
                return status;
            }
        }

        return null;
    }

    public static enum Series {
        INFORMATIONAL(1),
        SUCCESSFUL(2),
        REDIRECTION(3),
        CLIENT_ERROR(4),
        SERVER_ERROR(5);

        private final int value;

        private Series(int value) {
            this.value = value;
        }

        public int value() {
            return this.value;
        }

        /** @deprecated */
        @Deprecated
        public static HttpStatus.Series valueOf(HttpStatus status) {
            return status.series;
        }

        public static HttpStatus.Series valueOf(int statusCode) {
            HttpStatus.Series series = resolve(statusCode);
            if (series == null) {
                throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
            } else {
                return series;
            }
        }

        @Nullable
        public static HttpStatus.Series resolve(int statusCode) {
            int seriesCode = statusCode / 100;

            for(HttpStatus.Series series : values()) {
                if (series.value == seriesCode) {
                    return series;
                }
            }

            return null;
        }
    }
}

3. WebMvc配置

3.1 添加 WebMvc 所需mvn 包

<!-- webmvc 依赖于 spring-boot-starter-web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springboot 官方 api 文档,集成Swagger v3 -->
<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.8.3</version>
</dependency>

3.2 自定义@RestControllerAdvice自动使用Result<T>包装返回值

注: 使用RestControllerAdvice封装返回值不会修改swagger-ui显示,需要而外添加swagger ui api封装,参考文章后续方案

如果未生效,启动类添加注解@ComponentScan(basePackages = {"com.xxx.xxx"})确定扫描包范围(但是会导致spring扫描bean注入有问题,idea报错一些spring类无法找到注入,要把需要扫描的所有包都加进去)


@SpringBootApplication
@ComponentScan(basePackages = {
       "com.xxx.xxx",    // 主模块包路径
})
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}

封装正常返回


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jagger.ai.common.core.domain.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
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;


/***
 * 所有返回值统一封装{@link Result}
 */
@RestControllerAdvice(basePackages = "com.xx.xx") // 扫描包范围
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {
    /***
     * 判断是否需要处理返回值
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 排除已包装的 Result 类型
        return !returnType.getParameterType().isAssignableFrom(Result.class);
    }

    /***
     * 统一包装返回值
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType mediaType, Class converterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        // 如果返回值是 String 类型,手动转为 JSON 字符串
        // 在 Spring 的 统一响应处理中,只有 String 类型需要单独处理,而其他类型(如 int、对象、集合等)不需要特殊处理。
        // 这是因为 Spring 对 String 类型的返回值有特殊的 StringHttpMessageConverter 默认行为,而其他类型会通过 JSON 序列化正常处理。
        Result<?> result = Result.success(body);
        if (body instanceof String) {
            try {
                return new ObjectMapper().writeValueAsString(result);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return Result.success(body);
    }
}

4. WebFlux配置

4.1. 安装依赖,添加配置

<!-- 使用 webflux 响应式流(Reactive Streams)默认通过 Netty 启动排除 Tomcat -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<!-- springboot 官方 api 文档,支持集成Swagger -->
<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>

自定义WebProperties配置控制全局封装过滤路由

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
@ConfigurationProperties(prefix = "jagger.web")
@Data
public class WebProperties {
    // 需要无视的路由
    private List<String> ignoreRoute = new ArrayList<>();

    // 统一封装所有响应返回
    private Boolean unifiedResponse = true;
}

application.yml文件添加配置

jagger:
  web:
    ignore-route:
    # 过滤 swagger api 路由
      - ${springdoc.api-docs.path}/** 

4.2. 自定义GlobalResponseHandler自动Result<T>包装返回值


import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;


/***
 * 所有返回值统一封装{@link Result}
 */
public class GlobalResponseHandler extends ResponseBodyResultHandler {

    // 需要无视的路由配置,避免封装导致api返回异常,例如: swagger api的路径
    private final List<String> ignoreRoute;

    public GlobalResponseHandler(List<String> ignoreRoute, List<HttpMessageWriter<?>> writers, RequestedContentTypeResolver resolver) {
        super(writers, resolver);
        this.ignoreRoute = ignoreRoute;
        setOrder(getOrder() - 1); // 修改调用优先级使其高于 ResponseBodyResultHandler。
    }


    private static MethodParameter param;

    static {
        try {
            Method method = GlobalResponseHandler.class.getDeclaredMethod("methodForParams");
            param = new MethodParameter(method, -1);
            param = param.nestedIfOptional();
        } catch (NoSuchMethodException e) {
            throw new IllegalStateException("Failed to find methodForParams method", e);
        }
    }

    // 虚拟方法用于初始化MethodParameter
    private static Result<?> methodForParams() {
        return null;
    }

    private final PathMatcher pathMatcher = new AntPathMatcher();  // Spring内置的路径匹配器

    /***
     * 判断是否是需要忽视的路由
     */
    private boolean isIgnoreRoute(String route) {
        return ignoreRoute.stream()
                .anyMatch(pattern -> pathMatcher.match(pattern, route));
    }

    /***
     * 判断是否需要执行封装操作
     */
    @Override
    public boolean supports(HandlerResult result) {
        // 使用 ResolvableType 解析返回值类型
        ResolvableType returnType = result.getReturnType();

        // 检查是否包含泛型参数
        if (returnType.getGenerics().length == 0) {
            // 无泛型参数,需要封装
            return true;
        }

        Class<?> rawClass = returnType.getRawClass();

        if (rawClass == null) {
            return true;
        }

        // 判断泛型是否是 Result 类型
        return !Result.class.isAssignableFrom(rawClass);
    }

    @Override
    public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
        // 跳过需要忽视路由,避免第三方包api调用异常,例如:Swagger Api
        if (isIgnoreRoute(exchange.getRequest().getURI().getPath())) {
            return super.handleResult(exchange, result);
        }

        Object returnValue = result.getReturnValue();

        if (returnValue instanceof Mono) {
            // 处理Mono<T>,将其映射为Mono<Result<T>>
            return ((Mono<?>) returnValue)
                    .map(item -> {
                        ResolvableType resolvableType = ResolvableType.forType(item.getClass());
                        Class<?> rawClass = resolvableType.getRawClass();
                        // 非Result类型,包装成Result
                        if (rawClass != null && Result.class.isAssignableFrom(rawClass)) {
                            // 已经是Result类型,直接返回
                            return (Result<?>) item;
                        } else {
                            // 非Result类型,包装成Result
                            return Result.success(item);
                        }
                    })
                    .defaultIfEmpty(Result.success(null))   // 处理空 Mono(如 Mono.empty())
                    .flatMap(body -> writeBody(body, param, exchange));
        } else if (returnValue instanceof Flux) {
            // 处理Flux<T>,转换为Mono<Result<List<T>>>
            return ((Flux<?>) returnValue).collectList()
                    .map(items -> {
                        if (items.isEmpty()) {
                            return Result.success(items);
                        } else {
                            List<Object> resultList = new ArrayList<>();
                            for (Object item : items) {
                                ResolvableType resolvableType = ResolvableType.forType(item.getClass());
                                Class<?> rawClass = resolvableType.getRawClass();
                                if (rawClass != null && Result.class.isAssignableFrom(rawClass)) {
                                    resultList.add(((Result<?>) item).getData());
                                } else {
                                    resultList.add(item);
                                }
                            }
                            return Result.success(resultList);
                        }
                    })
                    .defaultIfEmpty(Result.success(null))
                    .flatMap(body -> writeBody(body, param, exchange));
        } else {
            // 调用父类方法处理包装后的返回值
            return writeBody(Result.success(returnValue), param, exchange);
        }
    }
}

4.3. 自定义WebFluxConfiguration并注入GlobalResponseHandler


import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.config.WebFluxConfigurer;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class WebFluxConfiguration implements WebFluxConfigurer {

    private final WebProperties webProperties;

    @Bean
    public GlobalResponseHandler responseWrapper(ServerCodecConfigurer serverCodecConfigurer,
                                                 RequestedContentTypeResolver requestedContentTypeResolver) {
        if (webProperties.getUnifiedResponse()) {
            log.info("全局响应自动封装,忽略路由: {}", webProperties.getIgnoreRoute());
            return new GlobalResponseHandler(webProperties.getIgnoreRoute(), serverCodecConfigurer.getWriters(), requestedContentTypeResolver);
        }
        return null;
    }
}

5. 自定义GlobalExceptionHandler监听全局异常WebMvc、WebFlux通用

定义异常

import lombok.Getter;

@Getter
public class AuthException extends RuntimeException {
    private final HttpStatus httpStatus;

    public AuthException(HttpStatus httpStatus) {
        super(httpStatus.getReasonPhrase());
        this.httpStatus = httpStatus;
    }
}

异常处理统一返回


import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice(basePackages = "com.jagger.ai")
public class GlobalExceptionHandler {

    @ExceptionHandler(AuthException.class)
    public ResponseEntity<Result<Void>> authException(AuthException e) {
        // 从异常中动态获取HTTP状态码
        return ResponseEntity
                .status(e.getHttpStatus().toSpringHttpStatus())
                .body(Result.error(e.getHttpStatus(), e.getMessage()));
    }

    // 处理其他未捕获异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Result<Void>> handleException(Exception e) {
        log.error(e.getMessage(), e);
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR.toSpringHttpStatus())
                .body(Result.error(HttpStatus.INTERNAL_SERVER_ERROR,e.getMessage()));
    }
}

此时定义的一个Controller,返回String会自动加一层封装返回Result<T>,以下是测试路由

public interface LoginApi {

@Slf4j
@RestController
@RequiredArgsConstructor
public class LoginController implements LoginApi {
    @Override
    public String login() {
        throw new AuthException(HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @GetMapping("/login2")
    public Mono<Result<String>> login2() {
        return Mono.create(sink -> sink.success(Result.success("success")));
    }
    @GetMapping("/login3")
    public Mono<Result<Result<String>>> login3() {
        return Mono.create(sink -> sink.success(Result.success(Result.success("success"))));
    }
    @GetMapping("/login4")
    public Flux<Result<String>> login4() {
        return Flux.create((t) -> {
            t.next(Result.success("1"));
            t.next(Result.success("2"));
            t.complete();
        });
    }
    @GetMapping("/login5")
    public List<String> login5() {
        return List.of("1", "2", "3");
    }

}

}

所有响应都会返回Result<T>,但是swagger-ui接口文档显示不正确

接口 /login 返回文档

string

接口 /login2 返回文档

{
  "code": 200,
  "msg": "Success",
  "data": "string"
}

6. 自定义ResultWrapperCustomizer修改Swagger Ui 显示Api文档,使其匹配Result<T>包装的返回值


import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springdoc.core.customizers.OpenApiCustomizer;

/***
 * swagger ui api显示所有返回值封装一层{@link Result}
 */
@Slf4j
@Component
public class ResultWrapperCustomizer implements OpenApiCustomizer {

    @Override
    public void customise(OpenAPI openApi) {
        openApi.getPaths().forEach((path, pathItem) -> {
            log.debug(path);
            pathItem.readOperations().forEach(operation -> {
                ApiResponses responses = operation.getResponses();
                responses.values().forEach(apiResponse -> processApiResponse(apiResponse));
            });
        });
    }

    /***
     * 处理api docs返回值显示内容
     * @param apiResponse
     */
    private void processApiResponse(ApiResponse apiResponse) {
        Content content = apiResponse.getContent();

        if (content == null) {
            // 针对无响应体的接口(如返回 void),创建默认的 content 和 schema
            content = new Content();
            MediaType mediaType = new MediaType();
            mediaType.setSchema(createWrappedSchema(null)); // 传入 null 表示 data 为 null
            content.addMediaType("application/json", mediaType);
            apiResponse.setContent(content);
            return;
        }

        content.forEach((mediaType, mediaTypeItem) -> {
            Schema originalSchema = mediaTypeItem.getSchema();
            if (originalSchema != null && !isResultWrapper(originalSchema)) {
                Schema<?> wrappedSchema = createWrappedSchema(originalSchema);
                mediaTypeItem.setSchema(wrappedSchema);
            }
        });
    }

    /***
     * 判断返回类型是否是Result
     */
    private boolean isResultWrapper(Schema<?> schema) {
        if (schema.get$ref() != null) {
            return schema.get$ref().startsWith("#/components/schemas/" + Result.class.getSimpleName());
        }
        return false;
    }

    /***
     * 封装一层Result
     */
    private Schema<?> createWrappedSchema(Schema<?> originalSchema) {
        return new ObjectSchema()
                .addProperty("code", new IntegerSchema().example(200))
                .addProperty("msg", new StringSchema().example("Success"))
                .addProperty("data", originalSchema);
    }
}

至此swagger-ui显示的接口文档,返回值也是正确的
接口 /login 返回文档

{
  "code": 200,
  "msg": "Success",
  "data": "string"
}

接口 /login2 返回文档

{
  "code": 200,
  "msg": "Success",
  "data": "string"
}

7. 同步Spring Boot和Swagger Ui的ObjectMapper,统一修改返回值驼峰格式为蛇形格式(下划线)

application.yml 添加配置

spring:
  jackson:
    property-naming-strategy: SNAKE_CASE

添加ModelResolver子类通过@Component注入 Spring

/***
 * 通过 @Component 注入 CustomModelResolver 覆盖 ModelResolver,将spring ObjectMapper注入CustomModelResolver。
 * 否则ModelResolver会使用自定义的ObjectMapper,无法与spring ObjectMapper保持同步配置,例如:swagger ui 返回值格式修改 SNAKE_CASE
 * 注:这里不能使用@bean注入ModelResolver实现,否则ModelResolver不会加载自定义ModelConverter,具体原因未知。
 */
@Component
public class CustomModelResolver extends ModelResolver {
    public CustomModelResolver(ObjectMapper mapper) {
        super(mapper);
    }
}

此时返回值都会是下划线格式

  • 本文作者: jagger
  • 本文链接: /archives/springbootswagger3-tong-yi-jie-kou-xiang-ying
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
Nacos 2023.0.1.2 之后版本不稳定,配置有改动不能直接2023.0.1.2升级到2023.0.1.3
Netty自定义通信协议,实现IM(即时通讯)
jagger

jagger

66 日志
31 分类
0 标签
Creative Commons
0%
© 2026 jagger
由 Halo 强力驱动