MicronautまたはDarlingを試して、フレームワークを減らしました
マイクロノートフレームワークについては、ニュースレターダイジェストを垣間見ることができました。 彼は自分がどんな獣なのかと思いました。 フレームワークは、Springに必要なすべてのツールが詰め込まれているのとは対照的です。

開発者がマイクロサービスでマイクロノートを使用する方法を話し、説明するカンファレンスを予想して、少なくとも一度は準備をして、特定の問題と質問のセットで少なくともいくつかのコンテキストを頭に入れることにしました。 いわば宿題をします。 私は、2、3の夕方のために小さなペットプロジェクトを立ち上げることにしました(進行中)。 記事の最後に、すべてのプロジェクトソースのリポジトリへのリンクがあります。
Micronautは、Java、Kotlin、Groovyの3つの開発言語をサポートするJVMフレームワークです。 Grailsを提供したのと同じ会社であるOCIによって開発されました。 CLIアプリケーションと一連の推奨ライブラリ(さまざまなリアクティブHTTPおよびデータベースクライアント)の形でチューニングされています
Springのアイデアを実装および繰り返すDIがあり、そのチップの多くを追加します-非同期性、AWS Lambdaのサポート、クライアント側の負荷分散。
サービスのアイデア:一度に6人のさまざまな暗号通貨で購入した愚か者の友人で、含浸された休暇と冬のジャケットの隠し場所に投資します。 このすべての暗号通貨物質のボラティリティは野生であり、トピック自体は一般に予測できないことを知っています。友人は最終的に彼の神経の世話をし、彼の「資産」で何が起こっているかを単に沈めることにしました。 しかし、時々あなたはまだ見たいと思うかもしれませんが、これらすべてにそこにあるものは、突然豊かになります。 そこで、シンプルなパネル(Grafanaなどのダッシュボード)、乾燥した情報を含む一種のWebページ、すべての費用が法定通貨(USD、RUR)でいくらになるかというアイデアが浮上しました。
免責事項
- 独自のソリューションを作成するという実現可能性はそのままにしておきます。HelloWorldよりも難しいもので新しいフレームワークを試すだけです。
- 計算アルゴリズム、予想されるエラー、エラーなど (少なくとも製品の最初のフェーズでは)、情報を抽出するための暗号交換の選択の有効性、友人の「投資」暗号ポートフォリオも問題外であり、議論や何らかの深い分析の対象ではありません。
そのため、要件の小さなセット:
- Webサービス(httpを介した外部からのアクセス)
- 暗号通貨ポートフォリオの合計値の概要を含むブラウザーでページを表示する
-  ポートフォリオを構成する機能(ポートフォリオ構造をロードおよびアンロードするためのJSON形式を選択します)。 ポートフォリオを更新してロードするための特定のREST API、つまり  2 API:保存/更新用-POST、アンロード用-GET。 ポートフォリオ構造は基本的にシンプルなタイプのプレートです 
      
 
 BTC – 0.00005 . XEM – 4.5 . ...
 
 
 
 
- 暗号通貨交換と通貨交換ソースからデータを取得します(不換通貨用)
-  ポートフォリオの合計値を計算するためのルール: 
      
 
   
 
 
もちろん、パラグラフ5に書かれていることはすべて、別個の紛争と疑念の対象ですが、ビジネスがそれを望んでいたことを認めましょう。
プロジェクト開始
 したがって、フレームワークの公式Webサイトにアクセスし、開発を開始する方法を確認します。 公式サイトでは、sdkmanツールのインストールを提案しています。  micronautフレームワーク(およびGrailsなどの他のプロジェクト)のプロジェクトの開発と管理を容易にする作品。 
      
        
        
        
      
    
 
      ちょっとした注意:キーなしでプロジェクトの初期化を開始するだけの場合、Gradle Collectorがデフォルトで選択されます。 フォルダを削除し、今度はキーを使用して再試行します。
 mn create-app com.room606.cryptonaut -b=maven
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      興味深い点は、sdkmanは、Spring Tool Suiteと同様に、プロジェクトを作成する段階で、最初に使用する「キューブ」を設定することを提供することです。 私はこれを特に実験したわけではなく、デフォルトのプリセットで作成しました。
 最後に、Intellij Ideaでプロジェクトを開き、micronautプロジェクトを作成するためのウィザードを備えたソースとリソースとディスクのセットを賞賛します。 
      
        
        
        
      
    
 
      Dockerfileに固執する
 FROM openjdk:8u171-alpine3.7 RUN apk --no-cache add curl COPY target/cryptonaut*.jar cryptonaut.jar CMD java ${JAVA_OPTS} -jar cryptonaut.jar
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      まあ、それは楽しくて立派です。 Prod / INT / QA /どのような環境にもアプリケーションを迅速に出力するためのツールがすぐに提供されました。 このため、プロジェクトへの精神的なプラス記号。
Mavenでプロジェクトを収集し、Dockerイメージを収集してDockerレジストリに公開するか、CIシステムのオプションとしてイメージバイナリをエクスポートするだけで十分です。
resourcesフォルダーには、アプリケーション構成パラメーター(Springのapplication.propertiesのアナログ)とlogbackライブラリーの構成ファイルを含むブランクも用意しました。 かっこいい!
アプリケーションのエントリポイントに移動して、クラスを学習します。 Spring Bootで、私たちに痛いほど馴染みのある写真が表示されます。 ここでは、フレームワークの開発者も何も発明し始めていませんでした。
 public static void main(String[] args) throws IOException { Micronaut.run(Application.class); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      おなじみのSpringコードと比較してください。
 public static void main(String[] args) { SpringApplication.run(Application.class, args); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      つまり また、必要に応じて作業に含まれるすべてのBeanを含むIoCコンテナーを作成します。 公式ドキュメントに従って少し実行したので、ゆっくりと開発を開始します。
以下が必要です。
- ドメインモデル
- REST APIを実装するためのコントローラー。
- データストレージレイヤー(データベースクライアントまたはORMなど)
- 暗号交換からのデータの消費者のコード、および不換通貨の交換からのデータ。 つまり サードパーティサービス用の最も単純なクライアントを作成する必要があります。 Springでは、有名なRestTemplateがこの役割に適していました。
- 柔軟な管理とアプリケーションの開始のための最小構成(構成の内容と方法について考えてみましょう)
- テスト! はい、自信を持って安全にコードを再結合し、新しい機能を実装するために、古いものの安定性を確認する必要があります
-  キャッシング。 これは基本的な要件ではありませんが、良好なパフォーマンスを得るには良いことです。このシナリオでは、キャッシュが間違いなく優れたツールである場所があります。 
      
 
 ネタバレ:ここではすべてが非常に悪くなります。
ドメインモデル
私たちの目的のために、次のモデルで十分です:暗号通貨ポートフォリオのモデル、一対の不換通貨の為替レート、不換通貨での暗号通貨価格、ポートフォリオの合計値。
 以下は、ほんの2、3のモデルのコードです 。残りはリポジトリで表示できます 。 そして、はい、私はこのプロジェクトでLombok
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    をねじ込むのがLombok
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    でした。 
 Portfolio.java package com.room606.cryptonaut.domain; import java.math.BigDecimal; import java.util.Collections; import java.util.Map; import java.util.TreeMap; public class Portfolio { private Map<String, BigDecimal> coins = Collections.emptyMap(); public Map<String, BigDecimal> getCoins() { return new TreeMap<>(coins); } public void setCoins(Map<String, BigDecimal> coins) { this.coins = coins; }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       FiatRate.java package com.room606.cryptonaut.domain; import java.math.BigDecimal; public class FiatRate { private String base; private String counter; private BigDecimal value; public FiatRate(String base, String counter, BigDecimal value) { this.base = base; this.counter = counter; this.value = value; } public String getBase() { return base; } public void setBase(String base) { this.base = base; } public String getCounter() { return counter; } public void setCounter(String counter) { this.counter = counter; } public BigDecimal getValue() { return value; } public void setValue(BigDecimal value) { this.value = value; } }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       Price.java ... Prices.java () ... Total.java ...
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      コントローラー
 最も単純なAPIを実装するコントローラーを作成し、指定されたコインの文字コードに従って暗号通貨の値を発行しようとしています。 
      
        
        
        
      
     つまり 
 GET /cryptonaut/restapi/prices.json?coins=BTC&coins=ETH&fiatCurrency=RUR
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      次のようなものを与える必要があります:
 {"prices":[{"coin":"BTC","value":407924.043300000000},{"coin":"ETH","value":13040.638266000000}],"fiatCurrency":"RUR"}
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
        ドキュメントによると、複雑なものはなく、同じSpring
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    アプローチと規則を思い出させます: 
 package com.room606.cryptonaut.rest; import com.room606.cryptonaut.domain.Price; import com.room606.cryptonaut.domain.Prices; import com.room606.cryptonaut.markets.FiatExchangeRatesService; import com.room606.cryptonaut.markets.CryptoMarketDataService; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.*; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @Controller("/cryptonaut/restapi/") public class MarketDataController { private final CryptoMarketDataService cryptoMarketDataService; private final FiatExchangeRatesService fiatExchangeRatesService; public MarketDataController(CryptoMarketDataService cryptoMarketDataService, FiatExchangeRatesService fiatExchangeRatesService) { this.cryptoMarketDataService = cryptoMarketDataService; this.fiatExchangeRatesService = fiatExchangeRatesService; } @Get("/prices.json") @Produces(MediaType.APPLICATION_JSON) public Prices pricesAsJson(@QueryValue("coins") String[] coins, @QueryValue("fiatCurrency") String fiatCurrency) { return getPrices(coins, fiatCurrency); } private Prices getPrices(String[] coins, String fiatCurrency) { List<Price> prices = Stream.of(coins) .map(coin -> new Price(coin, cryptoMarketDataService.getPrice(coin, fiatCurrency))) .collect(Collectors.toList()); return new Prices(prices, fiatCurrency); } }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       つまり 返される型としてPOJO
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    を静かに指定します。シリアライザー/デシリアライザーを構成せずに、追加の注釈をぶら下げなくても、Micronautはボックスからのデータを使用して正しいhttpボディを構築します。  Spring
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    方法と比較しましょう: 
 @RequestMapping(value = "/prices.json", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity<Prices> pricesAsJson(@RequestParam("userId") final String[] coins, @RequestParam("fiatCurrency") String fiatCurrency) {
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      一般的に、ドキュメントによると、コントローラーに問題はなく、期待どおりに機能していました。 スペルは直感的でシンプルでした。 先に進みます。
データストレージ層
アプリケーションの最初のバージョンでは、ユーザーのポートフォリオのみを保存します。 一般に、1人のユーザーの1つのポートフォリオのみを保持します。 簡単に言えば、まだ多くのユーザーのサポートはなく、暗号通貨のポートフォリオを持つメインユーザーは1人だけです。 これはすごい!
 データの永続性を実装するために、ドキュメントにはJPA接続のオプションと、さまざまなクライアントを使用してデータベースから読み取る断片的な例があります(「12.1.5 Postgresの設定」)。  JPA
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    断固として拒否され、クエリを自分の手で記述して操作することが優先されました。 ドキュメントによると、データベース構成がapplication.ymlに追加されました( Postgres
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    がRDBMSとして選択されました)。 
 postgres: reactive: client: port: 5432 host: localhost database: cryptonaut user: crypto password: r1ch13r1ch maxSize: 5
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
        postgres-reactive
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    ライブラリに応じて追加されました。 これは、非同期および同期の両方の方法でデータベースを操作するためのクライアントです。 
 <dependency> <groupId>io.micronaut.configuration</groupId> <artifactId>postgres-reactive</artifactId> <version>1.0.0.M4</version> <scope>compile</scope> </dependency>
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       そして最後に、 docker-compose.yml
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    ファイルが/ docker-compose.yml
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    に追加され、データベースコンポーネントが追加されたアプリケーションの将来の環境をデプロイしました。 
 db: image: postgres:9.6 restart: always environment: POSTGRES_USER: crypto POSTGRES_PASSWORD: r1ch13r1ch POSTGRES_DB: cryptonaut ports: - 5432:5432 volumes: - ${PWD}/../db/init_tables.sql:/docker-entrypoint-initdb.d/1.0.0_init_tables.sql
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      以下は、非常に単純なテーブル構造を持つデータベースの初期化スクリプトです。
 CREATE TABLE portfolio ( id serial CONSTRAINT coin_amt_primary_key PRIMARY KEY, coin varchar(16) NOT NULL UNIQUE, amount NUMERIC NOT NULL );
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      次に、ユーザーのポートフォリオを更新するコードをスローしてみましょう。 ポートフォリオコンポーネントは次のようになります。
 package com.room606.cryptonaut; import com.room606.cryptonaut.domain.Portfolio; import java.math.BigDecimal; import java.util.Optional; public interface PortfolioService { Portfolio savePortfolio(Portfolio portfolio); Portfolio loadPortfolio(); Optional<BigDecimal> calculateTotalValue(Portfolio portfolio, String fiatCurrency); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
        Postgres reactive client
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    メソッドのセットを見ると、このクラスがスローされています。 
 package com.room606.cryptonaut; import com.room606.cryptonaut.domain.Portfolio; import com.room606.cryptonaut.markets.CryptoMarketDataService; import io.micronaut.context.annotation.Requires; import io.reactiverse.pgclient.Numeric; import io.reactiverse.reactivex.pgclient.*; import javax.inject.Inject; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class PortfolioServiceImpl implements PortfolioService { private final PgPool pgPool; ... private static final String UPDATE_COIN_AMT = "INSERT INTO portfolio (coin, amount) VALUES (?, ?) ON CONFLICT (coin) " + "DO UPDATE SET amount = ?"; ... public Portfolio savePortfolio(Portfolio portfolio) { List<Tuple> records = portfolio.getCoins() .entrySet() .stream() .map(entry -> Tuple.of(entry.getKey(), Numeric.create(entry.getValue()), Numeric.create(entry.getValue()))) .collect(Collectors.toList()); pgPool.preparedBatch(UPDATE_COIN_AMT, records, pgRowSetAsyncResult -> { //   pgRowSetAsyncResult.cause().printStackTrace(); }); return portfolio; } ... }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      環境を立ち上げ、事前に慎重に実装されたAPIを通じてポートフォリオを更新しようとします。
 package com.room606.cryptonaut.rest; import com.room606.cryptonaut.PortfolioService; import com.room606.cryptonaut.domain.Portfolio; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.*; import javax.inject.Inject; @Controller("/cryptonaut/restapi/") public class ConfigController { @Inject private PortfolioService portfolioService; @Post("/portfolio") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Portfolio savePortfolio(@Body Portfolio portfolio) { return portfolioService.savePortfolio(portfolio); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
        curl
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    要求を実行します。 
 curl http://localhost:8080/cryptonaut/restapi/portfolio -X POST -H "Content-Type: application/json" --data '{"coins": {"XRP": "35.5", "LSK": "5.03", "XEM": "16.23"}}' -v
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      そして...ログでエラーをキャッチします。
 io.reactiverse.pgclient.PgException: syntax error at or near "," at io.reactiverse.pgclient.impl.PrepareStatementCommand.handleErrorResponse(PrepareStatementCommand.java:74) at io.reactiverse.pgclient.impl.codec.decoder.MessageDecoder.decodeError(MessageDecoder.java:250) at io.reactiverse.pgclient.impl.codec.decoder.MessageDecoder.decodeMessage(MessageDecoder.java:139) ...
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       カブをひっかいた後、公式のドックには解決策が見つかりませんpostgres-reactive
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    でドックをグーグルしようとします。これは正しい例であり、例と正しいクエリ構文が詳細に示されています。 これはプレースホルダーパラメーターの問題であり、 $x ($1, $2, etc.)
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    という形式の番号付きラベルを使用する必要があることがわかりました。 そのため、修正はターゲットリクエストを書き換えることです。 
 private static final String UPDATE_COIN_AMT = "INSERT INTO portfolio (coin, amount) VALUES ($1, $2) ON CONFLICT (coin) " + "DO UPDATE SET amount = $3";
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       アプリケーションを再起動し、同じREST
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    リクエストを試してみてください... データが加算されます。 読書に移りましょう。 
データベースからユーザーの暗号通貨のポートフォリオを読み取り、それらをPOJOオブジェクトにマッピングするという最も単純なタスクに直面しています。 これらの目的のために、pgPool.queryメソッド(SELECT_COINS_AMTS、pgRowSetAsyncResult)を使用します。
 public Portfolio loadPortfolio() { Map<String, BigDecimal> coins = new HashMap<>(); pgPool.query(SELECT_COINS_AMTS, pgRowSetAsyncResult -> { if (pgRowSetAsyncResult.succeeded()) { PgRowSet rows = pgRowSetAsyncResult.result(); PgIterator pgIterator = rows.iterator(); while (pgIterator.hasNext()) { Row row = pgIterator.next(); coins.put(row.getString("coin"), new BigDecimal(row.getFloat("amount"))); } } else { System.out.println("Failure: " + pgRowSetAsyncResult.cause().getMessage()); } }); Portfolio portfolio = new Portfolio(); portfolio.setCoins(coins); return portfolio; }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      これらすべてを、暗号通貨ポートフォリオを担当するコントローラーと接続します。
 @Controller("/cryptonaut/restapi/") public class ConfigController { ... @Get("/portfolio") @Produces(MediaType.APPLICATION_JSON) public Portfolio loadPortfolio() { return portfolioService.loadPortfolio(); } ...
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      サービスを再起動します。 テストのために、最初にこのポートフォリオを少なくともいくつかのデータで満たします。
 curl http://localhost:8080/cryptonaut/restapi/portfolio -X POST -H "Content-Type: application/json" --data '{"coins": {"XRP": "35.5", "LSK": "5.03", "XEM": "16.23"}}' -v
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      最後に、データベースからのコード読み取りをテストします。
 curl http://localhost:8080/cryptonaut/restapi/portfolio -v
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      そして...私たちは...奇妙な何かを得ます:
 {"coins":{}}
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       かなり奇妙ですね。 リクエストを10回再確認し、 curl
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    リクエストを再試行し、サービスを再起動します。 結果は同じままです...メソッドのシグネチャを読んで、 Reactive Pg client
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    があることを思い出した後、非同期化を扱っているという結論に達します。 思慮深いデバッグはこれを確認しました! 出来上がりのように、ゆっくりとデコードするだけの価値がありました。空ではないデータが得られました! 
再びライブラリドックに戻り、袖をまくり、真にブロックするが完全に予測可能なコードでコードを書き直します。
 Map<String, BigDecimal> coins = new HashMap<>(); PgIterator pgIterator = pgPool.rxPreparedQuery(SELECT_COINS_AMTS).blockingGet().iterator(); while (pgIterator.hasNext()) { Row row = pgIterator.next(); coins.put(row.getString("coin"), new BigDecimal(row.getValue("amount").toString())); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      これで、期待どおりの結果が得られました。 この問題を解決しました。
市場データを取得するクライアントを作成します
ここで、もちろん、私は最小数の自転車で問題を解決したいと思います。 その結果、2つのソリューションが得られます。
- 特定の暗号交換にアクセスするための既製のクライアントライブラリ
- 為替レートを申請するための小さな顧客独自のコード。 箱から出てきたのはマイクロノートです。
既製のライブラリでは、すべてがそれほど面白くありません。 クイック検索中に、プロジェクトhttps://github.com/knowm/XChangeが選択されたことにのみ注意してください 。
 原則として、ライブラリアーキテクチャはわずか3ペニーです-データを受信するためのインターフェイスのセット、メインインターフェイス、およびTicker
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     ( bid
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     、 ask
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     、あらゆる種類の始値、終値などを見つけることができます)、 CurrencyPair
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     、 Currency
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    などのモデルクラスがあります。 さらに、特定の暗号交換を参照する実装に依存関係を以前に接続して、コード内で実装自体を初期化します。 そして、私たちが行動する主なクラスはMarketDataService.java
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
たとえば、私たちの実験では、まず最初に、このような「構成」に満足しています。
 <dependency> <groupId>org.knowm.xchange</groupId> <artifactId>xchange-core</artifactId> <version>4.3.10</version> </dependency> <dependency> <groupId>org.knowm.xchange</groupId> <artifactId>xchange-bittrex</artifactId> <version>4.3.10</version> </dependency>
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      以下は、重要な機能を実行するコードです。特定の暗号通貨のコストを定額で計算します(要件ブロックの記事の冒頭に記載されている式を参照)。
 package com.room606.cryptonaut.markets; import com.room606.cryptonaut.exceptions.CryptonautException; import org.knowm.xchange.currency.Currency; import org.knowm.xchange.currency.CurrencyPair; import org.knowm.xchange.dto.marketdata.Ticker; import org.knowm.xchange.exceptions.CurrencyPairNotValidException; import org.knowm.xchange.service.marketdata.MarketDataService; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; import java.math.BigDecimal; @Singleton public class CryptoMarketDataService { private final FiatExchangeRatesService fiatExchangeRatesService; private final MarketDataService marketDataService; @Inject public CryptoMarketDataService(FiatExchangeRatesService fiatExchangeRatesService, MarketDataServiceFactory marketDataServiceFactory) { this.fiatExchangeRatesService = fiatExchangeRatesService; this.marketDataService = marketDataServiceFactory.getMarketDataService(); } public BigDecimal getPrice(String coinCode, String fiatCurrencyCode) throws CryptonautException { BigDecimal price = getPriceForBasicCurrency(coinCode, Currency.USD.getCurrencyCode()); if (Currency.USD.equals(new Currency(fiatCurrencyCode))) { return price; } else { return price.multiply(fiatExchangeRatesService.getFiatPrice(Currency.USD.getCurrencyCode(), fiatCurrencyCode)); } } private BigDecimal getPriceForBasicCurrency(String coinCode, String fiatCurrencyCode) throws CryptonautException { Ticker ticker = null; try { ticker = marketDataService.getTicker(new CurrencyPair(new Currency(coinCode), new Currency(fiatCurrencyCode))); return ticker.getBid(); } catch (CurrencyPairNotValidException e) { ticker = getTicker(new Currency(coinCode), Currency.BTC); Ticker ticker2 = getTicker(Currency.BTC, new Currency(fiatCurrencyCode)); return ticker.getBid().multiply(ticker2.getBid()); } catch (IOException e) { throw new CryptonautException("Failed to get price for Pair " + coinCode + "/" + fiatCurrencyCode + ": " + e.getMessage(), e); } } private Ticker getTicker(Currency base, Currency counter) throws CryptonautException { try { return marketDataService.getTicker(new CurrencyPair(base, counter)); } catch (CurrencyPairNotValidException | IOException e) { throw new CryptonautException("Failed to get price for Pair " + base.getCurrencyCode() + "/" + counter.getCurrencyCode() + ": " + e.getMessage(), e); } } }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      プロジェクトhttps://github.com/knowm/XChangeによって提供される特定の実装をわずかに無視するために、私たち自身のインターフェースを使用して可能な範囲ですべてが行われました。
すべてではありませんが、多くの暗号通貨交換では、流通しているフィアット通貨の限られたセット(USD、EUR、おそらくそれだけです..)があるという事実を考慮して、ユーザーの質問に対する最終的な回答のために、別のデータソースを追加する必要があります-また、オプションのコンバーター。 つまり RURでのWTF暗号通貨のコスト(ターゲット通貨、ターゲット通貨)の質問に答えるには、2つのサブ質問WTF / BaseCurrency(米ドルと見なします)、BaseCurrency / RURに答えてから、これら2つの値を掛けて結果を生成する必要があります。
 サービスの最初のバージョンでは、ターゲット通貨としてUSDとRURのみをサポートします。 
      
        
        
        
      
     したがって、RURをサポートするには、サービスの地理的位置に関連するソースを取得することをお勧めします(ロシアでのみホストして使用します)。 要するに、中央銀行のレートが私たちに合っています。 このようなデータのオープンソースはインターネット上で見つかり、JSONとして使用できます。 素晴らしい。 
以下は、現在の為替レート要求に対するサービスの応答です。
 { "Date": "2018-10-16T11:30:00+03:00", "PreviousDate": "2018-10-13T11:30:00+03:00", "PreviousURL": "\/\/www.cbr-xml-daily.ru\/archive\/2018\/10\/13\/daily_json.js", "Timestamp": "2018-10-15T23:00:00+03:00", "Valute": { "AUD": { "ID": "R01010", "NumCode": "036", "CharCode": "AUD", "Nominal": 1, "Name": "ђІЃ‚Ђ°»№Ѓє№ ґѕ»»°Ђ", "Value": 46.8672, "Previous": 46.9677 }, "AZN": { "ID": "R01020A", "NumCode": "944", "CharCode": "AZN", "Nominal": 1, "Name": "ђ·µЂ±°№ґ¶°ЅЃє№ ј°Ѕ°‚", "Value": 38.7567, "Previous": 38.8889 }, "GBP": { "ID": "R01035", "NumCode": "826", "CharCode": "GBP", "Nominal": 1, "Name": "¤ѓЅ‚ Ѓ‚µЂ»ЅіѕІ ЎѕµґЅµЅЅѕіѕ єѕЂѕ»µІЃ‚І°", "Value": 86.2716, "Previous": 87.2059 }, ...
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       実際、以下はCbrExchangeRatesClient
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    クライアントCbrExchangeRatesClient
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     。 
 package com.room606.cryptonaut.markets.clients; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.room606.cryptonaut.exceptions.CryptonautException; import io.micronaut.http.HttpRequest; import io.micronaut.http.client.Client; import io.micronaut.http.client.RxHttpClient; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; import java.math.BigDecimal; import java.util.*; @Singleton public class CbrExchangeRatesClient { private static final String CBR_DATA_URI = "https://www.cbr-xml-daily.ru/daily_json.js"; @Client(CBR_DATA_URI) @Inject private RxHttpClient httpClient; private final ObjectReader objectReader = new ObjectMapper().reader(); public Map<String, BigDecimal> getRates() { try { //return ratesCache.get("fiatRates"); HttpRequest<?> req = HttpRequest.GET(""); String response = httpClient.retrieve(req, String.class).blockingSingle(); JsonNode json = objectReader.readTree(response); String usdPrice = json.get("Valute").get("USD").get("Value").asText(); String eurPrice = json.get("Valute").get("EUR").get("Value").asText(); String gbpPrice = json.get("Valute").get("GBP").get("Value").asText(); Map<String, BigDecimal> prices = new HashMap<>(); prices.put("USD", new BigDecimal(usdPrice)); prices.put("GBP", new BigDecimal(gbpPrice)); prices.put("EUR", new BigDecimal(eurPrice)); return prices; } catch (IOException e) { throw new CryptonautException("Failed to obtain exchange rates: " + e.getMessage(), e); } } }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       ここで、 Micronaut
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    コンポーネントであるMicronaut
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    を注入しMicronaut
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     。 また、非同期のリクエスト処理またはブロックを行う選択もできます。 クラシックブロッキングを選択します。 
 httpClient.retrieve(req, String.class).blockingSingle();
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      構成
プロジェクトでは、ビジネスロジックまたは特定の側面に変化を与え、影響を与えるものを強調表示できます。 サポートされているフィアット通貨のリストをプロパティとして作成し、アプリケーションの開始時に挿入しましょう。
次のコードは、ポートフォリオの価値をまだ計算できない通貨コードを破棄します。
 public BigDecimal getFiatPrice(String baseCurrency, String counterCurrency) throws NotSupportedFiatException { if (!supportedCounterCurrencies.contains(counterCurrency)) { throw new NotSupportedFiatException("Counter currency not supported: " + counterCurrency); } Map<String, BigDecimal> rates = cbrExchangeRatesClient.getRates(); return rates.get(baseCurrency); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       したがって、私たちの意図は、 application.yml
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    の値を何らかの方法でsupportedCounterCurrencies
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    変数に注入することです。 
最初のバージョンでは、このようなコードは、FiatExchangeRatesService.javaクラスのフィールドの下に記述されていました。
 @Value("${cryptonaut.currencies:RUR}") private String supportedCurrencies; private final List<String> supportedCounterCurrencies = Arrays.asList(supportedCurrencies.split("[,]", -1));
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       ここで、 placeholder
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    はapplication.yml
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    ドキュメントの次の構造に対応します。 
 micronaut: application: name: cryptonaut #Uncomment to set server port server: port: 8080 postgres: reactive: client: port: 5432 host: localhost database: cryptonaut user: crypto password: r1ch13r1ch maxSize: 5 # app / business logic specific properties cryptonaut: currencies: "RUR"
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      アプリケーションの起動、クイックスモークテスト...エラー!
 Caused by: io.micronaut.context.exceptions.BeanInstantiationException: Error instantiating bean of type [com.room606.cryptonaut.markets.CryptoMarketDataService] Path Taken: new MarketDataController([CryptoMarketDataService cryptoMarketDataService],FiatExchangeRatesService fiatExchangeRatesService) --> new CryptoMarketDataService([FiatExchangeRatesService fiatExchangeRatesService],MarketDataServiceFactory marketDataServiceFactory) at io.micronaut.context.DefaultBeanContext.doCreateBean(DefaultBeanContext.java:1266) at io.micronaut.context.DefaultBeanContext.createAndRegisterSingleton(DefaultBeanContext.java:1677) at io.micronaut.context.DefaultBeanContext.getBeanForDefinition(DefaultBeanContext.java:1447) at io.micronaut.context.DefaultBeanContext.getBeanInternal(DefaultBeanContext.java:1427) at io.micronaut.context.DefaultBeanContext.getBean(DefaultBeanContext.java:852) at io.micronaut.context.AbstractBeanDefinition.getBeanForConstructorArgument(AbstractBeanDefinition.java:943) ... 36 common frames omitted Caused by: java.lang.NullPointerException: null at com.room606.cryptonaut.markets.FiatExchangeRatesService.<init>(FiatExchangeRatesService.java:20) at com.room606.cryptonaut.markets.$FiatExchangeRatesServiceDefinition.build(Unknown Source) at io.micronaut.context.DefaultBeanContext.doCreateBean(DefaultBeanContext.java:1252) ... 41 common frames omitted
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
          Micronaut
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
        Spring
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
            ,        compile time
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     .       ,     : 
 @Value("${cryptonaut.currencies:RUR}") private String supportedCurrencies; private List<String> supportedCounterCurrencies; @PostConstruct void init() { supportedCounterCurrencies = Arrays.asList(supportedCurrencies.split("[,]", -1)); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       ,    – javax.annotation.PostConstruct
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     ,        ,    ,     ,        .     . 
  , ,        Spring.    micronaut    @Property
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
           Map<String, String>
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     ,  @Configuration
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
        , Random Properties
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     (,      ID
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      , ,   -   )    PropertySourceLoader
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     , ..   .    Spring
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     –   ApplicationContext
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     ( xml
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     , web
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     , groovy
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     , ClassPath
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     etc.) ,              . 
テスト
       ,   micronaut.   Embedded Server feature,       Groovy
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
        Spock
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     .      Java
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     ,  groovy-   . ,    EmbeddedServer
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     + HttpClient
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
       Micronaut
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
          API — 
 GET /cryptonaut/restapi/portfolio/total.json?fiatCurrency={x}
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      API, .
:
 public class PortfolioReportsControllerTest { private static EmbeddedServer server; private static HttpClient client; @Inject private PortfolioService portfolioService; @BeforeClass public static void setupServer() { server = ApplicationContext.run(EmbeddedServer.class); client = server .getApplicationContext() .createBean(HttpClient.class, server.getURL()); } @AfterClass public static void stopServer() { if(server != null) { server.stop(); } if(client != null) { client.stop(); } } @Test public void total() { //TODO: Seems like code smell. I don't like it.. portfolioService = server.getApplicationContext().getBean(PortfolioService.class); Portfolio portfolio = new Portfolio(); Map<String, BigDecimal> coins = new HashMap<>(); BigDecimal amt1 = new BigDecimal("570.05"); BigDecimal amt2 = new BigDecimal("2.5"); coins.put("XRP", amt1); coins.put("QTUM", amt2); portfolio.setCoins(coins); portfolioService.savePortfolio(portfolio); HttpRequest request = HttpRequest.GET("/cryptonaut/restapi/portfolio/total.json?fiatCurrency=USD"); HttpResponse<Total> rsp = client.toBlocking().exchange(request, Total.class); assertEquals(200, rsp.status().getCode()); assertEquals(MediaType.APPLICATION_JSON_TYPE, rsp.getContentType().get()); Total val = rsp.body(); assertEquals("USD", val.getFiatCurrency()); assertEquals(TEST_VALUE.toString(), val.getValue().toString()); assertEquals(amt1.toString(), val.getPortfolio().getCoins().get("XRP").toString()); assertEquals(amt2.toString(), val.getPortfolio().getCoins().get("QTUM").toString()); } }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
            ,       mock   PortfolioService.java
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     : 
 package com.room606.cryptonaut; import com.room606.cryptonaut.domain.Portfolio; import io.micronaut.context.annotation.Requires; import javax.inject.Singleton; import java.math.BigDecimal; import java.util.Optional; @Singleton @Requires(env="test") public class MockPortfolioService implements PortfolioService { private Portfolio portfolio; public static final BigDecimal TEST_VALUE = new BigDecimal("56.65"); @Override public Portfolio savePortfolio(Portfolio portfolio) { this.portfolio = portfolio; return portfolio; } @Override public Portfolio loadPortfolio() { return portfolio; } @Override public Optional<BigDecimal> calculateTotalValue(Portfolio portfolio, String fiatCurrency) { return Optional.of(TEST_VALUE); } }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
             @Requires(env="test")
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     ,        Application Context
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      . -,     micronaut     test,     ,        . ,            ,   PortfolioServiceImpl
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
         @Requires(notEnv="test")
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     .               –      .  Micronaut
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
                . 
  ,   –   ,     ,   –        mockito
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     .       : 
 @Test public void priceForUsdDirectRate() throws IOException { when(marketDataServiceFactory.getMarketDataService()).thenReturn(marketDataService); String coinCode = "ETH"; String fiatCurrencyCode = "USD"; BigDecimal priceA = new BigDecimal("218.58"); Ticker targetTicker = new Ticker.Builder().bid(priceA).build(); when(marketDataService.getTicker(new CurrencyPair(new Currency(coinCode), new Currency(fiatCurrencyCode)))).thenReturn(targetTicker); CryptoMarketDataService cryptoMarketDataService = new CryptoMarketDataService(fiatExchangeRatesService, marketDataServiceFactory); assertEquals(priceA, cryptoMarketDataService.getPrice(coinCode, fiatCurrencyCode)); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      キャッシング
  ,       .            .    ,          . ,         ,      -   IP.     ,            @Cacheable
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
        . 
      
        
        
        
      
    
 
      ただし、ここではすべてが完全に失敗しました。この側面のドキュメントは紛らわしく、いくつかの画面をスクロールした後、互いに矛盾する設定(
appliction.yml
      
      )を見つけます。キャッシュとしてredisが選択され、その横にあるDockerコンテナーで持ち上げられました。その構成は次のとおりです。
 redis: image: 'bitnami/redis:latest' environment: - ALLOW_EMPTY_PASSWORD=yes ports: - '6379:6379'
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      そして、@ Cacheableによって注釈が付けられたコードの一部を次に示します。
 @Cacheable("fiatRates") public Map<String, BigDecimal> getRates() { HttpRequest<?> req = HttpRequest.GET(""); String response = httpClient.retrieve(req, String.class).blockingSingle(); try { JsonNode json = objectReader.readTree(response); String usdPrice = json.get("Valute").get("USD").get("Value").asText(); String eurPrice = json.get("Valute").get("EUR").get("Value").asText(); String gbpPrice = json.get("Valute").get("GBP").get("Value").asText(); Map<String, BigDecimal> prices = new HashMap<>(); prices.put("USD", new BigDecimal(usdPrice)); prices.put("GBP", new BigDecimal(gbpPrice)); prices.put("EUR", new BigDecimal(eurPrice)); return prices; } catch (IOException e) { throw new RuntimeException(e); } }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      しかしapplication.yml
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    、最も重要な謎がありました。あらゆる種類の構成を試しました。以下がその1つです。
 caches: fiatrates: expireAfterWrite: "1h" redis: caches: fiatRates: expireAfterWrite: "1h" port: 6379 server: localhost
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      以下がその1つです。
 #cache redis: uri: localhost:6379 caches: fiatRates: expireAfterWrite: "1h"
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      さらに、キャッシュ名の大文字を削除しようとしました。しかし、その結果、アプリケーションの起動時に同じ結果が得られました-「予期しないエラーが発生しました:名前にキャッシュが構成されていません:fiatRates」:
 ERROR imhsnetty.RoutingInBoundHandler - Unexpected error occurred: No cache configured for name: fiatRates io.micronaut.context.exceptions.ConfigurationException: No cache configured for name: fiatRates at io.micronaut.cache.DefaultCacheManager.getCache(DefaultCacheManager.java:67) at io.micronaut.cache.interceptor.CacheInterceptor.interceptSync(CacheInterceptor.java:176) at io.micronaut.cache.interceptor.CacheInterceptor.intercept(CacheInterceptor.java:128) at io.micronaut.aop.MethodInterceptor.intercept(MethodInterceptor.java:41) at io.micronaut.aop.chain.InterceptorChain.proceed(InterceptorChain.java:147) at com.room606.cryptonaut.markets.clients.$CbrExchangeRatesClientDefinition$Intercepted.getRates(Unknown Source) at com.room606.cryptonaut.markets.FiatExchangeRatesService.getFiatPrice(FiatExchangeRatesService.java:30) at com.room606.cryptonaut.rest.MarketDataController.index(MarketDataController.java:34) at com.room606.cryptonaut.rest.$MarketDataControllerDefinition$$exec2.invokeInternal(Unknown ...
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
           GitHub
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     -   SO
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
        .          .  , .          ,       .   boilerplate-,    -  Redis
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     -   ,    ,   Spring Boot  ,         . 
          ,      Micronaut
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     –    ,             Spring-. 
      
        
        
        
      
    
 
      ここでは、もちろん、12の免責事項を示す必要があります。私は、ベンチマークの専門家ではなく、開始時間の開始および測定方法、実験条件(マシン負荷、ハードウェア構成、OSなど)についてです。
ただし、最後のポイント:
 OS: 16.04.1-Ubuntu x86_64 x86_64 x86_64 GNU / Linux 
      
        
        
        
      
     CPU: Intel®Core(TM)i7-7700HQ CPU @ 2.80GHz 
      
        
        
        
      
     Mem: 2 x 8 Gb DDR4、速度:2400 MHz 
      
        
        
        
      
     SSDディスク: PCIe NVMe M SSD 2、256 GB
 私の  防衛  テクニック: 
- 手押し車を利用する
- 車の電源を入れます
- アプリケーション開始
- これと並行して、ループ内のクライアントコードは1つのAPIをポーリングします。これは、応答で1行を返すだけです
- APIからの応答を受信するとすぐに、「タイマー」が停止します。
- 結果はミリ秒単位でタブレットに慎重に入力されます
             –  Rest Controller
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
        –  IoC-,     . 
“ ” :
| Micronaut | Spring Boot | |
|---|---|---|
| Avg.(ms) | 2708.4 | 2735.2 | 
| cryptonaut (ms) | 1082 | - | 
  ,         –  27    Micronaut
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     . ,               . 
?
   .  ,   , ,      –  .            .      Groovy-,    ,    .     SO
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
         Spring.  ,  ,                .         —       .           Spring. 
:
- Micronaut – service-discovery, AWS
- Java. Kotlin Groovy.