Convenient Utilities for Vert.x Future
.
🇺🇸 English | 🇨🇳 简体中文
- Install
- Usage Example
- Futurization
- Wrapping Evaluation Result
- Default Value on Empty
- Empty to Failure
- Fallback Values on Failure/Empty
- Map Non-Null Value Only
- Access
CompositeFuture
Itself on Failure - Mapping
CompositeFuture
on Failure - Keep Generic Type of the Original
Future
s ofCompositeFuture
- Mapping the Original
Future
s of aCompositeFuture
on Failure - Access
CompositeFuture
and the OriginalFuture
s on Failure - Setting Default/Fallback Values before Composition
- Java 17
- Java 11
- Java 8
- 4.2.0 - 4.2.1 (with
vertx-core
excluded) - 4.1.0 - 4.1.2 (with
vertx-core
excluded) - 4.0.3
- 4.0.0 - 4.0.2 (with
vertx-core
excluded) - 3.9.0 - 3.9.8 (with
vertx-core
excluded) - 3.8.5 (with
vertx-core
excluded)
<dependency>
<groupId>me.hltj</groupId>
<artifactId>vertx-future-utils</artifactId>
<version>1.1.2</version>
</dependency>
implementation(group = "me.hltj", name = "vertx-future-utils", version = "1.1.2")
implementation group: 'me.hltj', name: 'vertx-future-utils', version: '1.1.2'
The default dependent version of io.vertx:vertx-core
is 4.0.3
, if you want to use vertx-future-utils
with vertx-core
3.8.5
to 3.9.8
, 4.0.0
to 4.0.2
, or 4.1.0
to 4.2.1
, please exclude the default one.
<dependency>
<groupId>me.hltj</groupId>
<artifactId>vertx-future-utils</artifactId>
<version>1.1.2</version>
<exclusions>
<exclusion>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
</exclusion>
</exclusions>
</dependency>
implementation(group = "me.hltj", name = "vertx-future-utils", version = "1.1.2") {
exclude(group = "io.vertx", module = "vertx-core")
}
implementation group: 'me.hltj', name: 'vertx-future-utils', version: '1.1.2', {
exclude group: "io.vertx", module: "vertx-core"
}
Convert a callback style Vert.x call to Future
result style. e.g.:
Future<Integer> lengthFuture = FutureUtils.<HttpResponse<Buffer>>futurize(handler ->
WebClient.create(Vertx.vertx()).get(443, "hltj.me", "/").ssl(true).send(handler)
).map(response -> response.bodyAsString().length());
Vert.x provided Future
result style APIs since 4.0.0, while futurize()
can also be used for third party APIs.
Wraps an evaluation result within Future
. e.g.:
Future<Integer> futureA = wrap(() -> Integer.parseInt("1")); // Succeed with 1
Future<Integer> futureB = wrap(() -> Integer.parseInt("@")); // Failed with a NumberFormatException
Wraps a function application result within Future
. e.g.:
Future<Integer> futureA = wrap("1", Integer::parseInt); // Succeed with 1
Future<Integer> futureB = wrap("@", Integer::parseInt); // Failed with a NumberFormatException
If the evaluation result itself is a Future
, use joinWrap()
(or its alias flatWrap()
)
to flatten the nested result Future
s. e.g.:
Future<Integer> future0 = wrap("0", Integer::parseInt);
Future<Integer> future1 = wrap("1", Integer::parseInt);
Future<Integer> futureA = joinWrap(() -> future0.map(i -> 2 / i)); // Failed with a ArithmeticException
Future<Integer> futureB = joinWrap(() -> future1.map(i -> 2 / i)); // Succeed with 2
Function<String, Future<Integer>> stringToIntFuture = s -> FutureUtils.wrap(s, Integer::parseInt);
Future<Integer> futureC = joinWrap("1", stringToIntFuture); // Succeed with 1
Future<Integer> futureD = joinWrap("@", stringToIntFuture); // Failed with a NumberFormatException
If a Future
succeed with null
, map it with a default value. e.g.:
// Succeed with 1
Future<Integer> plusOneFuture = defaultWith(Future.succeededFuture(), 0).map(i -> i + 1);
the lazy version:
Future<Double> doubleFuture = FutureUtils.<Double>defaultWith(Future.succeededFuture(), () -> {
double rand = Math.random();
System.out.println("default with random value: " + rand);
return rand;
}).map(d -> d + 1);
If you want to replace the Future
(succeed with null
) with another Future
(asynchronous and/or maybe failed), you can useflatDefaultWith()
. e.g.:
Future<Integer> cachedCountFuture = getCountFutureFromCache().otherwiseEmpty();
Future<Integer> countFuture = flatDefaultWith(countFuture, () -> getCountFutureViaHttp());
If a Future
failed or succeed with a non-null value, returns the Future
itself.
Otherwise (i.e. succeed with null
), returns a Future
failed with a NullPointerException
. e.g.:
nonEmpty(future).onFailure(t -> log.error("either failed or empty, ", t);
If a Future
failed or succeed with null
, returns a Future
that succeed with a default value.
Otherwise (i.e. succeed with a non-null value), returns the Future
itself. e.g.:
Future<Integer> plusOneFuture = fallbackWith(intFuture, 0).map(i -> i + 1);
the lazy version:
Future<Double> plusOneFuture = FutureUtils.<Double>fallbackWith(doubleFuture, throwableOpt ->
throwableOpt.map(t -> {
log.warn("fallback error with -0.5, the error is: ", t);
return -0.5;
}).orElseGet(() -> {
log.warn("fallback empty with 0.5");
return 0.5;
})
).map(d -> d + 1);
or with separated lambdas for failure & empty:
Future<Double> plusOneFuture = fallbackWith(doubleFuture, error -> {
System.out.println("fallback error with -0.5, the error is " + error);
return -0.5;
}, () -> {
System.out.println("fallback empty with 0.5");
return 0.5;
}).map(d -> d + 1);
If you want to replace the Future
(failed or succeed with null
) with another Future
(asynchronous and/or maybe failed), you can useflatFallbackWith()
. e.g.:
Future<Integer> cachedCountFuture = getCountFutureFromCache();
Future<Integer> countFuture1 = flatFallbackWith(countFuture, _throwableOpt -> getCountFutureViaHttp());
Future<Integer> countFuture2 = flatFallbackWith(
countFuture, throwable -> getCountFutureViaHttpOnFailure(throwable), () -> getCountFutureViaHttpOnEmpty()
);
Maps a Future
only when it succeeds with a non-null value. If the parameter Future
succeeds with null
,
mapSome()
also returns a Future
succeed with null
. e.g.:
Future<List<Integer>> intsFuture = getIntegers();
Future<List<String>> hexStringsFuture = mapSome(intsFuture, ints ->
ints.stream().map(i -> Integer.toString(i, 16)).collect(Collectors.toUnmodifiableList())
);
If the mapper itself returns a Future
, you can use flatMapSome()
to flatten the nested Future
s. e.g.:
Future<String> userIdFuture = getUserIdFuture();
Future<User> userFuture = flatMapSome(userIdFuture, id -> getUserFuture(id));
When a CompositeFuture
failed, we cannot access
the CompositeFuture
itself directly inside the lambda argument of
onComplete()
or onFailure
.
A workaround is introducing a local variable. e.g:
CompositeFuture composite = CompositeFuture.join(
Future.succeededFuture(1), Future.<Double>failedFuture("fail"), Future.succeededFuture("hello")
);
composite.onFailure(t -> {
System.out.println("The CompositeFuture failed, where these base Futures succeed:");
for (int i = 0; i < composite.size(); i++) {
if (composite.succeeded(i)) {
System.out.println(("Future-" + i));
}
}
});
But this is not fluent and cause an extra variable introduced, especially we repeat to do this again and again.
In this case, we can use CompositeFutureWrapper#use()
instead. e.g.:
CompositeFutureWrapper.of(CompositeFuture.join(
Future.succeededFuture(1), Future.<Double>failedFuture("fail"), Future.succeededFuture("hello")
)).use(composite -> composite.onFailure(t -> {
System.out.println("The CompositeFuture failed, where these base Futures succeed:");
for (int i = 0; i < composite.size(); i++) {
if (composite.succeeded(i)) {
System.out.println(("Future-" + i));
}
}
}));
While it's not recommended using CompositeFutureWrapper
directly, please use more powerful subclasses
CompositeFutureTuple[2-9]
instead.
When a CompositeFuture
failed, the lambda passed to its map()
/flatMap()
method won't be invoked.
If you still want to map the partial succeed results, you can use CompositeFutureWrapper#through()
(or its alias
mapAnyway()
). e.g.:
Future<Double> sumFuture = CompositeFutureWrapper.of(
CompositeFuture.join(Future.succeededFuture(1.0), Future.<Integer>failedFuture("error"))
).through(composite -> (composite.succeeded(0) ? composite.<Double>resultAt(0) : 0.0) +
(composite.succeeded(1) ? composite.<Integer>resultAt(1) : 0)
);
If the mapper itself returns a Future
, we can use CompositeFutureWrapper#joinThrough()
(or its alias
flatMapAnyway()
) to flatten the nested result Future
s. e.g.:
Future<Double> sumFuture = CompositeFutureWrapper.of(
CompositeFuture.join(Future.succeededFuture(1.0), Future.<Integer>failedFuture("error"))
).joinThrough(composite -> wrap(() -> composite.<Double>resultAt(0) + composite.<Integer>resultAt(1)));
While it's not recommended using CompositeFutureWrapper
directly, please use more powerful subclasses
CompositeFutureTuple[2-9]
instead.
In a CompositeFuture
, all the original Future
s are type erased.
We have to specify type parameters for the results frequently. e.g.:
Future<Integer> future0 = Future.succeededFuture(2);
Future<Double> future1 = Future.succeededFuture(3.5);
Future<Double> productFuture = CompositeFuture.all(future0, future1).map(
composite -> composite.<Double>resultAt(0) * composite.<Integer>resultAt(1)
);
The result productFuture
is 'succeed with 7.0'? Unfortunately, NO. It is 'failed with a ClassCastException',
because the type parameters are misspecified. They are (Integer, Double)
, not (Double, Integer)
!
We can use CompositeFutureTuple2#mapTyped()
(or its alias applift()
) to avoid this error-prone case. e.g.:
Future<Integer> future0 = Future.succeededFuture(2);
Future<Double> future1 = Future.succeededFuture(3.5);
Future<Double> productFuture = FutureUtils.all(future0, future1).mapTyped((i, d) -> i * d);
We needn't specify the type parameters manually inside the lambda argument of mapTyped()
anymore,
because the CompositeFutureTuple2
has already kept them.
Moreover, the code is significantly simplified with the boilerplate code reduced.
If the lambda result itself is a Future
, we can use CompositeFutureTuple2#flatMapTyped()
(or its alias
joinApplift()
) to flatten the nested result Future
s. e.g:
Future<Integer> future0 = Future.succeededFuture(2);
Future<Double> future1 = Future.failedFuture("error");
Future<Double> productFuture = FutureUtils.all(future0, future1).flatMapTyped((i, d) -> wrap(() -> i * d));
There are also any()
and join()
factory methods, and CompositeFutureTuple3
to CompositeFutureTuple9
for 3-9 arities.
In CompositeFutureTuple[2-9]
, there are additional overload through()
& joinThrough()
(and their alias
mapAnyway
& flatMapAnyway
) methods, they provide the original Future
s as parameters to invoke the lambda argument.
e.g. :
Future<Double> sumFuture = FutureUtils.join(
Future.succeededFuture(1.0), Future.<Integer>failedFuture("error")
).through((fut0, fut1) -> fallbackWith(fut0, 0.0).result() + fallbackWith(fut1, 0).result());
In CompositeFutureTuple[2-9]
, there is an additional overload use()
method, it provides the CompositeFuture
itself
as well as the original Future
s as parameters to invoke the lambda argument. e.g.:
Future<Double> future0 = Future.succeededFuture(1.0);
Future<Integer> future1 = Future.failedFuture("error");
FutureUtils.join(future0, future1).use((composite, fut0, fut1) -> composite.onComplete(_ar -> {
String status = composite.succeeded() ? "succeeded" : "failed";
System.out.println(String.format("composite future %s, original futures: (%s, %s)", status, fut0, fut1))
}));
Moreover, there is a new method with()
that likes use()
but return a value. e.g.:
Future<Double> future0 = Future.succeededFuture(1.0);
Future<Integer> future1 = Future.failedFuture("error");
Future<String> stringFuture = join(future0, future1).with((composite, fut0, fut1) -> composite.compose(
_x -> Future.succeededFuture(String.format("succeeded: (%s, %s)", fut0, fut1)),
_t -> Future.succeededFuture(String.format("failed: (%s, %s)", fut0, fut1))
));
We can set default values for each original Future
s before composition to avoid null
check. e.g.:
Future<Integer> future0 = defaultWith(futureA, 1);
Future<Integer> future1 = defaultWith(futureB, 1);
Future<Double> future2 = defaultWith(futureC, 1.0);
Future<Double> productFuture = FutureUtils.all(future0, future1, future2)
.mapTyped((i1, i2, d) -> i1 * i2 * d);
In fact, it's unnecessary to introduce so many temporary variables at all, we can use FutureTuple3#defaults()
to simplify it. e.g.:
Future<Double> productFuture = tuple(futureA, futureB, futureC)
.defaults(1, 1, 1.0)
.join()
.mapTyped((i1, i2, d) -> i1 * i2 * d);
the factory method tuple()
creates a FutureTuple3
object, and then invoke its defaults()
method to set default values, then invoke its join()
method to get a CompositeFutureTuple3
object.
Another useful method of FutureTuple[2-9]
is fallback()
, just likes defaults()
,
we can use it to set the fallback values at once. e.g.:
Future<Double> productFuture = tuple(futureA, futureB, futureC)
.fallback(1, 1, 1.0)
.all()
.mapTyped((i1, i2, d) -> i1 * i2 * d);
There are other similar methods in FutureTuple[2-9]
: mapEmpty()
, otherwise()
, otherwiseEmpty()
and overload methods for otherwise
, defaults()
, fallback()
with effect,
see the Java doc
. e.g.:
Future<String> productFutureA = tuple(futureA, futureB, futureC)
.otherwiseEmpty()
.any()
.mapTyped((i1, i2, d) -> String.format("results: (%d, %d, %f)", i1, i2, d));
Future<Double> productFutureB = tuple(futureA, futureB, futureC).fallback(
t -> log.error("fallback on failure", t),
() -> log.warn("fallback on empty"),
1, 1, 1.0
).all().mapTyped((i1, i2, d) -> i1 * i2 * d);