Futur4J is a powerful and intuitive open-source Java library that simplifies asynchronous task scheduling, inspired by the concept of JavaScript promises.
This documentation is outdated. Please don't read it.
The Futur4J project is composed of multiple modules. It is required to include the futur-api
module, and the other modules depend on it at runtime, however the others are optional and dependent on your use case.
repositories {
maven {
url 'https://repo.tommyjs.dev/repository/maven-releases/'
}
}
dependencies {
compile 'dev.tommyjs:futur-api:2.1.3'
compile 'dev.tommyjs:futur-reactor:2.1.3'
}
repositories {
maven("https://repo.tommyjs.dev/repository/maven-releases/")
}
dependencies {
implementation("dev.tommyjs:futur-api:2.1.3")
implementation("dev.tommyjs:futur-reactor:2.1.3")
}
<repositories>
<repository>
<id>tommyjs-repo</id>
<url>https://repo.tommyjs.dev/repository/maven-releases/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>dev.tommyjs</groupId>
<artifactId>futur-api</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>dev.tommyjs</groupId>
<artifactId>futur-reactor</artifactId>
<version>2.1.3</version>
</dependency>
</dependencies>
Futur4J uses an underlying Scheduler
instance to power both synchronous and asynchronous task execution.
The library was originally built for use in Minecraft servers, and therefore has native support for a "main" thread with can be seamlessly switched to in a Promise
chain (read more below).
It is recommended, but not required, to manually create a Scheduler
. If this is not done, a single-threaded default Scheduler
will be used.
// check if scheduler has already been loaded
if (!Schedulers.isLoaded()) {
Scheduler scheduler = ThreadPoolScheduler.create(6); // create a scheduler using an underlying thread pool (6 threads)
Schedulers.setScheduler(scheduler); // initialize the scheduling
}
The futur-standalone
module has two scheduler implementations available. The ExclusiveThreadPoolScheduler
operates one thread pool, and throws an exception when sync tasks are attempted to be executed.
The ThreadPoolScheduler
uses a single-threaded "main" pool and a multi-threaded async pool.
For Minecraft, Bukkit, Velocity and BungeeCord support is coming soon. Feel free to make PRs for other external library support as modules.
You can invoke tasks using the synchronous and asynchronous scheduler Scheduler
methods. It is worth noting that exceptions in these tasks are swallowed silently, and it is therefore better to
use promise chains, or use the Schedulers
utility class, which will wrap tasks in a try-catch and log to the standard error.
scheduler.runDelayedSync(() -> {
System.out.println("I was scheduled to execute 10 seconds later!");
}, 10L, TimeUnit.SECONDS);
Schedulers.runAsync(() -> {
throw new RuntimeException("I will be caught by the exception wrapper in the Schedulers class!");
});
You may have recieved some form of "future" instance (a task that will complete at an unspecified time in the future). This may be a future4j-native Promise
instance, or another form of future. In the case of the latter, you may obtain a Promise
instance through the various available wrappers (e.g. ReactorTransformer
for use with reactor core).
You can then add listeners to the Promise
, which will be executed asynchronously on promise "completion". A promise is deemed completed once the task has either concluded successfully or an exception has been thrown. You can access this information in a PromiseCompletion
,
which is passed to promise listeners. Promise listeners should not throw exceptions, and will print a stack trace if they do to avoid silent failure. However, callbacks in promise chain methods are permitted to throw exceptions as they will be passed down the chain.
Promise<String> promise = doSomeAsyncProcessing();
promise.addListener(ctx -> {
if (ctx.isError()) {
System.out.println("Error! Oh no!");
} else {
String result = ctx.getResult();
System.out.println(result);
}
});
Promise
also contains convenience wrapper methods to avoid checking the completion for exceptions every time. These methods also return a new promise which will resolve once the original promise, and then handler callback have finished execution. We can use this feature to create
"promise chains", which are a sequence of promise handlers. These handlers are permitted to throw exceptions, which will be passed down promise chains until they are handled. If exceptions are not handled properly, they will be silently swallowed. It is recommend to append logExceptions()
to chains unless exceptions are handled explicitly with a custom listener. This will simply log exceptions in the promise chain to the standard error.
You can also use the sync and async methods to choose which executor is chosen for task execution. The ability to seamlessly switch between executors comes in handy with applications where you must be on the "main thread" to perform non-threadsafe operations.
Promise.start()
can be used to create a "resolved" promise. This is useful when starting a promise chain or returning an already-completed promise (common in compose callbacks, mentioned below).
Promise.start().thenSupplyAsync(() -> {
// do some async processing
return "Hello World!";
}).thenApplyAsync(input -> {
// convert string to upper case
return input.toUpperCase();
}).thenConsumeSync(result -> {
// display result
System.out.println("Result: " + result);
}).logExceptions(); // log exceptions to the standard error
The promise chain methods follow Java convention of Supplier
, Consumer
and Runnable
through the thenSupply
, thenConsume
and thenRun
methods. There is also thenApply
(which acts as a Java Function
), and thenCompose
, which is explained below. Futur4J has its own implementations of the Java concepts though to allow for
exceptions within handlers.
The thenCompose
method is similar to the concept in the Java CompletableFuture
API. You are able to return another Promise
within a promise handler, and a Promise<Promise<T>>
will be wrapped up into just a Promise<T>
. This is often useful when using an external library that returns some sort of "future" inside a handler.
String username = "abc123";
mySuperSecretApi.usernameToId(username) // returns Promise<UUID>
.thenComposeAsync(id -> {
return userManager.fetchFromDatabase(id); // returns Promise<User>
}).thenConsumeSync(playerData -> {
System.out.println(username + " has " + playerData.getCoins() + " coins!");
}).logExceptions();
The Promises
utility class provides many helpful methods for working with promises and groups of promises.
Promises.combine(Promise<K>, Promise<V>)
combines two promises into one Promise<Entry<K, V>>
.
Promises.erase(Promise)
erases the type on a Promise
instance and returns a Promise<Void>
. This is also supported for lists of promises with Promises.all(List<Promise>)
, which will return a Promise<Void>
representing the future whereby all promises have completed.
If all promises are of identical type, you can use Promises.combine(List<Promise<T>>)
which will return one Promise<List<T>>
, similarly representing the future whereby all promises have completed.
This can also be applied to key-value scenarios in the same way, where you can provide a mapper function to be applied to all elements and combine into a single promise.
External libaries provide asynchronous ways to interact with their output in all shapes and sizes. Futur4J currently has wrappers for the following libraries/frameworks:
- Reactor Core (via futur-reactor)
- Java CompletableFuture (via
Promises.wrap
in futur-api)