Contenu connexe Similaire à サーバーサイドでの非同期処理で色々やったよ (20) サーバーサイドでの非同期処理で色々やったよ2. 自己紹介
● LINE Fukuoka Corp
○ Java でサーバーサイド開発
● Taiwan Java User Group メンバー
○ https://www.meetup.com/taiwanjug/
6. 複雑化していくシステム
List<Item> getRanking(String country) {
String rankingType = api.getRankingType("JP");
List<String> rankingIds =
searchClient.getRanking(rankingType);
return rankingIds.stream()
.map(dao::getItem)
.map(...)
.collect(toList());
}
● パフォーマンスや機能の追加/複雑さを軽減するため、いろん
なサービス/ミドルウェア/チームに分ける
9. 同期なコードを非同期に書き直す
● フレームワークの変更
● 戻り値の型は Guava ListenableFuture を選択
○ CompletableFuture に対応するライブラリがまだ少ない
○ Futures#transform で非同期が組み合わせれる
○ Dagger で非同期 DI が利用できる
● ストレージアクセス
● リモート API アクセス
10. フレームワークを社内製品へ
● RESTful と Thrift RPC のエンドポイントを提供
○ Spring Web/Spark と Facebook Nifty を使っていた
● 社内製でオープンソースの Armeria に移行
○ https://github.com/line/armeria
○ Netty ベース HTTP/2 対応の非同期 RPC/REST library
● 実際 Nifty + swift で非同期も可能です
○ https://github.com/facebook/swift
○ その swift ではない !
11. REST Controller も非同期へ
● 全てのフレームワークを Spring Web
● 一部同期の Controller から非同期
○ Spring Web の DeferredResult<T>
@RequestMapping("/hello")
public DefferredResult<String> hello() {
DeferredResult<String> deferredResult = new
DeferredResult<>();
... // callback で deferredResult.setResult("hello");
return deferredResult;
}
12. Thrift とは?
● RPC フレームワーク
● .thrift の IDL を定義
service HelloService{
string hello(1: string name)
}
● Thrift Compiler で対応の言語のコードを生成
● ロジックを入れて、サポートしてるライブラリ上でデプロイすれ
ば良い
13. 同期の Iface から非同期の AsyncIface へ
@Override
public String hello(String name) {
return "Hello, " + name + '!';
}
@Override
public void hello(String name,
AsyncMethodCallback<String> resultHandler) {
resultHandler.onComplete("Hello, " + name + '!');
}
18. MongoDB を ListenableFuture に
public ListenableFuture<List<Model>> list(
String id, int offset, int limit) {
SettableFuture<List<Model>> future =
SettableFuture.create();
collection.find(eq(ID, id))
.skip(offset)
.limit(limit).into(list, (result, t) -> {
if (t != null) { future.setException(t); }
else { future.set(result); }
});
return future;
}
19. リモート API アクセス
● Apache HttpComponents から Armeria の HttpClient へ
○ Apache HttpComponents にも Async Client がある
● REST API が多すぎるので、Retrofit を利用して、ネットワーク
層は Armeria の HttpClient
20. Retrofit と併用
● Retrofit で API を Java コードへマッピングする
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service =
retrofit.create(GitHubService.class);
21. Retrofit と GuavaCallAdapterFactory
● Retrofit は戻り値の型を拡張できる
public interface GitHubService {
@GET("users/{user}/repos")
ListenableFuture<List<Repo>> listRepos(
@Path("user") String user);
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addCallAdapterFactory(
GuavaCallAdapterFactory.create())
.build();
23. Guava の transform で組み合わせ
ListenableFuture<String> result =
FuturesExtra.syncTransform(
dao.getUser("id"),
user -> user.getName());
ListenableFuture<Image> result =
Futures.transformAsync(
dao.getUser("id"),
user -> apiClient.getIcon(user));
27. その他
● Spotify の Futures-extra を多用
○ https://github.com/spotify/futures-extra
○ Guava 19 で transform メソッドのオーバロディングで
コンパイルウォーニングがめんどくさい
● AsyncRetrier と ConcurrencyLimiter も便利
28. AsyncRetrier
int retryCount = 3;
int delayMillis = 100;
AsyncRetrier retrier = AsyncRetrier.create(
Executors.newSingleThreadScheduledExecutor());
ListenableFuture<List<String>> listFuture =
retrier.retry(() -> api.listByRanking("JP"),
retryCount,
delayMillis);
29. ConcurrencyLimiter
int maxConcurrentCount = 100;
int maxQueueSize = 1000;
ConcurrencyLimiter<List<String>> concurrencyLimiter =
ConcurrencyLimiter.create(maxConcurrentCount,
maxQueueSize);
ListenableFuture<List<String>> listFuture =
concurrencyLimiter.add(() ->
dao.listByRanking("JP"));
● 非同期化で一気にリモートアクセスが一杯流せてリソー
スを喰いつくすのを防ぐ
34. Dagger Producers で複雑さを軽減
● Dagger
○ コンパイル時依存性を解決する DI フレームワーク
● Dagger Producers
○ 非同期な DI を実現
○ メソッドのリターンタイプを ListenableFuture<T> にして、
受取メソッドのパラメータを T にすると Dagger がよしなに
組み合わせてくれる
35. @ProducerModule
public static class RankingGraph {
@ProductionComponent(modules = { RankingGraph.class, ExecutorModule.class })
interface Component {
ListenableFuture<List<Item>> getRanking();
}
public RankingGraph(Service service, String country) {
...
}
@Produces
public ListenableFuture<String> getRankingType() {
return service.getRankingType(country);
}
@Produces
public ListenableFuture<List<String>> listByRanking(String type) {
return service.listByRanking(type);
}
@Produces
public ListenableFuture<List<Item>> listUsers(List<String> ids) {
return Futures.allAsList(ids.stream().map(service::getItemById).collect(toList()));
}
}
36. @ProducerModule
public static class RankingGraph {
@ProductionComponent(modules = { RankingGraph.class, ExecutorModule.class })
interface Component {
ListenableFuture<List<Item>> getRanking();
}
public RankingGraph(Service service, String country) {
...
}
@Produces
public ListenableFuture<String> getRankingType() {
return service.getRankingType(country);
}
@Produces
public ListenableFuture<List<String>> listByRanking(String type) {
return service.listByRanking(type);
}
@Produces
public ListenableFuture<List<Item>> listUsers(List<String> ids) {
return Futures.allAsList(ids.stream().map(service::getItemById).collect(toList()));
}
}
37. @ProducerModule
public static class RankingGraph {
@ProductionComponent(modules = { RankingGraph.class, ExecutorModule.class })
interface Component {
ListenableFuture<List<Item>> getRanking();
}
public RankingGraph(Service service, String country) {
...
}
@Produces
public ListenableFuture<String> getRankingType() {
return service.getRankingType(country);
}
@Produces
public ListenableFuture<List<String>> listByRanking(String type) {
return service.listByRanking(type);
}
@Produces
public ListenableFuture<List<Item>> listUsers(List<String> ids) {
return Futures.allAsList(ids.stream().map(service::getItemById).collect(toList()));
}
}
38. @ProducerModule
public static class RankingGraph {
@ProductionComponent(modules = { RankingGraph.class, ExecutorModule.class })
interface Component {
ListenableFuture<List<Item>> getRanking();
}
public RankingGraph(Service service, String country) {
...
}
@Produces
public ListenableFuture<String> getRankingType() {
return service.getRankingType(country);
}
@Produces
public ListenableFuture<List<String>> listByRanking(String type) {
return service.listByRanking(type);
}
@Produces
public ListenableFuture<List<Item>> listUsers(List<String> ids) {
return Futures.allAsList(ids.stream().map(service::getItemById).collect(toList()));
}
}
42. Dagger を使ったメリット
● transformAsync 等でのネストが減った
● 発火スレッドが全て ExecutorModule で指定した executor で
始まる
● Convention 化し易い
● メソッド毎にバラバラで書いても、コンパイルタイムで揃ってる
か検査してくれる
● 実際 Guava ドキュメントも勧めてる(?)
49. なぜ RxJava2 に移行した?
● ListenableFuture だけで組み合わせが書きやすくない
○ Guava 23 で FluentFuture がある
● Dagger Producers
○ Module の再利用が大変だった
○ 微妙に読みやすくない
● RxJava2 がそろそろ安定してそうだった
50. RxJava2
● Java VM implementation of Reactive Extensions
● A library for composing asynchronous and event-based
programs by using observable sequences.
51. RxJava2
● Single<T>
○ 1 個のデータ
○ CompletableFuture<T> で中身は絶対 null ではない
● Maybe<T>
○ 空っぽか1個のデータ
○ CompletableFuture<T> で中身は null かも知れない
● Completable
○ 空っぽ
○ CompletableFuture<Void>
54. RxJava2 で設計した API
class UserDao {
public Single<User> get(String id){...}
public Maybe<User> find(String id){...}
public Completable delete(String id){...}
public Flowable<User> listAll(){...}
public Single<List<User>> listAllSingle(){...}
}
55. JDBC のアクセスを RxJava2 に
ListeningExecutorService asyncExecutor;
public <E> Single<List<E>> selectListRx(
String statement, Object parameter) {
return toSingle(asyncExecutor.submit(() ->
delegate.selectList(statement, parameter)));
}
56. MongoDB を RxJava2 に
Public Single<List<Model>> listRx(
String id, int offset, int limit) {
SettableFuture<List<Model>> future =
SettableFuture.create();
collection.find(eq(ID, id))
.skip(offset)
.limit(limit).into(list, (result, t) -> {
if (t != null) { future.setException(t); }
else { future.set(result); }
});
return toSingle(future);
}
57. MongoDB を RxJava2 に
● 実際 MongoDB には reactive extension 対応の Driver があ
るので、それを RxJava2 化すればいい
Public Single<List<Model>> listRx(
String id, int offset, int limit) {
return Flowable.fromPublisher(
collection.find(eq(ID, id))
.skip(offset)
.limit(limit))
.toList();
}
58. Retrofit と RxJava2CallAdapterFactory
● Retrofit の戻り値の型をRxJava2 に
public interface GitHubService {
@GET("users/{user}/repos")
Single<List<Repo>> listRepos(
@Path("user") String user);
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addCallAdapterFactory(
RxJava2CallAdapterFactory.create())
.build();
60. @ProducerModule
public static class RankingGraph {
@ProductionComponent(modules = { RankingGraph.class, ExecutorModule.class })
interface Component {
ListenableFuture<List<Item>> getRanking();
}
public RankingGraph(Service service, String country) {
...
}
@Produces
public ListenableFuture<String> getRankingType() {
return service.getRankingType(country);
}
@Produces
public ListenableFuture<List<String>> listByRanking(String type) {
return service.listByRanking(type);
}
@Produces
public ListenableFuture<List<Item>> listUsers(List<String> ids) {
return
Futures.allAsList(ids.stream().map(service::getItemById).collect(toList()));
}
}
ListenableFuture<List<Item>> result =
DaggerRankingGraph_Component
.builder()
.rankingGraph(new RankingGraph(service, "JP"))
.build()
.getRanking();
63. 移行で大変だったとこ
● ListenableFuture と Dagger で慣れ始めたのに、またかよ...
○ RxJava2 は Stream API な感じでつなげていけるので、で
きれば同じような感覚で開発してもらいたい
○ Project Reactor とかもあるし、似てるようなプログラミング
手法がでてくる
64. 移行で大変だったとこ
● RxJava は null を容赦しない !
○ Flowable/Single/Maybe に null を入れたら、NPE !
Single.just("koji")
.map(id -> null) // NPE !!!
...//
65. 移行で大変だったとこ
● Eager vs Lazy
○ Future<User> getUser(String id)
■ 呼んだ瞬間に発火
○ Single<User> getUser(String id)
■ 戻り値に subscribe した時に発火
■ でも RxJava2 と Future 変換があるので、実はそうで
もない
67. 移行で大変だったとこ
● Flowable の flatMap vs concatEagerMap
○ 順番守りたいなら concatEagerMap
http://www.nurkiewicz.com/2017/08/flatmap-vs-concatmap-vs-co
ncatmapeager.html
Flowable.just("koji", "kishida", "tempo")
.flatMapSingle(id -> api.fetchUser(id))
...//
Flowable.just("koji", "kishida", "tempo")
.concatMapEager(id ->
api.fetchUser(id).toFlowable())
...//
68. 移行で大変だったとこ
● 複数回 subscribe 問題
Single<User> user = client.getUser("1234");
Single<Profile> first = user.map(...)...;
Single<Company> second = user.map(...)...;
Single.zip(first, second, (profile, address) -> {
...
})...;
69. 移行で大変だったとこ
● 複数回 subscribe 問題
Single<User> user = client.getUser("1234").cache();
Single<Profile> first = user.map(...)...;
Single<Company> second = user.map(...)...;
Single.zip(first, second, (profile, address) -> {
...
})...;
72. その他
● RxJava2 と Java8 CompletableFuture の変換ライブラ
リ
○ akarnokd/RxJava2Jdk8Interop
● Debugging と色々な便利 operators/transformers
○ akarnokd/RxJava2Extensions
74. これから...
● Java には extension method みたいなものがないので、
filterAsync みたいな compose でするしかない
● 複雑な組み合わせ以外は、async/await みたいなものがほし
い
● ……..kotlin かな?
76. ● Reactive Programming with RxJava
● Going Reactive with Spring 5 & Project Reactor
○ Devoxx youtube: https://youtu.be/yAXgkSlrmBA