web-dev-qa-db-fra.com

Le point de terminaison "/ api-docs" ne fonctionne pas avec GsonHttpMessageConverter personnalisé

J'ai migré de Springfox Swagger vers Springdoc OpenApi. J'ai ajouté quelques lignes dans ma configuration à propos de springdoc:

springdoc:
  pathsToMatch: /api/**
  packagesToScan: pl.sims.invipoconnector
  api-docs:
    path: /api-docs
  swagger-ui:
    path: /swagger-ui.html

Dans la classe de configuration MainConfig.kt, J'ai le code suivant:

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))
    }

Quand je vais à http: // localhost: 8013/swagger-ui.html (dans la configuration j'ai server.port: 8013) La page n'est pas redirigée vers swagger-ui/index.html?url=/api-docs&validatorUrl=. Mais ce n'est pas mon problème principal :). Lorsque je vais à swagger-ui/index.html?url=/api-docs&validatorUrl=, J'ai une page contenant ces informations:

 Impossible de rendre cette définition 
 La définition fournie ne spécifie pas de champ de version valide. 
 
 Veuillez indiquer un champ de version Swagger ou OpenAPI valide. Les champs de version pris en charge sont swagger: "2.0" et ceux qui correspondent à openapi: 3.0.n (par exemple, openapi: 3.0.0). 

Mais quand je vais à http: // localhost: 8013/api-docs j'ai ce résultat:

"{\"openapi\":\"3.0.1\",\"info\":{(...)}}"

J'ai essayé d'utiliser la configuration par défaut et j'ai commenté la méthode configureMessageConverters() et le résultat de \api-docs Ressemble maintenant au JSON normal:

// 20191218134933
// http://localhost:8013/api-docs

{
  "openapi": "3.0.1",
  "info": {(...)}
}

Je me souviens que lorsque j'utilisais Springfox, il y avait un problème avec la sérialisation et mon customGson avait une ligne supplémentaire: .registerTypeAdapter(Json::class.Java, JsonSerializer<Json> { src, _, _ -> JsonParser.parseString(src.value()) })

Je me demandais que je devrais avoir un JsonSerializer spécial. Après le débogage, ma première pensée conduisait à la classe OpenApi dans le package io.swagger.v3.oas.models. J'ai ajouté ce code: .registerTypeAdapter(OpenAPI::class.Java, JsonSerializer<OpenAPI> { _, _, _ -> JsonParser.parseString("") }) à customGson et rien n'a changé ... Donc, je creusais plus profondément ...

Après quand j'ai exécuté mes tests 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())
    }
}

J'ai vu dans la console ceci:

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)

Ensuite, je vérifie openapiJson dans OpenApiResource et ...

    @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);
    }

OK, Jackson ... J'ai désactivé Jackson par @EnableAutoConfiguration(exclude = [(JacksonAutoConfiguration::class)]) parce que moi (et mes collègues) préfère GSON, mais cela n'explique pas pourquoi la sérialisation tourne mal après l'ajout de GsonHttpMessageConverter personnalisé. Je n'ai aucune idée de ce que j'ai fait de mal. Cette openapiJson() est le point final et peut-être que ça gâche quelque chose ... Je ne sais pas. Je n'ai aucune idée. Avez-vous eu un problème similaire? Pouvez-vous donner des conseils ou des indices?

PS. Désolé pour mon mauvais anglais :).

5
powermilk

J'ai eu le même problème avec un projet écrit en Java, et je viens de le résoudre en définissant un filtre pour formater ma documentation springdoc-openapi json à l'aide de Gson. Je suppose que vous pouvez facilement porter cette solution de contournement vers 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) {

    }
}

Vous devrez également enregistrer votre filtre uniquement pour votre modèle d'URL de documentation.

@Bean
public FilterRegistrationBean<DocsFormatterFilter> loggingFilter() {
    FilterRegistrationBean<DocsFormatterFilter> registrationBean = new FilterRegistrationBean<>();

    registrationBean.setFilter(new DocsFormatterFilter());
    registrationBean.addUrlPatterns("/v3/api-docs");

    return registrationBean;
}
1
anselmoalves