端点“/api-docs”不适用于自定义 GsonHttpMessageConverter
Endpoint "/api-docs" doesn't work with custom GsonHttpMessageConverter
我从 Springfox Swagger 迁移到 Springdoc OpenApi。我在关于 springdoc 的配置中添加了几行:
springdoc:
pathsToMatch: /api/**
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
在配置中 class MainConfig.kt
我有以下代码:
val customGson: Gson = GsonBuilder()
.registerTypeAdapter(LocalDateTime::class.java, DateSerializer())
.registerTypeAdapter(ZonedDateTime::class.java, ZonedDateSerializer())
.addSerializationExclusionStrategy(AnnotationExclusionStrategy())
.enableComplexMapKeySerialization()
.setPrettyPrinting()
.create()
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
converters.add(GsonHttpMessageConverter(customGson))
}
当我转到 http://localhost:8013/swagger-ui.html(在配置中我有 server.port: 8013
)时,页面没有重定向到 swagger-ui/index.html?url=/api-docs&validatorUrl=
。但这不是我的主要问题:)。当我转到 swagger-ui/index.html?url=/api-docs&validatorUrl=
时,我得到了包含以下信息的页面:
Unable to render this definition
The provided definition does not specify a valid version field.
Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: "2.0" and those that match openapi: 3.0.n (for example, openapi: 3.0.0).
但是当我访问 http://localhost:8013/api-docs 时,我得到了这个结果:
"{\"openapi\":\"3.0.1\",\"info\":{(...)}}"
我尝试使用默认配置,我评论了 configureMessageConverters()
方法,\api-docs
的结果现在看起来正常 JSON:
// 20191218134933
// http://localhost:8013/api-docs
{
"openapi": "3.0.1",
"info": {(...)}
}
我记得当我使用 Springfox 时序列化有问题,我的 customGson
有额外的行:.registerTypeAdapter(Json::class.java, JsonSerializer<Json> { src, _, _ -> JsonParser.parseString(src.value()) })
我想知道我应该有特殊的 JsonSerializer
。调试后我的第一个想法是 OpenApi
class in io.swagger.v3.oas.models
package。我添加了这段代码:.registerTypeAdapter(OpenAPI::class.java, JsonSerializer<OpenAPI> { _, _, _ -> JsonParser.parseString("") })
到 customGson
并没有任何改变......所以,我正在深入挖掘......
在我 运行 我的 Swagger 测试之后:
@EnableAutoConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ExtendWith(SpringExtension::class)
@ActiveProfiles("test")
class SwaggerIntegrationTest(@Autowired private val mockMvc: MockMvc) {
@Test
fun `should display Swagger UI page`() {
val result = mockMvc.perform(MockMvcRequestBuilders.get("/swagger-ui/index.html"))
.andExpect(status().isOk)
.andReturn()
assertTrue(result.response.contentAsString.contains("Swagger UI"))
}
@Disabled("Redirect doesn't work. Check it later")
@Test
fun `should display Swagger UI page with redirect`() {
mockMvc.perform(MockMvcRequestBuilders.get("/swagger-ui.html"))
.andExpect(status().isOk)
.andExpect(MockMvcResultMatchers.content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
}
@Test
fun `should get api docs`() {
mockMvc.perform(MockMvcRequestBuilders.get("/api-docs"))
.andExpect(status().isOk)
.andExpect(MockMvcResultMatchers.content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.openapi").exists())
}
}
我在控制台中看到了这个:
MockHttpServletRequest:
HTTP Method = GET
Request URI = /api-docs
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = org.springdoc.api.OpenApiResource
Method = org.springdoc.api.OpenApiResource#openapiJson(HttpServletRequest, String)
接下来我在 OpenApiResource
中检查 openapiJson
并且...
@Operation(hidden = true)
@GetMapping(value = API_DOCS_URL, produces = MediaType.APPLICATION_JSON_VALUE)
public String openapiJson(HttpServletRequest request, @Value(API_DOCS_URL) String apiDocsUrl)
throws JsonProcessingException {
calculateServerUrl(request, apiDocsUrl);
OpenAPI openAPI = this.getOpenApi();
return Json.mapper().writeValueAsString(openAPI);
}
好的,Jackson...我已通过 @EnableAutoConfiguration(exclude = [(JacksonAutoConfiguration::class)])
禁用了 Jackson,因为我(和我的同事)更喜欢 GSON,但这并不能解释为什么在添加自定义后序列化出错 GsonHttpMessageConverter
。我不知道我做错了什么。这个 openapiJson()
是端点,也许它搞砸了……我不知道。我不知道。你有类似的问题吗?你能给点建议或提示吗?
PS。抱歉我的英语不好:).
我在用 Java 编写的项目中遇到了同样的问题,我刚刚通过定义一个过滤器来使用 Gson 格式化我的 springdoc-openapi json 文档来解决这个问题。我想您可以轻松地将此解决方法移植到 Kotlin。
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
ByteResponseWrapper byteResponseWrapper = new ByteResponseWrapper((HttpServletResponse) response);
ByteRequestWrapper byteRequestWrapper = new ByteRequestWrapper((HttpServletRequest) request);
chain.doFilter(byteRequestWrapper, byteResponseWrapper);
String jsonResponse = new String(byteResponseWrapper.getBytes(), response.getCharacterEncoding());
response.getOutputStream().write((new com.google.gson.JsonParser().parse(jsonResponse).getAsString())
.getBytes(response.getCharacterEncoding()));
}
@Override
public void destroy() {
}
static class ByteResponseWrapper extends HttpServletResponseWrapper {
private PrintWriter writer;
private ByteOutputStream output;
public byte[] getBytes() {
writer.flush();
return output.getBytes();
}
public ByteResponseWrapper(HttpServletResponse response) {
super(response);
output = new ByteOutputStream();
writer = new PrintWriter(output);
}
@Override
public PrintWriter getWriter() {
return writer;
}
@Override
public ServletOutputStream getOutputStream() {
return output;
}
}
static class ByteRequestWrapper extends HttpServletRequestWrapper {
byte[] requestBytes = null;
private ByteInputStream byteInputStream;
public ByteRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream inputStream = request.getInputStream();
byte[] buffer = new byte[4096];
int read = 0;
while ((read = inputStream.read(buffer)) != -1) {
baos.write(buffer, 0, read);
}
replaceRequestPayload(baos.toByteArray());
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
return byteInputStream;
}
public void replaceRequestPayload(byte[] newPayload) {
requestBytes = newPayload;
byteInputStream = new ByteInputStream(new ByteArrayInputStream(requestBytes));
}
}
static class ByteOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos = new ByteArrayOutputStream();
@Override
public void write(int b) {
bos.write(b);
}
public byte[] getBytes() {
return bos.toByteArray();
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
static class ByteInputStream extends ServletInputStream {
private InputStream inputStream;
public ByteInputStream(final InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
您还必须仅为文档 url 模式注册过滤器。
@Bean
public FilterRegistrationBean<DocsFormatterFilter> loggingFilter() {
FilterRegistrationBean<DocsFormatterFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DocsFormatterFilter());
registrationBean.addUrlPatterns("/v3/api-docs");
return registrationBean;
}
我从 Springfox Swagger 迁移到 Springdoc OpenApi。我在关于 springdoc 的配置中添加了几行:
springdoc:
pathsToMatch: /api/**
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
在配置中 class MainConfig.kt
我有以下代码:
val customGson: Gson = GsonBuilder()
.registerTypeAdapter(LocalDateTime::class.java, DateSerializer())
.registerTypeAdapter(ZonedDateTime::class.java, ZonedDateSerializer())
.addSerializationExclusionStrategy(AnnotationExclusionStrategy())
.enableComplexMapKeySerialization()
.setPrettyPrinting()
.create()
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
converters.add(GsonHttpMessageConverter(customGson))
}
当我转到 http://localhost:8013/swagger-ui.html(在配置中我有 server.port: 8013
)时,页面没有重定向到 swagger-ui/index.html?url=/api-docs&validatorUrl=
。但这不是我的主要问题:)。当我转到 swagger-ui/index.html?url=/api-docs&validatorUrl=
时,我得到了包含以下信息的页面:
Unable to render this definition The provided definition does not specify a valid version field. Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: "2.0" and those that match openapi: 3.0.n (for example, openapi: 3.0.0).
但是当我访问 http://localhost:8013/api-docs 时,我得到了这个结果:
"{\"openapi\":\"3.0.1\",\"info\":{(...)}}"
我尝试使用默认配置,我评论了 configureMessageConverters()
方法,\api-docs
的结果现在看起来正常 JSON:
// 20191218134933
// http://localhost:8013/api-docs
{
"openapi": "3.0.1",
"info": {(...)}
}
我记得当我使用 Springfox 时序列化有问题,我的 customGson
有额外的行:.registerTypeAdapter(Json::class.java, JsonSerializer<Json> { src, _, _ -> JsonParser.parseString(src.value()) })
我想知道我应该有特殊的 JsonSerializer
。调试后我的第一个想法是 OpenApi
class in io.swagger.v3.oas.models
package。我添加了这段代码:.registerTypeAdapter(OpenAPI::class.java, JsonSerializer<OpenAPI> { _, _, _ -> JsonParser.parseString("") })
到 customGson
并没有任何改变......所以,我正在深入挖掘......
在我 运行 我的 Swagger 测试之后:
@EnableAutoConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ExtendWith(SpringExtension::class)
@ActiveProfiles("test")
class SwaggerIntegrationTest(@Autowired private val mockMvc: MockMvc) {
@Test
fun `should display Swagger UI page`() {
val result = mockMvc.perform(MockMvcRequestBuilders.get("/swagger-ui/index.html"))
.andExpect(status().isOk)
.andReturn()
assertTrue(result.response.contentAsString.contains("Swagger UI"))
}
@Disabled("Redirect doesn't work. Check it later")
@Test
fun `should display Swagger UI page with redirect`() {
mockMvc.perform(MockMvcRequestBuilders.get("/swagger-ui.html"))
.andExpect(status().isOk)
.andExpect(MockMvcResultMatchers.content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
}
@Test
fun `should get api docs`() {
mockMvc.perform(MockMvcRequestBuilders.get("/api-docs"))
.andExpect(status().isOk)
.andExpect(MockMvcResultMatchers.content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.openapi").exists())
}
}
我在控制台中看到了这个:
MockHttpServletRequest:
HTTP Method = GET
Request URI = /api-docs
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = org.springdoc.api.OpenApiResource
Method = org.springdoc.api.OpenApiResource#openapiJson(HttpServletRequest, String)
接下来我在 OpenApiResource
中检查 openapiJson
并且...
@Operation(hidden = true)
@GetMapping(value = API_DOCS_URL, produces = MediaType.APPLICATION_JSON_VALUE)
public String openapiJson(HttpServletRequest request, @Value(API_DOCS_URL) String apiDocsUrl)
throws JsonProcessingException {
calculateServerUrl(request, apiDocsUrl);
OpenAPI openAPI = this.getOpenApi();
return Json.mapper().writeValueAsString(openAPI);
}
好的,Jackson...我已通过 @EnableAutoConfiguration(exclude = [(JacksonAutoConfiguration::class)])
禁用了 Jackson,因为我(和我的同事)更喜欢 GSON,但这并不能解释为什么在添加自定义后序列化出错 GsonHttpMessageConverter
。我不知道我做错了什么。这个 openapiJson()
是端点,也许它搞砸了……我不知道。我不知道。你有类似的问题吗?你能给点建议或提示吗?
PS。抱歉我的英语不好:).
我在用 Java 编写的项目中遇到了同样的问题,我刚刚通过定义一个过滤器来使用 Gson 格式化我的 springdoc-openapi json 文档来解决这个问题。我想您可以轻松地将此解决方法移植到 Kotlin。
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
ByteResponseWrapper byteResponseWrapper = new ByteResponseWrapper((HttpServletResponse) response);
ByteRequestWrapper byteRequestWrapper = new ByteRequestWrapper((HttpServletRequest) request);
chain.doFilter(byteRequestWrapper, byteResponseWrapper);
String jsonResponse = new String(byteResponseWrapper.getBytes(), response.getCharacterEncoding());
response.getOutputStream().write((new com.google.gson.JsonParser().parse(jsonResponse).getAsString())
.getBytes(response.getCharacterEncoding()));
}
@Override
public void destroy() {
}
static class ByteResponseWrapper extends HttpServletResponseWrapper {
private PrintWriter writer;
private ByteOutputStream output;
public byte[] getBytes() {
writer.flush();
return output.getBytes();
}
public ByteResponseWrapper(HttpServletResponse response) {
super(response);
output = new ByteOutputStream();
writer = new PrintWriter(output);
}
@Override
public PrintWriter getWriter() {
return writer;
}
@Override
public ServletOutputStream getOutputStream() {
return output;
}
}
static class ByteRequestWrapper extends HttpServletRequestWrapper {
byte[] requestBytes = null;
private ByteInputStream byteInputStream;
public ByteRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream inputStream = request.getInputStream();
byte[] buffer = new byte[4096];
int read = 0;
while ((read = inputStream.read(buffer)) != -1) {
baos.write(buffer, 0, read);
}
replaceRequestPayload(baos.toByteArray());
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
return byteInputStream;
}
public void replaceRequestPayload(byte[] newPayload) {
requestBytes = newPayload;
byteInputStream = new ByteInputStream(new ByteArrayInputStream(requestBytes));
}
}
static class ByteOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos = new ByteArrayOutputStream();
@Override
public void write(int b) {
bos.write(b);
}
public byte[] getBytes() {
return bos.toByteArray();
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
static class ByteInputStream extends ServletInputStream {
private InputStream inputStream;
public ByteInputStream(final InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
您还必须仅为文档 url 模式注册过滤器。
@Bean
public FilterRegistrationBean<DocsFormatterFilter> loggingFilter() {
FilterRegistrationBean<DocsFormatterFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DocsFormatterFilter());
registrationBean.addUrlPatterns("/v3/api-docs");
return registrationBean;
}