【问题标题】:Endpoint "/api-docs" doesn't work with custom GsonHttpMessageConverter端点“/api-docs”不适用于自定义 GsonHttpMessageConverter
【发布时间】:2019-12-18 13:35:00
【问题描述】:

我从 Springfox Swagger 迁移到 Springdoc OpenApi。我在配置中添加了几行关于 springdoc 的内容:

springdoc:
  pathsToMatch: /api/**
  api-docs:
    path: /api-docs
  swagger-ui:
    path: /swagger-ui.html

在配置类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&amp;validatorUrl=。但这不是我的主要问题:)。当我转到swagger-ui/index.html?url=/api-docs&amp;validatorUrl= 时,我得到了包含以下信息的页面:

无法呈现此定义 提供的定义未指定有效的版本字段。 请指明有效的 Swagger 或 OpenAPI 版本字段。支持的版本字段是 swagger: "2.0" 和那些与 openapi: 3.0.n 匹配的(例如,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&lt;Json&gt; { src, _, _ -&gt; JsonParser.parseString(src.value()) })

我想知道我应该有特殊的JsonSerializer。调试后,我的第一个想法是导致 io.swagger.v3.oas.models 包中的 OpenApi 类。我添加了这段代码:.registerTypeAdapter(OpenAPI::class.java, JsonSerializer&lt;OpenAPI&gt; { _, _, _ -&gt; 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。对不起我的英语不好:)。

【问题讨论】:

    标签: spring-boot kotlin gson swagger-ui openapi


    【解决方案1】:

    我在用 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;
    }
    

    【讨论】:

    • 好的,我试过了,似乎没问题。我的页面有不同的问题 - 我得到了 404 :)。当我转到localhost:8062/api/swagger-ui.html(另一个项目,另一个端口:D)时,我遇到了类似的重定向问题:“无法解析名称为'redirect:/api/swagger-ui/index.html?configUrl=/api-docs /swagger-config' 在名为 'dispatcherServlet' 的 servlet 中”。当我转到此链接时,我有:“出现意外错误(类型=未找到,状态=404)。”。 /api-docs/ 格式有效(漂亮的 JSON),但我没有“/api-docs/swagger-config”。我将尝试降级 Springdoc OpenApi。
    • 好的。我添加 ``` override fun configureViewResolvers(registry: ViewResolverRegistry) = super.configureViewResolvers(registry.apply { viewResolver(InternalResourceViewResolver()) }) ``` 来解决重定向问题。
    • 这个:override fun addResourceHandlers(registry: ResourceHandlerRegistry) { registry.addResourceHandler("/swagger-ui/**").addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/3.25.0/") } 解析查看localhost:8062/swagger-ui/index.html?configUrl=/api-docs/… 页面
    猜你喜欢
    • 1970-01-01
    • 2020-01-16
    • 1970-01-01
    • 1970-01-01
    • 2019-06-19
    • 2015-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多