- 统一配置
- 定义统一响应类
Result<T> - WebMvc配置
3.1. 安装依赖,添加配置
3.2. 自定义@RestControllerAdvice+ResponseBodyAdvice自动Result<T>包装返回值 - WebFlux配置
4.1. 安装依赖,添加配置
4.2. 自定义GlobalResponseHandler自动Result<T>包装返回值
4.3. 自定义WebFluxConfiguration并注入GlobalResponseHandler - 自定义
GlobalExceptionHandler监听全局异常WebMvc、WebFlux通用 - 自定义
ModelConverter修改Swagger Ui 显示Api文档,使其匹配Result<T>包装的返回值 - 同步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);
}
}
此时返回值都会是下划线格式