簡體   English   中英

如何使用 Spring Boot @RestController 流式傳輸分塊響應

[英]How To Stream Chunked Response With 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 發送分塊響應的秘訣是什么? 具有諷刺意味的是,我發現一些帖子想知道如何禁用分塊編碼,但沒有關於啟用它的信息。

事實證明,上面的代碼正是我們所尋求的。 我們觀察到的行為不是由於 Spring 實現這些功能的方式的任何原因,而是由公司特定的啟動器引起的,該啟動器安裝了干擾 Spring 正常行為的 servlet 過濾器。 這個過濾器包裝了 HttpServletResponse OutputStream,這就是我們觀察問題中提到的 CachingOutputStream 的原因。 刪除啟動器后,上述代碼的行為與我們希望的完全一樣,我們正在以不會干擾此行為的方式重新實現 servlet 過濾器。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM