Spring REST 端点返回 StreamingResponseBody:30 秒后 AsyncRequestTimeoutException

Spring REST endpoint returning StreamingResponseBody: AsyncRequestTimeoutException after 30 seconds

我遇到了与描述相同的问题 here and here

我尝试了给出的答案及其组合,但 none 解决了我的问题。

当我尝试 this 回答时,30 秒后,下载从头开始重新开始,而不是超时,然后又过了 30 秒,然后超时。

我正在通过访问 Google Chrome 中的 REST 端点并尝试从那里下载文件来进行测试。

Here 我有显示此错误的项目。

提前致谢。

编辑: 来源:

src\main\java\io\github\guiritter\transferer_local\TransfererLocalApplication.java

package io.github.guiritter.transferer_local;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class TransfererLocalApplication {

    public static void main(String[] args) {
        SpringApplication.run(TransfererLocalApplication.class, args);
    }
}

src\main\java\io\github\guiritter\transferer_local\DefaultController.java

package io.github.guiritter.transferer_local;

import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.webmvc.RepositoryRestController;
// import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@RepositoryRestController
@RequestMapping("api")
public class DefaultController {

    @Value("${fileName}")
    private String fileName;

    @Value("${filePath}")
    private String filePath;

    @GetMapping("download")
    public StreamingResponseBody downloadHub(HttpServletResponse response) throws IOException {
        File file = new File(filePath + fileName);
        response.setContentType(APPLICATION_OCTET_STREAM_VALUE);
        response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
        response.setHeader("Content-Length", file.length() + "");
        InputStream inputStream = new FileInputStream(file);
        return outputStream -> {
            int nRead;
            byte[] data = new byte[1024*1024];
            while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
                outputStream.write(data, 0, nRead);
            }
            inputStream.close();
        };
    }

    // @GetMapping("download")
    // public ResponseEntity<StreamingResponseBody> downloadHub(HttpServletResponse response) throws IOException {
    //  File file = new File(filePath + fileName);
    //  response.setContentType(APPLICATION_OCTET_STREAM_VALUE);
    //  response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
    //  response.setHeader("Content-Length", file.length() + "");
    //  InputStream inputStream = new FileInputStream(file);

    //  return ResponseEntity.ok(outputStream -> {
    //      int nRead;
    //      byte[] data = new byte[1024*1024];
    //      while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
    //          outputStream.write(data, 0, nRead);
    //      }
    //      inputStream.close();
    //  });
    // }
}

src\main\java\io\github\guiritter\transferer_local\AsyncConfiguration.java

package io.github.guiritter.transferer_local;

// import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfiguration implements AsyncConfigurer {

    @Override
    @Bean(name = "taskExecutor")
    public AsyncTaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(Integer.MAX_VALUE);
        executor.setThreadNamePrefix("io.github.guiritter.transferer_local.async_executor_thread.");
        return executor;
    }

    /** Configure async support for Spring MVC. */
    @Bean
    public WebMvcConfigurer webMvcConfigurerAdapter(
            AsyncTaskExecutor taskExecutor) {
        return new WebMvcConfigurer() {

            @Override
            public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
                configurer
                        .setDefaultTimeout(Long.MAX_VALUE)
                        .setTaskExecutor(taskExecutor);
                configureAsyncSupport(configurer);
            }
        };
    }

    // @Autowired
    // private AsyncTaskExecutor taskExecutor;

    // /** Configure async support for Spring MVC. */
    // @Bean
    // public WebMvcConfigurer webMvcConfigurerAdapter() {
    //  return new WebMvcConfigurer() {

    //      @Override
    //      public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    //          configurer
    //                  .setDefaultTimeout(Long.MAX_VALUE)
    //                  .setTaskExecutor(taskExecutor);
    //          configureAsyncSupport(configurer);
    //      }
    //  };
    // }
}

src\main\java\io\github\guiritter\transferer_local\MyConfiguration.java

package io.github.guiritter.transferer_local;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
@EnableTransactionManagement
@EnableAsync
public class MyConfiguration implements WebMvcConfigurer {

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setDefaultTimeout(-1);
    }
}

src\main\resources\application.properties

server.port=8081
fileName=large_file_name.txt
filePath=C:\path\to\large\file\

# spring.mvc.async.request-timeout = 9223372036854775807
# spring.mvc.async.request-timeout = 2147483647
spring.mvc.async.request-timeout = -1

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.github.guiritter</groupId>
    <artifactId>transferer-local</artifactId>
    <version>1.0.0</version>
    <name>TransfererLocal</name>
    <description>Enables local network file transfer</description>

    <properties>
        <java.version>14</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-rest-webmvc -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-rest-webmvc</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

