I'd like to create an Excel sheet dynamically and return that to the client. I'd like to use the StreamingResponseBody as the return type in order to save some RAM. I'm also using try-with-resources to close the workbook automatically. Here is the code.
@RestController
public class ExcelController {
@GetMapping("/hello")
public ResponseEntity<StreamingResponseBody> excel() throws IOException {
try (var workbook = new XSSFWorkbook();) {
workbook.createSheet("hello world");
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=data.xlsx")
.body(workbook::write);
}
}
}
Unfortunately this does not work and I'm getting the following error message.
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
java.io.IOException: Cannot write data, document seems to have been closed already
at org.apache.poi.ooxml.POIXMLDocument.write(POIXMLDocument.java:215) ~[poi-ooxml-5.1.0.jar:5.1.0]
at org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler$StreamingResponseBodyTask.call(StreamingResponseBodyReturnValueHandler.java:111) ~[spring-webmvc-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler$StreamingResponseBodyTask.call(StreamingResponseBodyReturnValueHandler.java:98) ~[spring-webmvc-5.3.13.jar:5.3.13]
at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:337) ~[spring-web-5.3.13.jar:5.3.13]
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) ~[na:na]
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
I already tried using my custom TaskExecutor as described by StreamingResponseBody but that didn't really help. In the end I found a solution that works but I'm wondering why the first solution including try-with-resources and method reference does not work.
@RestController
public class ExcelController {
@GetMapping("/hello")
public ResponseEntity<StreamingResponseBody> excel() throws IOException {
var workbook = new XSSFWorkbook();
workbook.createSheet("hello world");
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=data.xlsx")
.body(
out -> {
workbook.write(out);
workbook.close();
});
}
}
I highly appreciate your help! Thank you
Because the
StreamingResponseBodyworks async and as your try-with-resources is outside of that part it might be that the resource is already closed (I would suspect in 99.9% of the cases?). To fix do all that stuff inside theStreamingResponseBody.To make it a bit more readable (and the fact that I don't like multiline lambdas) you can ofcourse also move that code to a method and call that from your lambda.
This will open (and close) the
Workbookat the proper time.