Depending on what kind of report and what kind of filter data, the time the app requires to build the report may vary. So, for the same report, the user can get the file immediately, or may need to wait some seconds to get it.
Now, you dont want to force the user to wait for generated the report when it takes much time. If the application detects that the report is lasting much, instead the file, it returns a message telling him that his file will be sent by other means (for example, by sending an email later).
This behaviour can be achieved with Asynchronous tasks, and Futures.
We have a Controller which receives the user request and then calls a service. The service constructs the report, returns it to the controller, and finally the controller returns it to the user. This is the basic flow of much Spring MVC applications.
In this example I have a Service called InvoiceReportService which returns a InvoiceReport.
public class InvoiceReport { private byte[] content; public byte[] getContent() { return content; } public void setContent(byte[] content) { this.content = content; } }
The service will be executed asynchronously so, instead returns a InvoiceReport object it will return a Future<InvoiceReport>:
public interface InvoiceReportService { Future<InvoiceReport> generateReport(); }
This is the service class. It just reads a PDF file from the classpath and returns it. I added a random delay to simulate some heavy tasks running. Note that the service method is annotated with @Async.
@Service public class InvoiceReportServiceImpl implements InvoiceReportService { @Override @Async public Future<InvoiceReport> generateReport() { Resource resource = new ClassPathResource("spring-boot-reference.pdf"); byte[] bytes = null; try { bytes = IOUtils.toByteArray(resource.getInputStream()); } catch (IOException e) { //Nothing } simulateDelay(); InvoiceReport report = new InvoiceReport(); report.setContent(bytes); return new AsyncResult<InvoiceReport>(report); } private void simulateDelay() { int secondsSleep = new Random().nextInt(2); try { System.out.println("Going to sleep " + secondsSleep + " seconds"); TimeUnit.SECONDS.sleep(secondsSleep); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("Awake!!"); } }
Working with futures you can specify a maximum time you want to wait for the result. If the result is not ready in this time, a TimeOutException is raised, but the underlying task is not interrupted.
So, the controller could be something like this:
@Controller public class InvoiceController { @Autowired private InvoiceReportService invoiceReportSerive; @RequestMapping(value="/report", method=RequestMethod.POST) public HttpEntity<byte[]> getReport(HttpServletResponse response) throws IOException { Future<InvoiceReport> reportFutre = invoiceReportSerive.generateReport(); InvoiceReport invoiceReport = null; try { invoiceReport = reportFutre.get(1, TimeUnit.SECONDS); System.out.println("In time"); return responseNow(invoiceReport); } catch(TimeoutException e) { System.out.println("Too late"); response.sendRedirect("/laterPlease"); return null; } catch(Exception e) { //Handle exception return null; } } private HttpEntity<byte[]> responseNow(InvoiceReport invoiceReport) { HttpHeaders header = new HttpHeaders(); header.setContentType(new MediaType("application", "pdf")); header.set("Content-Disposition", "attachment; filename= \"report.pdf\""); header.setContentLength(invoiceReport.getContent().length); return new HttpEntity<byte[]>(invoiceReport.getContent(), header); } }
The controller waits, as much, as one second for the report. If the service does not respond in that time, then shows the user a info page. Otherwise, the controller returns the file.
But this has one problem. How does the user get the report when it is not completed in the specified timeout? We need other task which wait for report as long as necessary, because the service will be running even when the controller returns the info page.
To solve this I created a second service which is the one who will wait until the first one ends.
@Service public class WaitUntilFinishService<T> { @Async public void waitUntilFinish (Future<T> future, AfterWaitComand<T> afterWaitCommand){ try { T result = future.get(); afterWaitCommand.docommand(result); } catch (InterruptedException | ExecutionException e) { System.out.println("The task was interrupted"); e.printStackTrace(); } } }This service receives the Future result and just waits for it. It also receives a call back service, so when the task is completed, it calls this call back service in order to complete the primary action.
This callback service implements a common interface, so you can wire any custom implementation you need. In this case, for example, the callback could retrieve the file and send it to the user by mail.
public interface AfterWaitComand<T> { void docommand(T result); }
@Service public class SendMailAfterWaitCommand implements AfterWaitComand<InvoiceReport> { @Override public void docommand(InvoiceReport result) { System.out.println("Sending invoice result by mail"); System.out.println(result); } }
Whit thease improvements, the controller will be as follows:
@Controller public class InvoiceController { @Autowired private InvoiceReportService invoiceReportSerive; @Autowired private WaitUntilFinishService<InvoiceReport> waitUntilFinishService; @Autowired private AfterWaitComand<InvoiceReport> sendMailAfterWaitCommand; @RequestMapping(value="/report", method=RequestMethod.POST) public HttpEntity<byte[]> getReport(HttpServletResponse response) throws IOException { Future<InvoiceReport> reportFutre = invoiceReportSerive.generateReport(); InvoiceReport invoiceReport = null; try { invoiceReport = reportFutre.get(1, TimeUnit.SECONDS); System.out.println("In time"); return responseNow(invoiceReport); } catch(TimeoutException e) { System.out.println("Too late"); waitUntilFinishService.waitUntilFinish(reportFutre, sendMailAfterWaitCommand); response.sendRedirect("/laterPlease"); return null; } catch(Exception e) { //Handle exception return null; } } private HttpEntity<byte[]> responseNow(InvoiceReport invoiceReport) { HttpHeaders header = new HttpHeaders(); header.setContentType(new MediaType("application", "pdf")); header.set("Content-Disposition", "attachment; filename= \"report.pdf\""); header.setContentLength(invoiceReport.getContent().length); return new HttpEntity<byte[]>(invoiceReport.getContent(), header); } }
You can download the source from my github: https://github.com/evazquezma/JEE6/tree/master/async
No hay comentarios:
Publicar un comentario