web-dev-qa-db-ja.com

Spring Boot @ RestControllerでチャンク応答をストリーミングする方法

私はこれに一日のように費やしました、そして私はうまくいく解決策を見つけることができません。このアプリケーションには、大きな応答を返すことができるエンドポイントがいくつかあります。私は、データベースクエリの結果を処理するときに、応答をストリーミングできるメカニズムを見つけようとしています。主な目標は、サービス側でピークメモリ使用量を制限し(メモリ内の応答全体を必要としない)、応答の最初のバイトまでの時間を最小限に抑えることです(応答が内に到着し始めない場合、クライアントシステムにはタイムアウトがあります)指定時間-10分)。これがとても難しいのには本当に驚いています。

StreamingResponseBodyを見つけました。これは私たちが望んでいたものに近いように見えましたが、非同期の側面は実際には必要ありませんが、クエリ結果を処理するときに応答のストリーミングを開始できるようにしたいだけです。 @ResponseBodyでアノテーションを付けたり、voidを返したり、OutputStreamのパラメーターを追加したりするなど、他のアプローチも試しましたが、渡されたOutputStreamは基本的に結果全体をバッファリングするCachingOutputStreamであったため、機能しませんでした。これが私が今持っているものです...

リソース方法:

@GetMapping(value = "/catalog/features")
public StreamingResponseBody findFeatures(                                      
        @RequestParam("provider-name") String providerName,
        @RequestParam(name = "category", required = false) String category,
        @RequestParam("date") String date,
        @RequestParam(value = "version-state", defaultValue = "*") String versionState) {

    CatalogVersionState catalogVersionState = getCatalogVersionState(versionState);

    log.info("GET - Starting DB query...");
    final List<Feature> features 
        = featureService.findFeatures(providerName, 
                                      category, 
                                      ZonedDateTime.parse(date), 
                                      catalogVersionState);
    log.info("GET - Query done!");

    return new StreamingResponseBody() {
        @Override
        public void writeTo(OutputStream outputStream) throws IOException {
            log.info("GET - Transforming DTOs");
            JsonFactory jsonFactory = new JsonFactory();
            JsonGenerator jsonGenerator = jsonFactory.createGenerator(outputStream);
            Map<Class<?>, JsonSerializer<?>> serializerMap = new HashMap<>();
            serializerMap.put(DetailDataWrapper.class, new DetailDataWrapperSerializer());
            serializerMap.put(ZonedDateTime.class, new ZonedDateTimeSerializer());
            ObjectMapper jsonMapper =  Jackson2ObjectMapperBuilder.json()
                .serializersByType(serializerMap)
                .deserializerByType(ZonedDateTime.class, new ZonedDateTimeDeserializer())
                .build();
            jsonGenerator.writeStartArray();
            for (Feature feature : features) {
                FeatureDto dto = FeatureMapper.MAPPER.featureToFeatureDto(feature);
                jsonMapper.writeValue(jsonGenerator, dto);
                jsonGenerator.flush();
            }
            jsonGenerator.writeEndArray();
            log.info("GET - DTO transformation done!");
        }
    };
}

非同期構成:

@Configuration
@EnableAsync
@EnableScheduling
public class ProductCatalogStreamingConfig extends WebMvcConfigurerAdapter {

    private final Logger log = LoggerFactory.getLogger(ProductCatalogStreamingConfig.class);

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setDefaultTimeout(360000).setTaskExecutor(getAsyncExecutor());
        configurer.registerCallableInterceptors(callableProcessingInterceptor());
    }

    @Bean(name = "taskExecutor")
    public AsyncTaskExecutor getAsyncExecutor() {
        log.debug("Creating Async Task Executor");
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("AsyncStreaming-");
        return executor;
    }

    @Bean
    public CallableProcessingInterceptor callableProcessingInterceptor() {
        return new TimeoutCallableProcessingInterceptor() {
            @Override
            public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task) throws 
Exception {
                log.error("timeout!");
                return super.handleTimeout(request, task);
            }
        };
    }
}

StreamingResponseBody.writeTo()が呼び出されるとすぐにクライアントが応答を確認し始め、応答ヘッダーに次のものが含まれることを期待していました。

Content-Encoding: chunked

だがしかし

Content-Length: xxxx

代わりに、StreamingResponseBody.writeTo()が返され、応答にContent-Lengthが含まれるまで、クライアントに応答は表示されません。 (ただし、コンテンツエンコーディングではありません)

私の質問は、writeTo()でOutputStreamに書き込んでいるときに、ペイロード全体をキャッシュせず、最後にのみ送信するようにSpringに指示する秘密のソースは何ですか?皮肉なことに、チャンクエンコーディングを無効にする方法を知りたいが、有効にする方法については何も知りたくない投稿を見つけました。

4
sceaj

上記のコードは、私たちが求めていたものを正確に実行していることがわかりました。私たちが観察した動作は、Springがこれらの機能を実装した方法によるものではなく、通常のSpringの動作を妨げるサーブレットフィルターをインストールした企業固有のスターターが原因でした。このフィルターはHttpServletResponseOutputStreamをラップしました。そのため、質問に記載されているCachingOutputStreamを確認しました。スターターを削除した後、上記のコードは期待どおりに動作し、この動作を妨げない方法でサーブレットフィルターを再実装しています。

1
sceaj