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
CompositeFutureItself on Failure - Mapping
CompositeFutureon Failure - Keep Generic Type of the Original
Futures ofCompositeFuture - Mapping the Original
Futures of aCompositeFutureon Failure - Access
CompositeFutureand the OriginalFutures on Failure - Setting Default/Fallback Values before Composition
- Java 17
- Java 11
- Java 8
- 4.2.0 - 4.2.1 (with
vertx-coreexcluded) - 4.1.0 - 4.1.2 (with
vertx-coreexcluded) - 4.0.3
- 4.0.0 - 4.0.2 (with
vertx-coreexcluded) - 3.9.0 - 3.9.8 (with
vertx-coreexcluded) - 3.8.5 (with
vertx-coreexcluded)
<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.
Details
<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 NumberFormatExceptionWraps 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 NumberFormatExceptionIf the evaluation result itself is a Future, use joinWrap()(or its alias flatWrap())
to flatten the nested result Futures. 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 NumberFormatExceptionIf 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 Futures. 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 Futures. 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 Futures 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 Futures. 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 Futures 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 Futures 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 Futures 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);