Using Deferred Objects and Promises with Java 8 and JDeferred
September 27th, 2015 by Micha KopsPromises may help us when dealing with asynchronous code and we need to merge, pipe or track the progress and the results of single parts of computation in our applications.
In the following tutorial I’d like to demonstrate a small library, JDeferred that helps us for this specific use case.
Contents
Dependencies
Using Maven here, we simply need to add one dependency for jdeferred-core to our pom.xml:
<dependency> <groupId>org.jdeferred</groupId> <artifactId>jdeferred-core</artifactId> <version>1.2.4</version> </dependency>
What is a Deferred-Object and what a Promise?
A Promise is used for deferred and asynchronous computations and, similar to Java’s native Future object, represents the result of an operation that has not been completed yet.
Pending, Rejected and Fulfilled are the three valid states for a Promise and for each state, event handlers may be attached and Promises may be chained.
More information about this concept may be found on the Mozilla website.
In the terminology of this library, Promises are immutable versions of DeferredObjects. As the DeferredObject grants access to the resolve and reject methods, most library code should return a Promise so that users do not mess around with the internals here.
Example Classes
To demonstrate the usage of JDeferred here I have written two sample classes that hold an inner instance of Deferred and report changes of their state (progress, fail, completion ..) to this instance.
In addition they provide a getter to access the specific promise. The promise itself is immutable, the deferred is not, so we should be careful not to pass the deferred object around.
The design of the following examples classes is quite bad (mutable state, state handling, thread-safety etc..) but should be sufficient for this tutorial to demonstrate JDeferred :)
Multiplier
Our first class simply multiplies a given number a designated number of times.
Each round when the number is multiplied again, the state of the deferred object is updated with information about the progress (completion in %).
package com.hascode.tutorial; import org.jdeferred.Deferred; import org.jdeferred.Promise; import org.jdeferred.impl.DeferredObject; public class Multiplier { private final long factor; private final Deferred<Long, Long, String> deferred = new DeferredObject<>(); public Multiplier(long factor) { this.factor = factor; } public long multiplyNTimes(int rounds) { long result = 1; for (int i = 1; i <= rounds; i++) { deferred.notify("status: " + (i * 100 / rounds) + "%"); result *= factor; } deferred.resolve(result); return result; } public Promise<Long, Long, String> promise() { return deferred.promise(); } }
WebCrawler
The other ugly class is a stateful simulated web-crawler that fetches a list of key-words from a given website (by given URL string).
package com.hascode.tutorial; import java.util.ArrayList; import java.util.List; import org.jdeferred.Deferred; import org.jdeferred.Promise; import org.jdeferred.impl.DeferredObject; public class WebCrawler { // ugly, stateful, demonstration only ;) private final Deferred<List<String>, String, Integer> deferred = new DeferredObject<>(); private List<String> keywordsFound = new ArrayList<>(); public void crawl(String url) { initializeCrawler(); // fake deferred.notify(10); fetchSiteContent(url);// fake deferred.notify(20); filterDuplicateKeywords();// fake deferred.notify(80); storeKeywords();// fake deferred.resolve(keywordsFound); } public Promise<List<String>, String, Integer> promise() { return deferred.promise(); } private void storeKeywords() { System.out.println("storing keywords"); keywordsFound.add("foo"); keywordsFound.add("bar"); keywordsFound.add("baz"); } private void filterDuplicateKeywords() { System.out.println("filtering duplicate keywords"); } private void fetchSiteContent(String url) { System.out.println("fetching site content for url: " + url); } private void initializeCrawler() { System.out.println("initializing crawler"); } }
Now we’re ready to play around with promises..
Simple Promises
In the first example, we’re binding simple promises to both demonstration classes.
On progress we’re printing the progress information to STDOUT, finally we’re displaying the computation result of the multiplication (in the first example) and display information about keywords found (in the second example).
Java 8′s lambda expressions spare us using anonymous classes here (as long as we don’t get any type ambiguity ;):
package com.hascode.tutorial; public class SimplePromiseExample { public static void main(String[] args) { System.out.println("## Example 1"); Multiplier m = new Multiplier(5); m.promise().progress(System.out::println).done((res) -> System.out.println("the result is: " + res)); m.multiplyNTimes(4); System.out.println("## Example 2"); WebCrawler crawler = new WebCrawler(); crawler.promise().progress((i) -> System.out.println("Progress: " + i + "%")).done((keywords) -> { System.out.println("Done, " + keywords.size() + " keywords found: "); keywords.forEach(System.out::println); }); crawler.crawl("https://www.hascode.com/"); } }
Running the code above in our IDE of choice or using Maven in the console should produce a similar output:
$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.SimplePromiseExample [..] status: 25% status: 50% status: 75% status: 100% the result is: 625 ## Example 2 initializing crawler Progress: 10% fetching site content for url: https://www.hascode.com/ Progress: 20% filtering duplicate keywords Progress: 80% storing keywords Done, 3 keywords found: foo bar baz
Promise Filtering
We may add a filter to a promise to manipulate the result of its computation.
In the following example, we’re sorting the keywords found alphabetically using a DoneFilter.
package com.hascode.tutorial; import java.util.Collections; import java.util.List; import org.jdeferred.Promise; public class PromiseFilterExample { public static void main(String[] args) { WebCrawler crawler = new WebCrawler(); Promise<List<String>, String, Integer> promise = crawler.promise() .progress((i) -> System.out.println("Progress: " + i + "%")).done((keywords) -> { System.out.println("Done, " + keywords.size() + " keywords found: "); keywords.forEach(System.out::println); }); Promise<List<String>, String, Integer> filteredPromise = promise.then((keywords) -> Collections.sort(keywords)); filteredPromise.done((keywords) -> { System.out.println("Done (sorted by filter), " + keywords.size() + " keywords found: "); keywords.forEach(System.out::println); }); crawler.crawl("https://www.hascode.com/"); } }
Running the code above in our IDE of choice or using Maven in the console should produce a similar output:
$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.PromiseFilterExample [..] initializing crawler Progress: 10% fetching site content for url: https://www.hascode.com/ Progress: 20% filtering duplicate keywords Progress: 80% storing keywords Done, 3 keywords found: foo bar baz Done (sorted by filter), 3 keywords found: bar baz foo
Pipes
Pipes allow us to react to the outcome of a promises computation.
In the following example, we’re using a DonePipe to handle different results of our multiplication class.
- If the result is less than 50, we’re rejecting the result and return an exception
- If the result is less than 130, we’re modifying the result by multiplying it with 2
- Else we’re returning the original result
package com.hascode.tutorial; import java.util.stream.IntStream; import org.jdeferred.DonePipe; import org.jdeferred.Promise; import org.jdeferred.impl.DeferredObject; public class PipeExample { public static void main(String[] args) { IntStream.of(4, 1, 3).forEach(i -> { Multiplier m = new Multiplier(5); Promise<Long, Long, String> promise = m.promise().progress(System.out::println) .done((res) -> System.out.println("the result is: " + res)); promise.then(new DonePipe<Long, Long, Exception, String>() { @Override public Promise<Long, Exception, String> pipeDone(Long result) { if (result < 50) { System.out.println("result < 50, that's too low -> error"); return new DeferredObject<Long, Exception, String>().reject(new Exception("result too low")); } if (result < 130) { System.out.println("result < 130, doubling the value.."); return new DeferredObject<Long, Exception, String>().resolve(result * 2); } return new DeferredObject<Long, Exception, String>().resolve(result); } }).fail((exception) -> System.err.println("error: " + exception.getMessage())) .done((result) -> System.out.println("pipe result: " + result)); m.multiplyNTimes(i); }); } }
Running the code above in our IDE of choice or using Maven in the console should produce a similar output:
$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.PipeExample [..] status: 25% status: 50% status: 75% status: 100% the result is: 625 pipe result: 625 status: 100% the result is: 5 result < 50, that's too low -> error error: result too low status: 33% status: 66% status: 100% the result is: 125 result < 130, doubling the value.. pipe result: 250
Deferred Manager
The deferred manager is useful when we need to deal with asynchronous tasks using Callables or Runnables or as demonstrated below when we need to track the progress and the result of the computation of multiple Promise objects.
In the example below, we’re using a DeferredManager to observe three instances of our WebCrawler to display their progress and when everything is done, to display the results of each single promise:
package com.hascode.tutorial; import java.util.List; import org.jdeferred.DeferredManager; import org.jdeferred.Promise; import org.jdeferred.impl.DefaultDeferredManager; public class DeferredManagerExample { public static void main(String[] args) { WebCrawler crawl1 = new WebCrawler(); WebCrawler crawl2 = new WebCrawler(); WebCrawler crawl3 = new WebCrawler(); Promise<List<String>, String, Integer> promise1 = crawl1.promise(); Promise<List<String>, String, Integer> promise2 = crawl2.promise(); Promise<List<String>, String, Integer> promise3 = crawl3.promise(); DeferredManager dm = new DefaultDeferredManager(); dm.when(promise1, promise2, promise3).progress((p) -> { System.out.println( "progress-stats done: " + p.getDone() + " failed: " + p.getFail() + " total: " + p.getTotal()); }).done((r) -> { System.out.println("All promises completed: "); r.forEach((o) -> { System.out.println("result: " + o.getResult()); }); }); crawl1.crawl("https://www.hascode.com/"); crawl2.crawl("http://www.java.net/"); crawl3.crawl("http://www.micha-kops.com/"); } }
Running the code above in our IDE of choice or using Maven in the console should produce a similar output:
$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.DeferredManagerExample [..] initializing crawler progress-stats done: 0 failed: 0 total: 3 fetching site content for url: https://www.hascode.com/ progress-stats done: 0 failed: 0 total: 3 filtering duplicate keywords progress-stats done: 0 failed: 0 total: 3 storing keywords progress-stats done: 1 failed: 0 total: 3 initializing crawler progress-stats done: 1 failed: 0 total: 3 fetching site content for url: http://www.java.net/ progress-stats done: 1 failed: 0 total: 3 filtering duplicate keywords progress-stats done: 1 failed: 0 total: 3 storing keywords progress-stats done: 2 failed: 0 total: 3 initializing crawler progress-stats done: 2 failed: 0 total: 3 fetching site content for url: http://www.micha-kops.com/ progress-stats done: 2 failed: 0 total: 3 filtering duplicate keywords progress-stats done: 2 failed: 0 total: 3 storing keywords progress-stats done: 3 failed: 0 total: 3 All promises completed: result: [foo, bar, baz] result: [foo, bar, baz] result: [foo, bar, baz]
So as we can see, it’s nice to track the progress of our Promise objects here and to aggregate their result.
Deferred Manager with Executor Service
We may combine the DeferredManager with an ExecutorService for further thread control:
package com.hascode.tutorial; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.jdeferred.DeferredManager; import org.jdeferred.Promise; import org.jdeferred.impl.DefaultDeferredManager; public class DeferredManagerWithExecutorServiceExample { public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); DeferredManager dm = new DefaultDeferredManager(executorService); WebCrawler crawl1 = new WebCrawler(); // [..] } }
The result is identical to the last example above.
Tutorial Sources
Please feel free to download the tutorial sources from my Bitbucket repository, fork it there or clone it using Git:
git clone https://bitbucket.org/hascode/jdeferred-tutorial.git
Resources
Tags: asynchronous, deferred, deferredobject, functional, javascript, jdeferred, js, promise, promises
May 27th, 2017 at 8:12 am
Thanks…
May 29th, 2017 at 10:15 am
You’re welcome! :)
June 7th, 2017 at 9:26 pm
Line 17 of PromiseFilterExample, the “then” invocation is ambiguous for the version of jderred jar library being used.
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-co mpile) on project promise: Compilation failure: Compilation failure:
[ERROR] /C:/Users/203293/git/jdeferred-promise/src/main/java/com/cubic/cts/bo/PromiseFilterExample.ja va:[21,73] reference to then is ambiguous
[ERROR] both method then(org.jdeferred.DoneFilter) in org.jdeferred.Pro mise and method then(org.jdeferred.DonePipe) in org.jdeferred .Promise match
[ERROR] /C:/Users/sean/git/jdeferred-promise/src/main/java/com/sean/PromiseFilterExample.ja va:[21,78] incompatible types: inference variable D_OUT has incompatible bounds
[ERROR] equality constraints: java.util.List
[ERROR] lower bounds: void
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following artic les:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
java version “1.8.0_131″
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
Apache Maven 3.5.0 (ff8f5e7444045639af65f6095c62210b5713f426; 2017-04-03T12:39:06-07:00)
Maven home: C:\bin\maven-3.5.0
Java version: 1.8.0_131, vendor: Oracle Corporation