问题描述
在日常开发中我们会定义类似下面的 Result
类来统一接口返回结果的结构
1 2 3 4 5 6 7 8 9
| public class Result<T> implements Serializable { private static final long serialVersionUID = 1L;
private Integer code;
private String message;
private T data; }
|
此时 Controller
中的方法就会写成这个样子
1 2 3 4 5
| @GetMapping("/detail1/{userId}") public Result<User> detail1(@PathVariable(name = "userId") Long userId) { User user = .... return Result.<User>builder().data(user).build(); }
|
在每个方法上都写上类似 Result<User>
这样的代码还是比较繁琐的,我们期望写成 User
这样简单的形式
1 2 3 4 5
| @GetMapping("/detail2/{userId}") public User detail2(@PathVariable(name = "userId") Long userId) { User user = .... return user; }
|
而由框架去处理返回结果的包装。此时就会使用到 Spring 的 ResponseBodyAdvice
接口
1 2 3 4
| @RestControllerAdvice public class ResultResponseBodyAdvice implements ResponseBodyAdvice<Object> { }
|
同时我们也会使用 SpringDoc 对外提供可访问的文档。此时问题出现了,我们发现在 Swagger 的文档中方法 detail1
和方法 detail2
对应的 Schema 不一样
我们期望 detail2
展示的结果和 detail1
一样。
问题分析
io.swagger.v3.oas.models.OpenAPI
是 Swagger 的核心类,它是 Swagger 文档结构在 Java 语言中的表示,SpringDoc 干的事情就是构建这个类的实例并返回。默认情况下 SpringDoc 会在请求 /v3/api-docs
时去构建 OpenAPI
对象,它的实现在 OpenApiWebMvcResource
类的 openapiJson(HttpServletRequest, @Value(API_DOCS_URL) String, Locale)
方法。
跟踪 openapiJson
方法的实现我们得到了一个主线与返回值类型处理有关的方法调用链路图
丛中可以发现返回值的类型的处理与 GenericResponseService
有关,是不是可以重写它的一些方法来实现给返回值类型不是 Result
的增加一点处理。
除了构造方法 GenericResponseService
只有下面几个方法是公开的
buildContentFromDoc
setDescription
setResponseEntityExceptionHandlerClass
build
buildGenericResponse
getApiResponses
buildContent
evaluateResponseStatus
我们只能在这 8 个方法中选择合适的方法进行重写。通过进一步的分析,我们选择重写 build
方法,在返回值类型不是 Result
时构建类似 ResultUser
的 Schema。
构建好我们自己的 Schema 后还没完,此时只是把 Schema 加入到了 OpenAPI
对象中,Operation
对应的引用还没有修改,当然也可以在构建的同时进行修改,但是我们有一个更好的地方对它进行修改,当然也是为了展示 SpringDoc 的自定义能力。SpringDoc 在 org.springdoc.core.customizers
包下为我们提供了很多定制器,对我们的目标而言我们只需要 OperationCustomizer
。实现这个定制器,在返回类型值类型不是 Result
时,用我们自定义的 Schema 替换原来的 Schema 即可。
解决问题
根据问题分析中介绍我们首先继承 GenericResponseService
并重写它的 build
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| package me.acomma.example.swagger;
import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.core.converter.ResolvedSchema; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.responses.ApiResponses; import me.acomma.example.common.Result; import org.springdoc.core.GenericResponseService; import org.springdoc.core.MethodAttributes; import org.springdoc.core.OperationService; import org.springdoc.core.PropertyResolverUtils; import org.springdoc.core.ReturnTypeParser; import org.springdoc.core.SpringDocConfigProperties; import org.springframework.web.method.HandlerMethod;
import java.util.Arrays; import java.util.List;
public class ExampleGenericResponseService extends GenericResponseService {
public ExampleGenericResponseService(OperationService operationService, List<ReturnTypeParser> returnTypeParsers, SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils) { super(operationService, returnTypeParsers, springDocConfigProperties, propertyResolverUtils); }
@Override public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation, MethodAttributes methodAttributes) { ApiResponses apiResponses = super.build(components, handlerMethod, operation, methodAttributes);
Class<?> returnType = handlerMethod.getMethod().getReturnType(); Class<?> wrapperType = Result.class;
if (returnType != wrapperType) { String returnTypeSimpleName = returnType.getSimpleName(); String wrapperTypeSimpleName = wrapperType.getSimpleName(); String returnTypeSchemaName = returnTypeSimpleName; String wrapperTypeSchemaName = wrapperTypeSimpleName + returnTypeSimpleName;
if (!components.getSchemas().containsKey(returnTypeSchemaName)) { ResolvedSchema resolvedSchema = ModelConverters.getInstance().resolveAsResolvedSchema(new AnnotatedType(returnType).resolveAsRef(false)); components.getSchemas().put(returnTypeSchemaName, resolvedSchema.schema); }
if (!components.getSchemas().containsKey(wrapperTypeSchemaName)) { ResolvedSchema resolvedSchema = ModelConverters.getInstance().resolveAsResolvedSchema(new AnnotatedType(wrapperType).resolveAsRef(false));
Schema<?> schema = resolvedSchema.schema; schema.setName(wrapperTypeSchemaName); if (!returnTypeSimpleName.equals("void")) { schema.getProperties().get("data").set$ref("#/components/schemas/" + returnTypeSchemaName); }
components.getSchemas().put(wrapperTypeSchemaName, schema); }
if (returnTypeSimpleName.equals("void")) { apiResponses.forEach((responseName, apiResponse) -> { if (!responseName.equals("200")) { return; } if (apiResponse.getContent() == null) { MediaType mediaType = new MediaType(); mediaType.setSchema(components.getSchemas().get(returnTypeSchemaName));
Content content = new Content(); setContent(methodAttributes.getMethodProduces(), content, mediaType);
apiResponse.setContent(content); } }); } }
return apiResponses; }
private void setContent(String[] methodProduces, Content content, io.swagger.v3.oas.models.media.MediaType mediaType) { Arrays.stream(methodProduces).forEach(mediaTypeStr -> content.addMediaType(mediaTypeStr, mediaType)); } }
|
然后,实现 OperationCustomizer
对返回类型值类型不是 Result
的 Schema 进行替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package me.acomma.example.swagger;
import io.swagger.v3.oas.models.Operation; import me.acomma.example.common.Result; import org.springdoc.core.customizers.OperationCustomizer; import org.springframework.web.method.HandlerMethod;
public class ExampleOperationCustomer implements OperationCustomizer { @Override public Operation customize(Operation operation, HandlerMethod handlerMethod) { Class<?> returnType = handlerMethod.getMethod().getReturnType(); Class<?> wrapperType = Result.class; String returnTypeSimpleName = returnType.getSimpleName(); String wrapperTypeSimpleName = wrapperType.getSimpleName();
if (returnType != wrapperType) { String schemaName = wrapperTypeSimpleName + returnTypeSimpleName; operation.getResponses().forEach((responseName, apiResponse) -> { if (!responseName.equals("200")) { return; } if (apiResponse.getContent() == null) { return; } apiResponse.getContent().forEach((contentName, mediaType) -> mediaType.getSchema().set$ref("#/components/schemas/" + schemaName)); }); }
return operation; } }
|
最后,我们要把它们注册到容器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package me.acomma.example.swagger;
import org.springdoc.core.OperationService; import org.springdoc.core.PropertyResolverUtils; import org.springdoc.core.ReturnTypeParser; import org.springdoc.core.SpringDocConfigProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration public class ExampleSwaggerConfiguration { @Bean public ExampleOperationCustomer exampleOperationCustomer() { return new ExampleOperationCustomer(); }
@Bean public ExampleGenericResponseService exampleGenericResponseService(OperationService operationService, List<ReturnTypeParser> returnTypeParsers, SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils) { return new ExampleGenericResponseService(operationService, returnTypeParsers, springDocConfigProperties, propertyResolverUtils); } }
|
这样就实现了我们的目标。
参考资料
- Swagger 统一应答类型处理,在使用 SpringFox 时如何实现统一应答类型处理