Proper way of streaming using ResponseEntity and making sure the InputStream gets closed
you can try to use StreamingResponseBody
StreamingResponseBody
A controller method return value type for asynchronous request processing where the application can write directly to the response OutputStream without holding up the Servlet container thread.
Because you are working on a separate thread, writing directly to the response, your problem to call close()
before return
is solved.
probably you can start by the following example
public ResponseEntity<StreamingResponseBody> export(...) throws FileNotFoundException {
//...
InputStream inputStream = new FileInputStream(new File("/path/to/example/file"));
StreamingResponseBody responseBody = outputStream -> {
int numberOfBytesToWrite;
byte[] data = new byte[1024];
while ((numberOfBytesToWrite = inputStream.read(data, 0, data.length)) != -1) {
System.out.println("Writing some bytes..");
outputStream.write(data, 0, numberOfBytesToWrite);
}
inputStream.close();
};
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=generic_file_name.bin")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(responseBody);
}
You can also try to use Files
(since java 7)
so you don't have to manage InputStream
File file = new File("/path/to/example/file");
StreamingResponseBody responseBody = outputStream -> {
Files.copy(file.toPath(), outputStream);
};
As @Stackee007 described in comment, under heavy load in production environment it's a good practice also to define a @Configuration
class for a TaskExecutor
to tune parameters and manage Async
processes.
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfiguration implements AsyncConfigurer {
private final Logger log = LoggerFactory.getLogger(AsyncConfiguration.class);
private final TaskExecutionProperties taskExecutionProperties;
public AsyncConfiguration(TaskExecutionProperties taskExecutionProperties) {
this.taskExecutionProperties = taskExecutionProperties;
}
// ---------------> Tune parameters here
@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
log.debug("Creating Async Task Executor");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(taskExecutionProperties.getPool().getCoreSize());
executor.setMaxPoolSize(taskExecutionProperties.getPool().getMaxSize());
executor.setQueueCapacity(taskExecutionProperties.getPool().getQueueCapacity());
executor.setThreadNamePrefix(taskExecutionProperties.getThreadNamePrefix());
return executor;
}
// ---------------> Use this task executor also for async rest methods
@Bean
protected WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setTaskExecutor(getTaskExecutor());
}
};
}
@Bean
protected ConcurrentTaskExecutor getTaskExecutor() {
return new ConcurrentTaskExecutor(this.getAsyncExecutor());
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
How to test with mockMvc
You can simply follow this sample code in your integration test as:
.andExpect(request().asyncStarted())
.andDo(MvcResult::getAsyncResult)
.andExpect(status().isOk()).getResponse().getContentAsByteArray();
Content type of ResponseEntity<StreamingResponseBody>
is a MediaType.APPLICATION_OCTET_STREAM
in this example and you can get byte[] (.getContentAsByteArray()
) but you can get String/Json/plaintext of everything depending of your body response content type.
Assuming you are using Spring, your method could return a Resource and let Spring handle the rest (including closing underlying stream). There are few implementations of Resource are available within Spring API or else you need to implement your own. In the end, your method would become simple and would like something like below
public ResponseEntity<Resource> getFo0(...) {
return new InputStreamResource(<Your input stream>);
}
You could refactor all your controller methods that read local files and set their contents as the body of the HTTP response:
Instead of using the ResponseEntity
approach you inject the underlying HttpServletResponse
and copy the bytes of the inputstream returned from your getContent(...)
method to the outputstream of the HttpServletResponse
, e.g. by using IO-related utility methods of Apache CommonsIO or Google Guava library. In any case make sure you close the inputstream! The code below does this implicitly by the use of a 'try-with-resources'-statement that closes the declared inputstream at the end of the statement.
@RequestMapping(value="/foo", method=RequestMethod.GET)
public void getFoo(HttpServletResponse response) {
// use Java7+ try-with-resources
try (InputStream content = getContent(...)) {
// if needed set content type and attachment header
response.addHeader("Content-disposition", "attachment;filename=foo.txt");
response.setContentType("txt/plain");
// copy content stream to the HttpServletResponse's output stream
IOUtils.copy(myStream, response.getOutputStream());
response.flushBuffer();
}
}
reference:
https://docs.oracle.com/javase/7/docs/api/java/io/InputStream.html https://docs.oracle.com/javase/7/docs/api/java/lang/AutoCloseable.html https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html https://google.github.io/guava/releases/19.0/api/docs/com/google/common/io/ByteStreams.html https://commons.apache.org/proper/commons-io/javadocs/api-release/index.html
(especially have a look at methods public static int copy(InputStream input, OutputStream output) throws IOException
and public static int copyLarge(InputStream input, OutputStream output) throws IOException
of class org.apache.commons.io.IOUtils
)