web-dev-qa-db-ja.com

エンドポイント「/ api-docs」がカスタムGsonHttpMessageConverterで機能しない

Springfox SwaggerからSpringdoc OpenApiに移行しました。 springdocに関する構成に数行を追加しました。

_springdoc:
  pathsToMatch: /api/**
  packagesToScan: pl.sims.invipoconnector
  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&validatorUrl=_にリダイレクトされません。しかし、これは私の主な問題ではありません:)。 _swagger-ui/index.html?url=/api-docs&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<Json> { src, _, _ -> JsonParser.parseString(src.value()) })

特別なJsonSerializerが必要だと思っていました。デバッグ後、私の最初の考えは_io.swagger.v3.oas.models_パッケージのOpenApiクラスにつながることでした。私は次のコードを追加しました:.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)
_

次に、openapiJsonOpenApiResourceをチェックし、...

_    @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、ジャクソン... @EnableAutoConfiguration(exclude = [(JacksonAutoConfiguration::class)])でジャクソンを無効にしました。私(および私の同僚)はGSONを好みますが、カスタムGsonHttpMessageConverterを追加した後にシリアル化がうまくいかない理由は説明されていません。何が悪いのか分かりません。このopenapiJson()はエンドポイントであり、おそらく混乱しています...わかりません。何も思いつきません。同様の問題がありましたか?アドバイスやヒントはありますか?

PS。私の悪い英語でごめんなさい:).

5
powermilk

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;
}
1
anselmoalves