如何通过 Spring MVC 从外部源异步流大文件?

5 java asynchronous servlets spring-mvc resttemplate

呼吁所有 Spring 框架专家,

设想

用户A向 Spring 控制器执行GET请求,后者 GET向远程主机发出另一个请求以获取文件,该文件的内容将流式传输(缓冲区字节复制)给初始用户A作为响应。

PS:Spring Controller 的作用就像一个代理

使用

  • Spring WebMVC 4.3.3.RELEASE
  • 阿帕奇汤姆猫 8.5.5

问题

好像没有可用的 Servlet 请求线程或者其他东西被阻止了......

在下面列出的所有情况下,第一个用户都可以启动文件下载。不幸的是,Spring会停止调用控制器下载方法,直到第一次下载完成为止(但有时,它会在用户等待 XX 秒后调用它)。尝试过的方法

  • @Async在包含方法的服务上- 当响应输出缓冲区为( )时RestTemplate抛出。服务-方法。NullPointerExceptionbyte copy operationnullat org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.doWrite(Http11OutputBuffer.java:561)download
  • StreamingResponseBody@Async即使包含在返回的服务级别中,也无法解决并发下载问题AsyncResult<StreamingResponseBody>。服务-downloadAsync方法。

也许有人知道在 Spring 中执行此操作的更好方法?

方法

  • RestTemplate其执行FileCopyUtils.copy(downloadResponse.getBody(), userResponse.getOutputStream());范围为ResponseExtractor.
  • HttpsURLConnectionFileCopyUtils.copy(downloadResponse.getBody(), userResponse.getOutputStream());在控制器返回时执行StreamingResponseBody

所有列出的方法都有效,它们将文件内容从一台服务器响应传输InputStream到用户响应OutputStream。但是,并发下载存在问题(Spring停止调用控制器的下载方法)。

来源

私人信息(网址、身份验证等)已被替换,代码严格出于问题演示目的而编写

调度员

通过扩展构造AbstractAnnotationConfigDispatcherServletInitializer.

控制器

包含不同方法的 3 个端点。

    @RequestMapping(value = "/download/way1", method = RequestMethod.GET)
    public void requestDownloadPage(final HttpServletResponse downloadResponse, final HttpServletRequest downloadRequest) throws ExecutionException, InterruptedException, URISyntaxException {
         dwn.download(downloadResponse);
    }

    @RequestMapping(value = "/download/way2", method = RequestMethod.GET)
    public StreamingResponseBody requestDownloadPage2(final HttpServletResponse response) throws ExecutionException, InterruptedException, URISyntaxException {
       return dwn.downloadAsync(response).get();
    }

    @RequestMapping(value = "/download/way3", method = RequestMethod.GET)
    public StreamingResponseBody requestDownloadPage3(final HttpServletResponse response) throws ExecutionException, InterruptedException, URISyntaxException, IOException {
        return outputStream -> {
            URL url = new URL("https://some/url/path/veryBig.zip");
            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
            connection.setDoOutput(false);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setRequestProperty ("Authorization", "Basic " + Base64.getEncoder().encodeToString("username:password".getBytes(StandardCharsets.UTF_8)));
            connection.setRequestMethod("GET");

            response.addHeader(HttpHeaders.CONTENT_TYPE, connection.getHeaderField(HttpHeaders.CONTENT_TYPE));
            response.addHeader(HttpHeaders.CONTENT_LENGTH, connection.getHeaderField(HttpHeaders.CONTENT_LENGTH));
            String disposition = connection.getHeaderField(HttpHeaders.CONTENT_DISPOSITION);
            if (disposition != null && !disposition.isEmpty()) {
                response.addHeader(HttpHeaders.CONTENT_DISPOSITION, disposition);
            }

            try (InputStream inputStream = connection.getInputStream();) {
                FileCopyUtils.copy(inputStream, outputStream);

            } catch (Throwable any) {
                // failed
            }
        };
    }
Run Code Online (Sandbox Code Playgroud)

服务

public void download(HttpServletResponse downloadResponse) {
    final RestTemplate restTemplate = new RestTemplate();
    RequestCallback requestCallback = new RequestCallback() {
        @Override
        public void doWithRequest(ClientHttpRequest request) throws IOException {
            request.getHeaders().set(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("username:password".getBytes(StandardCharsets.UTF_8)));
        }
    };

    ResponseExtractor<Void> responseExtractor = response -> {

        List<String> type = response.getHeaders().get(HttpHeaders.CONTENT_TYPE);
        List<String> length = response.getHeaders().get(HttpHeaders.CONTENT_LENGTH);
        List<String> disposition = response.getHeaders().get(HttpHeaders.CONTENT_DISPOSITION);

        downloadResponse.addHeader(HttpHeaders.CONTENT_TYPE, type.get(0));
        downloadResponse.addHeader(HttpHeaders.CONTENT_LENGTH, length.get(0));
        if (disposition != null && !disposition.isEmpty()) {
            downloadResponse.addHeader(HttpHeaders.CONTENT_DISPOSITION, disposition.get(0));
        }

        FileCopyUtils.copy(response.getBody(), downloadResponse.getOutputStream());

        return null;
    };


    restTemplate.execute("https://some/url/path/veryBig.zip",
            HttpMethod.GET,
            requestCallback,
            responseExtractor);
}

@Async
public Future<StreamingResponseBody> downloadAsync(HttpServletResponse response) {
    final RestTemplate restTemplate = new RestTemplate();
    RequestCallback requestCallback = new RequestCallback() {
        @Override
        public void doWithRequest(ClientHttpRequest request) throws IOException {
            request.getHeaders().set(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("username:password".getBytes(StandardCharsets.UTF_8)));
        }
    };

    ResponseExtractor<StreamingResponseBody> responseExtractor = responseOwnCloud -> {

        List<String> type = responseOwnCloud.getHeaders().get(HttpHeaders.CONTENT_TYPE);
        List<String> length = responseOwnCloud.getHeaders().get(HttpHeaders.CONTENT_LENGTH);
        List<String> disposition = responseOwnCloud.getHeaders().get(HttpHeaders.CONTENT_DISPOSITION);

        response.setHeader(HttpHeaders.CONTENT_TYPE, type.get(0));
        response.setHeader(HttpHeaders.CONTENT_LENGTH, length.get(0));
        if (disposition != null && !disposition.isEmpty()) {
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, disposition.get(0));
        }

        return outputStream -> {
            FileCopyUtils.copy(responseOwnCloud.getBody(), response.getOutputStream());
        };
    };


    return new AsyncResult<>(restTemplate.execute("https://some/url/path/veryBig.zip",
            HttpMethod.GET,
            requestCallback,
            responseExtractor));

}
Run Code Online (Sandbox Code Playgroud)

Spring异步配置

@Configuration
@ComponentScan(basePackages = { "some.service"})
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(4);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("AsyncExec-");
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)