更新: 尝试 Manuel 的回答(提交到分支 answer_Manuel):

src\main\java\io\github\guiritter\transferer_local\DefaultController.java

package io.github.guiritter.transferer_local;

import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@RepositoryRestController
@RequestMapping("api")
public class DefaultController {

    @Value("${fileName}")
    private String fileName;

    @Value("${filePath}")
    private String filePath;

    @GetMapping("download")
    public WebAsyncTask<ResponseEntity<StreamingResponseBody>> downloadHub(HttpServletResponse response) throws IOException {
        File file = new File(filePath + fileName);
        response.setContentType(APPLICATION_OCTET_STREAM_VALUE);
        response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
        response.setHeader("Content-Length", file.length() + "");
        InputStream inputStream = new FileInputStream(file);

        return new WebAsyncTask<ResponseEntity<StreamingResponseBody>>(Long.MAX_VALUE, () ->

            ResponseEntity.<StreamingResponseBody>ok(outputStream -> {

                int nRead;
                byte[] data = new byte[1024*1024];
                while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
                    outputStream.write(data, 0, nRead);
                }
                inputStream.close();
            })
        );
    }
}

它抛出 AsyncRequestTimeoutException 和这个:

java.lang.IllegalArgumentException: Cannot dispatch without an AsyncContext
        at org.springframework.util.Assert.notNull(Assert.java:198) ~[spring-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.dispatch(StandardServletAsyncWebRequest.java:131) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.web.context.request.async.WebAsyncManager.setConcurrentResultAndDispatch(WebAsyncManager.java:391) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing(WebAsyncManager.java:315) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.lambda$onError[=18=](StandardServletAsyncWebRequest.java:146) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1510) ~[na:na]
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.onError(StandardServletAsyncWebRequest.java:146) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.apache.catalina.core.AsyncListenerWrapper.fireOnError(AsyncListenerWrapper.java:49) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.catalina.core.AsyncContextImpl.setErrorState(AsyncContextImpl.java:422) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:239) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:237) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:860) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1591) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630) ~[na:na]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
        at java.base/java.lang.Thread.run(Thread.java:832) ~[na:na]

更新: 尝试 Manuel 的更新答案(致力于分支 answer_Manuel_2020-04-06):

src\main\java\io\github\guiritter\transferer_local\DefaultController.java

package io.github.guiritter.transferer_local;

import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@RepositoryRestController
@RequestMapping("api")
public class DefaultController {

    @Value("${fileName}")
    private String fileName;

    @Value("${filePath}")
    private String filePath;

    @GetMapping("download")
    public ResponseEntity<StreamingResponseBody> downloadHub() throws IOException {
        File file = new File(filePath + fileName);
        InputStream inputStream = new FileInputStream(file);
        return ResponseEntity
                .ok()
                .contentType(APPLICATION_OCTET_STREAM)
                .header("Content-Disposition", "attachment; filename=" + fileName)
                .header("Content-Length", file.length() + "")
                .<StreamingResponseBody>body(outputStream -> {
                    int nRead;
                    byte[] data = new byte[1024*1024];
                    while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
                        outputStream.write(data, 0, nRead);
                    }
                    inputStream.close();
                });
    }
}

解决问题:

@RepositoryRestController 更改为例如 @RestController

如果您使用 @RepositoryRestController,超时将设置为 RequestMappingHandlerAdapter。但是在请求下载时,RepositoryRestHandlerAdapter 会处理请求,因为注释要求他这样做。

如果您使用 @RestController,那么(正确的)RequestMappingHandlerAdapter 将处理下载,超时设置为 -1。

原答案:

您可以尝试 declarative/explicit 定义超时,返回一个 org.springframework.web.context.request.async.WebAsyncTask

如果提议设置一个Callable<V> with a timeout:

那么您的 DefaultController 可能如下所示:

public WebAsyncTask<ResponseEntity<StreamingResponseBody>> downloadHub() throws IOException {
  ...
  new WebAsyncTask<ResponseEntity<StreamingResponseBody>>(myTimeOutAsLong, callable);
}

更新:

  1. 请从您的 REST 控制器方法中删除 HttpServletResponse 参数。可以肯定的是,HttpServletResponse 中的 OutputStream 不会干扰 StreamingResponseBody 中的 OutputStream
  2. 关于错误“没有 AsyncContext 无法分派”: