├── .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 | ![The Reactive Magazine Publisher ](img/thereactivepublisher.png) 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 | --------------------------------------------------------------------------------