Callbacks Flow
When using a Flow user interface, the simplest way of allowing background jobs to interact with it is through callbacks. You can use Consumer, Runnable, and Supplier as callback interfaces, depending on how you want to interact with the background job.
| Event | Callback |
|---|---|
Completed without a result. |
|
Completed with a result of type |
|
Completed with an exception. |
|
Reported percentage done. |
|
Cancelled by user. |
|
The examples on this page are methods of an application service that acts as the trigger of the job. Like all application service methods, they are protected with method security. Because of this, they hand the job to an injected TaskExecutor instead of using the @Async annotation. See User Triggered Jobs for why, and for how to inject the TaskExecutor.
Returning a Result
A background job that returns a string or an exception could be implemented like this:
Source code
Java
@PreAuthorize("hasAuthority('permission:startjob')")
public void startBackgroundJob(Consumer<String> onComplete,
Consumer<Exception> onError) {
taskExecutor.execute(() -> {
try {
var result = doSomethingThatTakesALongTime();
onComplete.accept(result);
} catch (Exception ex) {
onError.accept(ex);
}
});
}Reporting Progress
When the background job is also reporting its progress, for instance as a percentage number, it could look like this:
Source code
Java
@PreAuthorize("hasAuthority('permission:startjob')")
public void startBackgroundJob(Consumer<String> onComplete,
Consumer<Double> onProgress,
Consumer<Exception> onError) {
taskExecutor.execute(() -> {
try {
onProgress.accept(0.0);
var step1Result = performStep1();
onProgress.accept(0.25);
var step2Result = performStep2(step1Result);
onProgress.accept(0.50);
var step3Result = performStep3(step2Result);
onProgress.accept(0.75);
var result = performStep4(step3Result);
onProgress.accept(1.0);
onComplete.accept(result);
} catch (Exception ex) {
onError.accept(ex);
}
});
}Cancelling
A job can be cancelled. To do that, it would look like this:
Source code
Java
@PreAuthorize("hasAuthority('permission:startjob')")
public void startBackgroundJob(Consumer<String> onComplete,
Consumer<Double> onProgress,
Consumer<Exception> onError,
Supplier<Boolean> isCancelled) {
taskExecutor.execute(() -> {
try {
onProgress.accept(0.0);
if (isCancelled.get()) {
return;
}
var step1Result = performStep1();
onProgress.accept(0.25);
if (isCancelled.get()) {
return;
}
var step2Result = performStep2(step1Result);
onProgress.accept(0.50);
if (isCancelled.get()) {
return;
}
var step3Result = performStep3(step2Result);
onProgress.accept(0.75);
if (isCancelled.get()) {
return;
}
var result = performStep4(step3Result);
onProgress.accept(1.0);
onComplete.accept(result);
} catch (Exception ex) {
onError.accept(ex);
}
});
}All callbacks have to be thread-safe since they’re called from the background thread, but owned and created by the user interface. For more information about how to implement these callbacks, see the Implementing Callbacks documentation page.
Improving Cancel API
To make the cancelling API nicer, you can replace the callback with a handle. First, create a handle interface that the user interface can use to cancel the job:
Source code
Java
@FunctionalInterface
public interface CancellableJob {
void cancel();
}Next, implement the service method like this:
Source code
Java
@PreAuthorize("hasAuthority('permission:startjob')")
public CancellableJob startBackgroundJob(Consumer<String> onComplete,
Consumer<Double> onProgress,
Consumer<Exception> onError) {
var cancelled = new AtomicBoolean(false);
taskExecutor.execute(() -> {
try {
onProgress.accept(0.0);
if (cancelled.get()) {
return;
}
var step1Result = performStep1();
onProgress.accept(0.25);
if (cancelled.get()) {
return;
}
var step2Result = performStep2(step1Result);
onProgress.accept(0.50);
if (cancelled.get()) {
return;
}
var step3Result = performStep3(step2Result);
onProgress.accept(0.75);
if (cancelled.get()) {
return;
}
var result = performStep4(step3Result);
onProgress.accept(1.0);
onComplete.accept(result);
} catch (Exception ex) {
onError.accept(ex);
}
});
return () -> cancelled.set(true);
}The user interface would have to store the handle while the job is running, and call the cancel() method to cancel it.
The handle itself is thread safe because you’re using an AtomicBoolean. You don’t need to take any special precautions to call it from the user interface.