3

I have a request mapping that looks like this:

private final static byte[] byteArray = ...;

@RequestMapping(value=Array("/foobar"))
void sendByteArray(@RequestBody Request request, OutputStream os) {
  os.write(byteArray);
  os.flush();
  doLengthyCleanup();
}

I'm finding that the request client does not actually receive the response body until after the service has completed doLengthyCleanup().

Since the cleanup doesn't affect the response itself, I'd like to improve my response time by performing the cleanup after sending the response. How can I do this?

Cory Klein
  • 51,188
  • 43
  • 183
  • 243
  • One solution is to execute the cleanup asynchronously. – Amir Pashazadeh May 31 '16 at 17:07
  • I had an expectation that Spring would actually pass the bytes on through the output stream synchronously, making any sort of threading unnecessary and keeping things simpler. It seems that there should be a first-class supported method of doing this in Spring that is simpler than creating a whole new thread or tying into the `HandlerInterceptorAdapter`. – Cory Klein May 31 '16 at 17:09
  • Is byte array a class member? Statefull controller is not thread-safe. – Abylay Sabirgaliyev May 31 '16 at 17:11
  • @doge `byteArray` is just there for illustration. In reality the controller generates the response value based on the request itself, but I think that's irrelevant to the question at hand. For simplicity, you could consider `byteArray` to be static content. – Cory Klein May 31 '16 at 17:13
  • What about calling `response.flush()`? Spring shall somehow know that there is nothing more gonna be written to stream, or you shall `flush` it explicitly. – Amir Pashazadeh May 31 '16 at 17:16
  • @AmirPashazadeh Ah, I forgot to add that to the example. I tried `os.flush()` and it doesn't cause the response to actually be sent. :/ – Cory Klein May 31 '16 at 17:17
  • `Response.flush()` or `OutputStream.flush()`? As I see you don't have `response` in your method signature. – Amir Pashazadeh May 31 '16 at 17:19
  • `os.flush` != `response.flush` but if you want to cleanup in another thread then you will need a `@Async` method or do something in the `afterCompletion` else it will hang and nothing will send to the client (as you already noticed. – M. Deinum May 31 '16 at 17:20
  • Make your controller method [asynchronous](http://stackoverflow.com/questions/17167020/when-to-use-spring-async-vs-callable-controller-async-controller-servlet-3). Spring supports both void or type methods. – Abylay Sabirgaliyev May 31 '16 at 17:20
  • @AmirPashazadeh @M. Deinum, what type of `response` object has a `flush()` method? Not seeing it on javax's `Response` or on `HttpServletResponse`. – Cory Klein May 31 '16 at 17:22
  • The best way to do this by doing asynchronously as suggested by @Amir using java threading. – Naga Srinu Kapusetti May 31 '16 at 17:22
  • If flushing the response allows for me to synchronously send the response body before doing my cleanup, it seems that avoiding threading would be a simpler solution, but it is good to know that `@Async` is an option as well. I'm looking into both. – Cory Klein May 31 '16 at 17:24
  • I have figured it out and posted the solution as an answer. – Cory Klein May 31 '16 at 17:32

2 Answers2

3
@RequestMapping(value=Array("/foobar"))
void sendByteArray(@RequestBody Request request, OutputStream os) {
  os.write(byteArray);
  os.flush(); // not sure
  doLengthyCleanup(); 
}

@Async
void doLengthyCleanup() {
  // this will be executed asynchronously
}

Update: taken from this question

If you are calling the @Async method from another method in the same class, unless you enable AspectJ proxy mode for the @EnableAsync (and provide a weaver of course) that won't work (google "proxy self-invocation"). The easiest fix is to put the @Async method in another @Bean.

Community
  • 1
  • 1
  • I like this better than my alternate solution because I don't have to manage the response object myself. – Cory Klein May 31 '16 at 17:40
  • Also why not to return byte array as ResponseBody? – Abylay Sabirgaliyev May 31 '16 at 17:42
  • I just tried doing so. It appears that the call to `cleanUp()` doesn't get executed asynchronously if I return the `byte[]` afterwards. Hmm. Maybe checking the docs on this, I may need to add some configuration. – Cory Klein May 31 '16 at 17:46
2

As shown in this answer, you need to indicate to Spring that you are handling the response yourself by accepting the response directly and setting the status code yourself:

void sendByteArray(@RequestBody Request request, HttpServletResponse response) {
  response.setStatus(HttpStatus.SC_OK);
  OutputStream os = response.getOutputStream();
  os.write(byteArray);
  os.flush();
  os.close();
  doLengthyCleanup();
}
Community
  • 1
  • 1
Cory Klein
  • 51,188
  • 43
  • 183
  • 243
  • Hey, I tried your solution but didn't work. After wr.close() i have put sleep method for 10sec. When i am hitting the request then it is waiting for 10 sec to send back response. PFB for code. outputStream.close(); System.out.println("Stream closed."); System.out.println("Sleeping"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Awake"); Can you please help if I am missing something? – Vivek Garg Jul 18 '19 at 08:32
  • @gargvive The above code worked just fine for me 3 years ago, but it's impossible to debug your code in the comments of my answer. I'd recommend creating a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) and asking your own question. – Cory Klein Jul 18 '19 at 16:21
  • Sure will do that. Just a quick question though, Does application needs to in spring-boot for the above code to work. We have an application that runs on tomcat7 and has spring-core as one of the dependencies. I am asking because I have tried the same thing to try in a spring boot application and it worked. But our project in which we are trying to implement runs on tomcat. – Vivek Garg Jul 19 '19 at 07:16