├── .editorconfig
├── .gitignore
├── README.md
├── img
└── thereactivepublisher.png
├── pom.xml
└── src
└── main
├── java
└── com
│ └── thepracticaldeveloper
│ ├── MagazineSubscriber.java
│ ├── ReactiveFlowApp.java
│ └── logutils
│ └── ColorConsoleAppender.java
└── resources
└── log4j.properties
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset=utf-8
3 | end_of_line=lf
4 | insert_final_newline=true
5 | indent_style=space
6 | indent_size=4
7 |
8 | [*.java]
9 | indent_style=space
10 | indent_size=2
11 |
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### STS ###
2 | .apt_generated
3 | .classpath
4 | .factorypath
5 | .project
6 | .settings
7 | .springBeans
8 |
9 | ### IntelliJ IDEA ###
10 | .idea
11 | *.iws
12 | *.iml
13 | *.ipr
14 |
15 | ### NetBeans ###
16 | nbproject/private/
17 | build/
18 | nbbuild/
19 | dist/
20 | nbdist/
21 | .nb-gradle/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Java 9 Flow Example - TPD.io
2 |
3 | 
4 |
5 | ## Description
6 |
7 | This is a simple project that shows how to use Java 9's Flow API to create a Reactive Programming example. It's based on a story: a Magazine Publisher with two different Subscribers, one of them being too slow to pick items.
8 |
9 | The guide [Reactive Programming with Java 9 Flow](https://thepracticaldeveloper.com/2018/01/31/reactive-programming-java-9-flow/) explains this code and the fictional use case with detail.
10 |
11 | ## Code Structure
12 |
13 | * `ReactiveFlowApp` creates a Publisher using Java 9's `SubmissionPublisher`. It implements backpressure and dropping of items by setting a timeout for subscribers.
14 | * `MagazineSubscriber` implements `Flow.Subscriber` and let you experiment what happens when subscribers are slow.
15 | * `ColorConsoleAppender` has nothing to do with Java 9 or reactive programming, is just a utility class to set colors to log messages so the result can be understood easier.
16 |
17 | ## Feedback and Contribution
18 |
19 | If you find something wrong, feel free to create an issue. Same if you have questions. If you want to help me creating more code examples you can [buy me a coffee](https://www.buymeacoffee.com/ZyLJNUR).
20 |
--------------------------------------------------------------------------------
/img/thereactivepublisher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mechero/java-9-flow-reactive/d54499099f7ddb1b119dc57f41e9ec75d30bbf53/img/thereactivepublisher.png
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
5 | 4.0.0
6 | com.thepracticaldeveloper
7 | tpd-flow-example
8 | jar
9 | 1.0-SNAPSHOT
10 |
11 |
12 |
13 | org.apache.maven.plugins
14 | maven-compiler-plugin
15 |
16 | 9
17 | 9
18 |
19 |
20 |
21 |
22 | tpd-flow-example
23 | https://thepracticaldeveloper.com
24 |
25 |
26 | org.slf4j
27 | slf4j-log4j12
28 | 1.7.25
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/main/java/com/thepracticaldeveloper/MagazineSubscriber.java:
--------------------------------------------------------------------------------
1 | package com.thepracticaldeveloper;
2 |
3 | import java.util.concurrent.Flow;
4 | import java.util.stream.IntStream;
5 |
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 |
9 | public class MagazineSubscriber implements Flow.Subscriber {
10 |
11 | public static final String JACK = "Jack";
12 | public static final String PETE = "Pete";
13 |
14 | private static final Logger log = LoggerFactory.
15 | getLogger(MagazineSubscriber.class);
16 |
17 | private final long sleepTime;
18 | private final String subscriberName;
19 | private Flow.Subscription subscription;
20 | private int nextMagazineExpected;
21 | private int totalRead;
22 |
23 | MagazineSubscriber(final long sleepTime, final String subscriberName) {
24 | this.sleepTime = sleepTime;
25 | this.subscriberName = subscriberName;
26 | this.nextMagazineExpected = 1;
27 | this.totalRead = 0;
28 | }
29 |
30 | @Override
31 | public void onSubscribe(final Flow.Subscription subscription) {
32 | this.subscription = subscription;
33 | subscription.request(1);
34 | }
35 |
36 | @Override
37 | public void onNext(final Integer magazineNumber) {
38 | if (magazineNumber != nextMagazineExpected) {
39 | IntStream.range(nextMagazineExpected, magazineNumber).forEach(
40 | (msgNumber) ->
41 | log("Oh no! I missed the magazine " + msgNumber)
42 | );
43 | // Catch up with the number to keep tracking missing ones
44 | nextMagazineExpected = magazineNumber;
45 | }
46 | log("Great! I got a new magazine: " + magazineNumber);
47 | takeSomeRest();
48 | nextMagazineExpected++;
49 | totalRead++;
50 |
51 | log("I'll get another magazine now, next one should be: " +
52 | nextMagazineExpected);
53 | subscription.request(1);
54 | }
55 |
56 | @Override
57 | public void onError(final Throwable throwable) {
58 | log("Oops I got an error from the Publisher: " + throwable.getMessage());
59 | }
60 |
61 | @Override
62 | public void onComplete() {
63 | log("Finally! I completed the subscription, I got in total " +
64 | totalRead + " magazines.");
65 | }
66 |
67 | private void log(final String logMessage) {
68 | log.info("<=========== [" + subscriberName + "] : " + logMessage);
69 | }
70 |
71 | public String getSubscriberName() {
72 | return subscriberName;
73 | }
74 |
75 | private void takeSomeRest() {
76 | try {
77 | Thread.sleep(sleepTime);
78 | } catch (InterruptedException e) {
79 | throw new RuntimeException(e);
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/com/thepracticaldeveloper/ReactiveFlowApp.java:
--------------------------------------------------------------------------------
1 | package com.thepracticaldeveloper;
2 |
3 | import java.util.concurrent.ForkJoinPool;
4 | import java.util.concurrent.SubmissionPublisher;
5 | import java.util.concurrent.TimeUnit;
6 | import java.util.stream.IntStream;
7 |
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | public class ReactiveFlowApp {
12 |
13 | private static final int NUMBER_OF_MAGAZINES = 20;
14 | private static final long MAX_SECONDS_TO_KEEP_IT_WHEN_NO_SPACE = 2;
15 | private static final Logger log =
16 | LoggerFactory.getLogger(ReactiveFlowApp.class);
17 |
18 | public static void main(String[] args) throws Exception {
19 | final ReactiveFlowApp app = new ReactiveFlowApp();
20 |
21 | log.info("\n\n### CASE 1: Subscribers are fast, buffer size is not so " +
22 | "important in this case.");
23 | app.magazineDeliveryExample(100L, 100L, 8);
24 |
25 | log.info("\n\n### CASE 2: A slow subscriber, but a good enough buffer " +
26 | "size on the publisher's side to keep all items until they're picked up");
27 | app.magazineDeliveryExample(1000L, 3000L, NUMBER_OF_MAGAZINES);
28 |
29 | log.info("\n\n### CASE 3: A slow subscriber, and a very limited buffer " +
30 | "size on the publisher's side so it's important to keep the slow " +
31 | "subscriber under control");
32 | app.magazineDeliveryExample(1000L, 3000L, 8);
33 |
34 | }
35 |
36 | void magazineDeliveryExample(final long sleepTimeJack,
37 | final long sleepTimePete,
38 | final int maxStorageInPO) throws Exception {
39 | final SubmissionPublisher publisher =
40 | new SubmissionPublisher<>(ForkJoinPool.commonPool(), maxStorageInPO);
41 |
42 | final MagazineSubscriber jack = new MagazineSubscriber(
43 | sleepTimeJack,
44 | MagazineSubscriber.JACK
45 | );
46 | final MagazineSubscriber pete = new MagazineSubscriber(
47 | sleepTimePete,
48 | MagazineSubscriber.PETE
49 | );
50 |
51 | publisher.subscribe(jack);
52 | publisher.subscribe(pete);
53 |
54 | log.info("Printing 20 magazines per subscriber, with room in publisher for "
55 | + maxStorageInPO + ". They have " + MAX_SECONDS_TO_KEEP_IT_WHEN_NO_SPACE +
56 | " seconds to consume each magazine.");
57 | IntStream.rangeClosed(1, 20).forEach((number) -> {
58 | log.info("Offering magazine " + number + " to consumers");
59 | final int lag = publisher.offer(
60 | number,
61 | MAX_SECONDS_TO_KEEP_IT_WHEN_NO_SPACE,
62 | TimeUnit.SECONDS,
63 | (subscriber, msg) -> {
64 | subscriber.onError(
65 | new RuntimeException("Hey " + ((MagazineSubscriber) subscriber)
66 | .getSubscriberName() + "! You are too slow getting magazines" +
67 | " and we don't have more space for them! " +
68 | "I'll drop your magazine: " + msg));
69 | return false; // don't retry, we don't believe in second opportunities
70 | });
71 | if (lag < 0) {
72 | log("Dropping " + -lag + " magazines");
73 | } else {
74 | log("The slowest consumer has " + lag +
75 | " magazines in total to be picked up");
76 | }
77 | });
78 |
79 | // Blocks until all subscribers are done (this part could be improved
80 | // with latches, but this way we keep it simple)
81 | while (publisher.estimateMaximumLag() > 0) {
82 | Thread.sleep(500L);
83 | }
84 |
85 | // Closes the publisher, calling the onComplete() method on every subscriber
86 | publisher.close();
87 | // give some time to the slowest consumer to wake up and notice
88 | // that it's completed
89 | Thread.sleep(Math.max(sleepTimeJack, sleepTimePete));
90 | }
91 |
92 | private static void log(final String message) {
93 | log.info("===========> " + message);
94 | }
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/src/main/java/com/thepracticaldeveloper/logutils/ColorConsoleAppender.java:
--------------------------------------------------------------------------------
1 | package com.thepracticaldeveloper.logutils;
2 |
3 | import com.thepracticaldeveloper.MagazineSubscriber;
4 |
5 | import org.apache.log4j.ConsoleAppender;
6 | import org.apache.log4j.spi.LoggingEvent;
7 |
8 | public class ColorConsoleAppender extends ConsoleAppender {
9 |
10 | @Override
11 | protected void subAppend(final LoggingEvent event) {
12 | int color = 36;
13 | if (event.getRenderedMessage().contains(MagazineSubscriber.JACK)) {
14 | color = 32;
15 | } else if (event.getRenderedMessage().contains(MagazineSubscriber.PETE)) {
16 | color = 31;
17 | }
18 | qw.write("\u001b[0;" + color + "m");
19 | super.subAppend(event);
20 | qw.write("\u001b[m");
21 | if (this.immediateFlush) qw.flush();
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/resources/log4j.properties:
--------------------------------------------------------------------------------
1 | log4j.logger.com.thepracticaldeveloper=ALL,CONSOLE
2 | log4j.appender.CONSOLE=com.thepracticaldeveloper.logutils.ColorConsoleAppender
3 | log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
4 | log4j.appender.CONSOLE.layout.ConversionPattern=[%p] %d %m%n
5 |
--------------------------------------------------------------------------------