Upload
igor-lozynskyi
View
204
Download
2
Embed Size (px)
Citation preview
RxJava Applied: Concise Examples where It Shines
Igor Lozynskyi
JavaDay Kyiv - Oct 14-15, 2016
Software Engineer at GlobalLogic
Presentation’s home
https://github.com/aigor/rx-presentation
Outline
● RxJava vs Java 8 Streams
● RxJava usage example
Pre Java 8 data processing
interface Iterator<T> {
T next();
boolean hasNext(); void remove();}
Pre Java 8 data processing
List<Employee> employees = service.getEmployees();
Map<Integer, List<Employee>> ageDistribution = new HashMap<>();for (Employee employee : employees) { if (employee.getAge() > 25){ List<Employee> thisAge = ageDistribution.get(employee.getAge()); if (thisAge != null){ thisAge.add(employee); } else{ List<Employee> createThisAge = new ArrayList<>(); createThisAge.add(employee); ageDistribution.put(employee.getAge(), createThisAge); } }}
System.out.println(ageDistribution);
Java 8 Stream ...
● Connects data source and client
● Do not hold any data
● Implements map / filter / reduce pattern
● Enforces functional style of data processing
Stream collectors
List<Employee> employees = service.getEmployees();
Map<Integer, List<Employee>> ageDistribution = new HashMap<>();for (Employee employee : employees) { if (employee.getAge() > 25) { List<Employee> thisAge = ageDistribution.get(employee.getAge()); if (thisAge != null){ thisAge.add(employee); } else { List<Employee> createThisAge = new ArrayList<>(); createThisAge.add(employee); ageDistribution.put(employee.getAge(), createThisAge); } }}
System.out.println(ageDistribution);
Stream collectors
List<Employee> employees = service.getEmployees();
Map<Integer, List<Employee>> ageDistribution =
employees.stream()
.filter(e -> e.getAge() > 25)
.collect(Collectors.groupingBy(Employee::getAge));
System.out.println(ageDistribution);
Stream collectors
List<Employee> employees = service.getEmployees();
Map<Integer, Long> ageDistribution =
employees.stream()
.filter(e -> e.getAge() > 25)
.collect(Collectors.groupingBy(
Employee::getAge,
Collectors.counting()
));
Stream sources
Stream<String> stream1 = Arrays.asList("A", "B").stream();
Stream<String> stream2 = Stream.of("Q", "P", "R");
IntStream chars = "some text".chars();
Stream<String> words
= Pattern.compile(" ").splitAsStream("some other text");
Non-standard stream sources
Can we use non-standard stream sources?
?
Stream generator
Stream .generate(() -> UUID.randomUUID().toString()) .limit(4) .forEach(System.out::println);
Spliterator interface
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics(); }
class RandomSpliterator implements Spliterator<String> { public boolean tryAdvance(Consumer<? super String> action) { action.accept(UUID.randomUUID().toString()); return true; }
// for parallel streams public Spliterator<String> trySplit() { return null; }
public long estimateSize() { return Long.MAX_VALUE; };
public int characteristics() { return NONNULL | DISTINCT; }}
Generate sequence: 1, 2, 2, 3, 3, 3, 4, 4, 4, 4, ...
IntStream sequence = IntStream.rangeClosed(1, 50) .flatMap(i -> IntStream.iterate(i, identity()).limit(i) );
sequence.forEach(System.out::println);
Stream API is powerful!
Stream API - async processing
getEmployeeIds().stream() .map(this::doHttpRequest) .collect(Collectors.toList())
Output:[main] processing request: c677c497[main] processing request: 3b5320a9[main] processing request: 9248b92e[main] processing request: 97a68a53
Stream API - async processing
getEmployeeIds().stream() .parallel() .map(this::doHttpRequest) .collect(Collectors.toList())
Output:[main] processing request: 7674da72[ForkJoinPool.commonPool-worker-2] processing request: 747ae948[ForkJoinPool.commonPool-worker-1] processing request: 33fe0bac[ForkJoinPool.commonPool-worker-3] processing request: 812f69f3[main] processing request: 11dda466[ForkJoinPool.commonPool-worker-2] processing request: 12e22a10[ForkJoinPool.commonPool-worker-1] processing request: e2b324f9[ForkJoinPool.commonPool-worker-3] processing request: 8f9f8a97
Stream API - async processing
ForkJoinPool forkJoinPool = new ForkJoinPool(80);
forkJoinPool.submit(() -> getEmployeeIds().stream() .parallel() .map(this::doHttpRequest) .collect(Collectors.toList())).get();
Stream API has some limitations
Reactive Streams: what the difference
RxJavahttp://reactivex.iohttps://github.com/ReactiveX/RxJava
17,700 stars on GitHub
Short history
● From 2009 for .NET
● From 2013 for JVM (latest: 1.2.1, Oct 5, 2016)
● Now a lot of other languages
RxJava Observer
interface Observer<T> {
void onNext(T t);
void onCompleted();
void onError(Throwable e);}
RxJava Subscription
interface Subscription {
void unsubscribe();
boolean isUnsubscribed();}
Observable
● Central class in RxJava library
● It’s big: ~ 10k lines of code (with comments)
● It’s complex: ~ 100 static methods, ~ 150 non-static
Subscription sub = Observable .create(s -> { s.onNext("A"); s.onNext("B"); s.onCompleted(); }) .subscribe(m -> log.info("Message received: " + m), e -> log.warning("Error: " + e.getMessage()), () -> log.info("Done!"));
Output:Message received: AMessage received: BDone!
Observable<Integer> empty = Observable.empty();
Observable<Integer> never = Observable.never();
Observable<Integer> error = Observable.error(exception);
Observable useful for tests
never() - never emit anything (no data, no errors)
public static <T, Resource> Observable<T> using(
final Func0<Resource> resourceFactory,
final Func1<Resource, Observable<T>> observableFactory,
final Action1<Resource> disposeAction){ }
Observable from resource
Observable .from(Arrays.asList(1, 2, 5, 7, 8, 12, 3, 6, 7, 8)) .filter(i -> (i > 3 && i < 8)) .forEach(System.out::println);
Output:5767
Marble diagram: filter
Marble diagram: last
Marble diagram: reduce
Marble diagram: buffer
Marble diagram: timer
Marble diagram: interval
Time series
Observable<Long> timer = Observable.timer(2, TimeUnit.SECONDS);
Observable<Long> interval = Observable.interval(1, TimeUnit.SECONDS);
timer.forEach(System.out::println);
interval.forEach(System.out::println);
Output:Process finished with exit code 0
Time series
Observable<Long> timer = Observable.timer(2, TimeUnit.SECONDS);
Observable<Long> interval = Observable.interval(1, TimeUnit.SECONDS);
timer.forEach(i -> System.out.println(currentThread().getName() + " - " + i));
interval.forEach(i -> System.out.println(currentThread().getName() + " - " + i));
Output:RxComputationThreadPool-2 - 0RxComputationThreadPool-1 - 0RxComputationThreadPool-2 - 1Process finished with exit code 0
Thread.sleep(2000);
Schedulers
public final class Schedulers {
public static Scheduler immediate() {...}
public static Scheduler trampoline() {...}
public static Scheduler newThread() {...}
public static Scheduler computation() {...}
public static Scheduler io() {...}
public static TestScheduler test() {...}
public static Scheduler from(Executor executor) {...}
}
Schedulers
Observable.from("one", "two", "three", "four", "five")
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(/* an Observer */);
Marble diagram: merge
Merge
Observable<Integer> odds = Observable
.just(1, 3, 5).subscribeOn(Schedulers.io());
Observable<Integer> evens = Observable.just(2, 4, 6);
Observable.merge(odds, evens)
.subscribe(
System.out::println,
System.err::println,
() -> System.out.println("Finished"));
Marble diagram: zip
Zip
Observable<String> odds = Observable.just("A", "B", "C", "D");
Observable<Integer> evens = Observable.just(2, 4, 6);
Observable.zip(odds, evens, (a, b) -> a + "-" + b)
.subscribe(
System.out::println,
System.err::println,
() -> System.out.println("Finished"));
Output:A-2B-4C-6Finished
Cold & Hot Observables
● Cold Observable emits events only having subscriber
● Hot Observable emits events even without subscribers
Backpressure
Backpressure
Backpreasure is a way to slow down emission of elements
It can act on observing side
Strategies:
● Buffering items (buffer, window)
● Skipping items (sampling, throttling, etc)
● Request for specific number of elements
Backpressure: request for elements
public interface Producer {
void request(long n);
}
Stream API vs RxJava
RxJava:
● allows to process data in chosen thread, this is useful for IO,
computations, specialized threads (GUI threads),
● allows synchronization on clocks and application events,
● works in push mode, producer initiates data transfer, but consumer may
control data flow via backpressure.
Stream API:
● tuned for hot data processing,
● good for parallel computation,
● has rich set of collectors for data.
Shakespeare Plays Scrabble Benchmark (throughput)
Non-Parallel Streams 44.995 ± 1.718 ms/op
Parallel Streams 14.095 ± 0.616 ms/op
RxJava 310.378 ± 9.688 ms/op
RxJava (2.0.0-RC4) 156.334 ± 8.423 ms/op
Performance
Based on JMH benchmark by Jose Paumard
Scenarios where RxJava shines
● Observables are better callbacks (easily wrap callbacks)
● Observables are highly composable (on contrast with callbacks)
● Provide async stream of data (on contrast with CompletableFuture)
● Observables can handle errors (have retry / backup strategies)
● Give complete control over running threads
● Good for managing IO rich application workflows
● Perfect for Android development (no Java 8 required, retrolambda compatible)
● Netflix uses RxJava for most their internal APIs
Request flow
Created with draw.io
Stream API and RxJava are friends!
You can easily build Observable from Stream
Iterator<T> iterator = ...;
Observable<T> observable = Observable.from(() -> iterator);
You can map Observable to Stream by implementing adapter
public static<T> Iterator<T> of(Observable<? extends T> ob){
class Adapter implements Iterator<T>, Consumer<T> {
...
}
return new Adapter();
}
External libraries that work with RxJava
● hystrix - latency and fault tolerance bulkheading library
● camel RX - to reuse Apache Camel components
● rxjava-http-tail - allows you to follow logs over HTTP
● mod-rxvertx - extension for VertX that provides support Rx
● rxjava-jdbc - use RxJava with JDBC to stream ResultSets
● rtree - immutable in-memory R-tree and R*-tree with RxJava
● and many more ...
RxJava is not new
JDeferred
CompletableFuture<T>
Scala.Rx
Scala Actors
Spring Integration
What’s around?
● Google Agera - reactive streams for Android by Google
● Project Reactor - flow API implementation by Pivotal & Spring
● Akka-Streams - Akka based streams
● Monix - Scala based implementation of Reactive Streams
● Vert.x, Lagom - if you want more than streams
Let’s build something with RxJava
Use case: Stream of tweets
Created with Balsamiq Mockups
Twitter API
Twitter Stream API (WebSocket alike):● Doc: https://dev.twitter.com/streaming/overview
● Library: com.twitter:hbc-core:2.2.0
Twitter REST API:● GET https://api.twitter.com/1.1/users/show.json?screen_name=jeeconf
● GET https://api.twitter.com/1.1/search/tweets.json?q=from:jeeconf
Let’s look at entities
class Tweet { String text; int favorite_count; String author; int author_followers;}
class Profile { String screen_name; String name; String location; int statuses_count; int followers_count;}
class UserWithTweet { Profile profile; Tweet tweet;}
Marble diagram
Profile getUserProfile(String screenName) { ObjectMapper om = new ObjectMapper(); return (Profile) om.readValue(om.readTree( Unirest.get(API_BASE_URL + "users/show.json") .queryString("screen_name", screenName) .header("Authorization", bearerAuth(authToken.get())) .asString() .getBody()), Profile.class);}
Get user profile synchronously
Get user profile asynchronously
Observable<Profile> getUserProfile(String screenName) { return Observable.fromCallable(() -> { ObjectMapper om = new ObjectMapper(); return (Profile) om.readValue(om.readTree( Unirest.get(API_BASE_URL + "users/show.json") .queryString("screen_name", screenName) .header("Authorization", bearerAuth(authToken.get())) .asString()
.getBody()), Profile.class); });}
Add some errors handling
Observable<Profile> getUserProfile(String screenName) { if (authToken.isPresent()) { return Observable.fromCallable(() -> { ObjectMapper om = new ObjectMapper(); return (Profile) om.readValue(om.readTree( Unirest.get(API_BASE_URL + "users/show.json") .queryString("screen_name", screenName) .header("Authorization", bearerAuth(authToken.get())) .asString() .getBody()), Profile.class); }).doOnCompleted(() -> log("getUserProfile completed for: " + screenName)); } else { return Observable.error(new RuntimeException("Can not connect to twitter")); }}
Let’s do something concurrently
Illustration: Arsenal Firearms S.r.l.
Get data concurrently
Observable<UserWithTweet> getUserAndPopularTweet(String author){ return Observable.just(author) .flatMap(u -> { Observable<Profile> profile = client.getUserProfile(u) .subscribeOn(Schedulers.io()); Observable<Tweet> tweet = client.getUserRecentTweets(u) .defaultIfEmpty(null) .reduce((t1, t2) -> t1.retweet_count > t2.retweet_count ? t1 : t2) .subscribeOn(Schedulers.io()); return Observable.zip(profile, tweet, UserWithTweet::new); });}
Let’s subscribe on stream of tweets!
streamClient.getStream("RxJava", "JavaDay", "Java")
streamClient.getStream("RxJava", "JavaDay", "Java", "Trump", "Hillary")
streamClient.getStream("RxJava", "JavaDay", "Java", "Trump", "Hillary")
.distinctUntilChanged()
.map(p -> p.author)
.flatMap(name -> getUserAndPopularTweet(name))
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.immediate())
.subscribe(p -> log.info("The most popular tweet of user " + p.profile.name + ": " + p.tweet));
.scan((u1, u2) -> u1.author_followers > u2.author_followers ? u1 : u2)
streamClient.getStream("RxJava", "JavaDay", "Java", "Trump") .scan((u1, u2) -> u1.author_followers > u2.author_followers ? u1 : u2) .distinctUntilChanged() .map(p -> p.author) .flatMap(name -> { Observable<Profile> profile = client.getUserProfile(name) .subscribeOn(Schedulers.io()); Observable<Tweet> tweet = client.getUserRecentTweets(name) .defaultIfEmpty(null) .reduce((t1, t2) -> t1.retweet_count > t2.retweet_count ? t1 : t2) .subscribeOn(Schedulers.io()); return Observable.zip(profile, tweet, UserWithTweet::new); }) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.immediate()) .subscribe(p -> log.info("The most popular tweet of user " + p.profile.name + ": " + p.tweet));
Marble diagram
@Test public void correctlyJoinsHttpResults() throws Exception {
String testUser = "testUser";
Profile profile = new Profile("u1", "Name", "USA", 10, 20, 30);
Tweet tweet1 = new Tweet("text-1", 10, 20, testUser, 30);
Tweet tweet2 = new Tweet("text-2", 40, 50, testUser, 30);
TwitterClient client = mock(TwitterClient.class);
when(client.getUserProfile(testUser)).thenReturn(Observable.just(profile));
when(client.getUserRecentTweets(testUser)).thenReturn(Observable.just(tweet1, tweet2));
TestSubscriber<UserWithTweet> testSubscriber = new TestSubscriber<>();
new Solutions().getUserAndPopularTweet(client, testUser).subscribe(testSubscriber);
testSubscriber.awaitTerminalEvent();
assertEquals(singletonList(new UserWithTweet(profile, tweet2)),
testSubscriber.getOnNextEvents());
}
Don’t believe it works?
● API is big (150+ methods to remember)
● Requires to understand underlying magic
● Hard to debug
● Don’t forget about back pressure
Conclusions: pitfalls
● It is functional, it is reactive*
● Good for integration scenarios
● Allows to control execution threads
● Easy to compose workflows
● Easy to integrate into existing solutions
● Easy Ok to test
* RxJava is inspired by FRP (Functional Reactive Programming), but doesn’t implement it
Conclusions: strength
● RxJava 2.0 in few weeks!
● RxJava 2.0 is better and faster
● RxJava 2.0 is Java 9 Flow API compatible
● We will see more and more reactive streams
Conclusions: future
Q&A https://github.com/aigor/rx-presentation