Essayez Micronaut ou Darling, j'ai réduit le cadre

Essayez Micronaut ou Darling, j'ai réduit le cadre


À propos du framework micronaut, j'ai aperçu le résumé de la newsletter. Il se demandait quel genre de bête il était. Le cadre est mis en contraste avec le ressort bourré de tous les outils nécessaires.


Micronaut


Anticipant la prochaine conférence des développeurs, où ils ne feront que dire et montrer comment utiliser le micronaut dans vos microservices, j'ai décidé de me préparer au moins une fois et de me présenter avec au moins un certain contexte dans ma tête, avec un certain ensemble de problèmes et de questions. Faites vos devoirs pour ainsi dire. J'ai décidé de faire un petit projet pour animaux de compagnie pour quelques soirées (comme ça va). À la fin de l'article, il y aura un lien vers le référentiel de toutes les sources du projet.


Micronaut est un framework JVM qui prend en charge trois langages de développement: Java, Kotlin, Groovy. Il a été développé par OCI, la même entreprise que Grails nous a donnée. Il a un réglage sous la forme d'une application cli et d'un ensemble de bibliothèques recommandées (divers réactifs-http et clients de base de données)

Il existe une DI qui implémente et répète les idées de Spring, en ajoutant un certain nombre de ses puces - asynchronisme, prise en charge d'AWS Lambda, équilibrage de charge côté client.

L'idée du service: un de mes amis à un moment avec un imbécile a acheté une demi-douzaine de toutes sortes de crypto-monnaies différentes, en y investissant des vacances imprégnées et une cachette d'une veste d'hiver. Nous savons tous que la volatilité de toute cette substance de crypto-monnaie est sauvage et que le sujet lui-même est généralement imprévisible, un ami a finalement décidé de prendre soin de ses nerfs et de simplement se plonger dans ce qui se passe avec ses «actifs». Mais parfois, vous voulez toujours regarder, mais qu'y a-t-il avec tout cela, tout à coup c'est déjà riche. L'idée d'un panneau simple (un tableau de bord, comme Grafana ou quelque chose de plus simple), une certaine page Web avec des informations sèches, combien cela coûte dans une monnaie fiduciaire (USD, RUR) est maintenant venue.


Avertissements


  1. Nous laisserons l'opportunité d'écrire notre propre solution par-dessus bord, nous avons juste besoin d'essayer le nouveau cadre sur quelque chose de plus rusé que HelloWorld.
  2. Algorithme de calcul, erreurs attendues, erreurs, etc. (au moins pour la première phase du produit), la validité du choix des échanges de crypto pour extraire des informations, le portefeuille de crypto «investissement» d'un ami sera également hors de question et ne fera pas l'objet de discussions ou d'une sorte d'analyse approfondie.

Donc, un petit ensemble d'exigences:


  1. Service Web (accès de l'extérieur, via http)
  2. Affichage d'une page dans un navigateur avec un résumé de la valeur totale du portefeuille de crypto-monnaie
  3. Possibilité de configurer le portefeuille (choisissez le format JSON pour charger et décharger la structure du portefeuille). Une certaine API REST pour mettre à jour un portefeuille et le charger, c'est-à-dire 2 API: pour sauvegarder / mettre à jour - POST, pour décharger - GET. La structure du portefeuille est essentiellement une plaque signalétique simple
    BTC –  0.00005 . XEM –  4.5 . ... 
  4. Nous prenons les données des cryptoexchanges et des sources de change (pour les monnaies fiduciaires)
  5. Règles de calcul de la valeur totale du portefeuille:
    Formules de calcul de la valeur totale du portefeuille


Bien sûr, tout ce qui est écrit au paragraphe 5 fait l'objet de différends et de doutes distincts, mais que ce soit le cas pour l'entreprise.


Début du projet


Nous allons donc sur le site officiel du framework et voyons comment nous pouvons commencer à nous développer. Le site officiel propose d'installer l'outil sdkman. Une pièce qui facilite le développement et la gestion de projets sur le cadre du micronaut (et d'autres autres dont, par exemple, Grails).


Le même gestionnaire de différents SDK

Une petite remarque: si vous venez de démarrer l'initialisation du projet sans aucune clé, le collecteur de gradles est sélectionné par défaut. Supprimez le dossier, réessayez, cette fois avec la clé:
 mn create-app com.room606.cryptonaut -b=maven 

Un point intéressant est que sdkman, comme Spring Tool Suite, vous propose au stade de la création d'un projet de définir les «cubes» que vous souhaitez utiliser au début. Je n'ai pas beaucoup expérimenté avec ça, je l'ai aussi créé avec un preset par défaut.


Enfin, nous ouvrons le projet dans Intellij Idea et admirons l'ensemble des sources et ressources et disques qui nous ont été fournis avec l'assistant de création du projet micronaut.


La structure du projet nu

Eye accroche à 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 

Eh bien, c'est amusant et louable. On nous a immédiatement fourni un outil pour sortir rapidement l'application vers l'environnement Prod / INT / QA / quelque soit l'environnement. Pour cela, un signe mental positif au projet.


Il suffit de collecter le projet par Maven, puis de collecter l'image Docker et de la publier dans votre registre Docker, ou d'exporter simplement l'image binaire en option vers votre système CI, c'est comme vous le souhaitez.


Dans le dossier des ressources, nous avons également préparé un blanc avec les paramètres de configuration d'application (analogue de application.properties dans Spring), ainsi qu'un fichier de configuration pour la bibliothèque de journalisation. Cool!


Nous allons au point d'entrée de l'application et étudions la classe. Nous voyons une image douloureusement familière de Spring Boot. Ici, les développeurs du framework n'ont pas non plus commencé à inventer et à inventer quoi que ce soit.


 public static void main(String[] args) throws IOException { Micronaut.run(Application.class); } 

Comparez avec le code Spring familier.


 public static void main(String[] args) { SpringApplication.run(Application.class, args); } 

C'est-à-dire nous élevons également le conteneur IoC avec tous les beans inclus dans le travail au fur et à mesure de leurs besoins. Après avoir exécuté un peu selon la documentation officielle, nous commençons lentement le développement.


Nous aurons besoin de:


  1. Modèles de domaine
  2. Contrôleurs pour implémenter l'API REST.
  3. Couche de stockage de données (client de base de données ou ORM ou autre)
  4. Le code des consommateurs de données de crypto-échanges, ainsi que des données de l'échange de monnaies fiduciaires. C'est-à-dire nous devons écrire les clients les plus simples pour les services tiers. Au printemps, le RestTemplate bien connu convenait bien à ce rôle.
  5. Configuration minimale pour une gestion flexible et le démarrage de l'application (réfléchissons à quoi et comment nous allons faire la configuration)
  6. Des tests! Oui, afin de refacheter le code en toute confiance et en toute sécurité et d'implémenter de nouvelles fonctionnalités, nous devons être sûrs de la stabilité de l'ancien
  7. Mise en cache. Ce n'est pas une exigence de base, mais quelque chose qui serait bien d'avoir pour de bonnes performances, et dans notre scénario, il y a des endroits où la mise en cache est définitivement un bon outil.
    Spoiler: tout ira très mal ici.

Modèles de domaine


Pour nos besoins, les modèles suivants suffiront: les modèles d'un portefeuille de crypto-monnaies, le taux de change d'une paire de monnaies fiduciaires, les prix des crypto-monnaies en monnaie fiduciaire et la valeur totale du portefeuille.


Vous trouverez ci-dessous le code de seulement quelques modèles, le reste peut être consulté dans le référentiel . Et oui, j'étais trop paresseux pour visser Lombok dans ce projet.


 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 ... 

Contrôleurs


Nous essayons d'écrire un contrôleur qui implémente l'API la plus simple, émettant la valeur des crypto-monnaies selon les codes lettres donnés des pièces.
C'est-à-dire


 GET /cryptonaut/restapi/prices.json?coins=BTC&coins=ETH&fiatCurrency=RUR 

Devrait produire quelque chose comme:


 {"prices":[{"coin":"BTC","value":407924.043300000000},{"coin":"ETH","value":13040.638266000000}],"fiatCurrency":"RUR"} 

Selon la documentation , rien de compliqué et rappelle les mêmes approches et conventions 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); } } 

C'est-à-dire nous spécifions calmement notre POJO comme type retourné, et sans configurer aucun sérialiseur / désérialiseur, même sans suspendre des annotations supplémentaires, Micronaut construira le corps http correct avec les données de la boîte. Comparons avec Spring Way:


 @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) { 

En général, je n'ai eu aucun problème avec les contrôleurs, ils ont juste fonctionné comme prévu, selon la documentation. Leur orthographe était intuitive et simple. Nous continuons.


Couche de stockage des données


Pour la première version de l'application, nous ne stockerons que le portfolio de l'utilisateur. En général, nous ne conserverons qu'un seul portefeuille d'un utilisateur. Autrement dit, nous n'aurons pas encore le soutien de nombreux utilisateurs, un seul utilisateur principal avec son portefeuille de crypto-monnaies. C'est génial!


Pour implémenter la persistance des données, la documentation propose des options avec connexion JPA, ainsi que des exemples fragmentaires d'utilisation de différents clients pour lire à partir de la base de données (section «12.1.5 Configuration de Postgres»). JPA définitivement rejeté et la préférence a été donnée à l'écriture et à la manipulation des requêtes vous-même. La configuration de la base de données a été ajoutée à application.yml, ( Postgres été choisi comme SGBDR), selon la documentation:


 postgres: reactive: client: port: 5432 host: localhost database: cryptonaut user: crypto password: r1ch13r1ch maxSize: 5 

Selon la bibliothèque postgres-reactive été ajoutée. Il s'agit d'un client pour travailler avec la base de données à la fois de manière asynchrone et synchrone.


 <dependency> <groupId>io.micronaut.configuration</groupId> <artifactId>postgres-reactive</artifactId> <version>1.0.0.M4</version> <scope>compile</scope> </dependency> 

Et enfin, le fichier docker-compose.yml été ajouté au docker-compose.yml / docker-compose.yml pour déployer le futur environnement de notre application, où le composant de base de données a été ajouté:


 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 

Voici le script d'initialisation de la base de données avec une structure de table très simple:


 CREATE TABLE portfolio ( id serial CONSTRAINT coin_amt_primary_key PRIMARY KEY, coin varchar(16) NOT NULL UNIQUE, amount NUMERIC NOT NULL ); 

Essayons maintenant de lancer un code qui met à jour le portefeuille de l'utilisateur. Notre composante de portefeuille ressemblera à ceci:


 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); } 

En regardant l'ensemble des méthodes Postgres reactive client , nous lançons cette classe:


 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; } ... } 

Lance un environnement, nous essayons de mettre à jour notre portefeuille à travers une API prudemment implémentée à l'avance:


 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); } 

Exécutez une demande de 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 

Et ... attraper l'erreur dans les journaux:


 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) ... 

Après avoir gratté nos navets, nous ne trouvons aucune solution dans le dock officiel, nous essayons de google le dock sur la bibliothèque postgres-reactive elle-même, et cela s'avère être la bonne solution, car des exemples et la syntaxe de requête correcte sont donnés en détail. Il s'agissait de paramètres d'espace réservé, il s'avère que vous devez utiliser des étiquettes numérotées de la forme $x ($1, $2, etc.) . Ainsi, le correctif consiste à réécrire la demande cible:


 private static final String UPDATE_COIN_AMT = "INSERT INTO portfolio (coin, amount) VALUES ($1, $2) ON CONFLICT (coin) " + "DO UPDATE SET amount = $3"; 

Nous redémarrons l'application, essayons la même demande REST ... cheers. Les données sont additionnées. Passons à la lecture.


Nous sommes confrontés à la tâche la plus simple de lire un portefeuille de crypto-monnaies d'un utilisateur de la base de données et de les mapper à un objet POJO. À ces fins, nous utilisons la méthode 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; } 

Nous connectons tout cela avec le contrôleur responsable du portefeuille de crypto-monnaie:


 @Controller("/cryptonaut/restapi/") public class ConfigController { ... @Get("/portfolio") @Produces(MediaType.APPLICATION_JSON) public Portfolio loadPortfolio() { return portfolioService.loadPortfolio(); } ... 

Redémarrez le service. Pour les tests, nous remplissons d'abord ce portefeuille avec au moins quelques données:


 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 

Maintenant, testons enfin notre lecture de code dans la base de données:


 curl http://localhost:8080/cryptonaut/restapi/portfolio -v 

Et ... nous obtenons ... quelque chose d'étrange:


 {"coins":{}} 

Assez étrange, non? Nous revérifions la demande dix fois, essayons de refaire la demande de bouclage, voire redémarrons notre service. Le résultat est toujours le même sauvage ... Après avoir relu la signature de la méthode, et en se souvenant également que nous avons un Reactive Pg client , nous arrivons à la conclusion que nous avons affaire à l'asynchronisation. Un débag réfléchi l'a confirmé! Cela valait un peu de décodage, comme voila, nous avons des données non vides!


Revenant au dock de la bibliothèque, retroussant nos manches, nous réécrivons le code avec un vrai code de blocage, mais complètement prévisible:


 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())); } 

Maintenant, nous obtenons ce que nous attendons. Nous avons décidé de ce problème, passons.


Nous écrivons à un client pour obtenir des données de marché


Ici, bien sûr, je voudrais résoudre le problème avec le moins de vélos. Le résultat est deux solutions:


  • bibliothèques clientes prêtes à l'emploi pour accéder à des échanges cryptographiques spécifiques
  • un petit code client pour demander le taux de change. Ce qui est sorti de la boîte, c'est Micronaut.

Avec des bibliothèques prêtes à l'emploi, tout n'est pas si intéressant. Je note seulement que lors d'une recherche rapide, le projet https://github.com/knowm/XChange a été sélectionné.


En principe, l'architecture de la bibliothèque est aussi simple que trois sous - il existe un ensemble d'interfaces pour recevoir des données, les principales interfaces et les classes de modèle comme Ticker (vous pouvez trouver l' bid , la ask , toutes sortes de prix ouverts, les prix de clôture, etc.), CurrencyPair , Currency . De plus, vous initialisez les implémentations elles-mêmes dans le code, après avoir précédemment connecté la dépendance à l'implémentation qui fait référence à un crypto-échange spécifique. Et la classe principale à travers laquelle nous opérons est MarketDataService.java


Par exemple, pour nos expérimentations, pour commencer, nous sommes satisfaits d'une telle «configuration»:


 <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> 

Ci-dessous est le code qui remplit une fonction clé - calculer le coût d'une crypto-monnaie particulière en termes fiat (voir Formules décrites au début de l'article dans le bloc des exigences):


 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); } } } 

Tout a été fait dans la mesure du possible en utilisant nos propres interfaces afin d'ignorer légèrement les implémentations spécifiques fournies par le projet https://github.com/knowm/XChange .


Étant donné que sur de nombreux échanges de crypto-monnaies, sinon tous, il n'y a qu'un ensemble limité de monnaies fiduciaires en circulation (USD, EUR, peut-être que c'est tout ..), pour la réponse finale à la question de l'utilisateur, il est nécessaire d'ajouter une autre source de données - les taux de change fiat, et également un convertisseur en option. C'est-à-dire Pour répondre à la question, combien coûte la crypto-monnaie WTF en RUR (devise cible, devise cible) maintenant, vous devrez répondre à deux sous-questions: WTF / BaseCurrency (nous considérons l'USD comme tel), BaseCurrency / RUR, puis multipliez ces deux valeurs et donnez-les comme résultat.


Pour notre première version du service, nous ne prendrons en charge que l'USD et le RUR comme devises cibles.
Ainsi, pour soutenir RUR, il serait conseillé de prendre des sources qui sont pertinentes pour la localisation géographique du service (nous l'hébergerons et l'utiliserons exclusivement en Russie). En bref, le taux de la Banque centrale nous conviendra. Une source ouverte de ces données a été trouvée sur Internet, qui peut être consommée en JSON. Super.


Voici la réponse du service à la demande de taux de change actuelle:


 { "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 }, ... 

En fait, ci-dessous se trouve le CbrExchangeRatesClient client 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); } } } 

Ici, nous injectons RxHttpClient , un composant du Micronaut . Cela nous donne également le choix de faire le traitement ou le blocage des demandes asynchrones. Nous choisissons le blocage classique:


 httpClient.retrieve(req, String.class).blockingSingle(); 

La configuration


Dans un projet, vous pouvez mettre en évidence des éléments qui changent et affectent la logique métier ou certains aspects spécifiques. Faisons une liste des monnaies fiduciaires prises en charge en tant que propriété et injectons-la au début de l'application.


Le code suivant supprimera les codes de devise pour lesquels nous ne sommes pas encore en mesure de calculer la valeur du portefeuille:


 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); } 

En conséquence, notre intention est d'injecter en quelque sorte la valeur de application.yml dans la variable supportCounterCurrencies.


Dans la première version, un tel code a été écrit, sous le champ de la classe FiatExchangeRatesService.java:


 @Value("${cryptonaut.currencies:RUR}") private String supportedCurrencies; private final List<String> supportedCounterCurrencies = Arrays.asList(supportedCurrencies.split("[,]", -1)); 

Ici, l' placeholder correspond à la structure suivante du document 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" 

Lancement de l'application, test de fumée rapide ... Erreur!


 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 , .. . SpringApplicationContext ( 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 .


Cache

Cependant, tout a échoué ici. La documentation sous cet aspect est déroutante, où après avoir fait défiler quelques écrans, vous trouvez des morceaux de configurations qui se contredisent ( appliction.yml). En tant que cache, redis a été choisi, également levé dans le conteneur Docker à côté de lui. Voici sa configuration:
 redis: image: 'bitnami/redis:latest' environment: - ALLOW_EMPTY_PASSWORD=yes ports: - '6379:6379' 

Et voici un morceau de code annoté par @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); } } 

Mais application.ymlc'était avec le mystère le plus important. J'ai essayé toutes sortes de configurations. En voici un:


 caches: fiatrates: expireAfterWrite: "1h" redis: caches: fiatRates: expireAfterWrite: "1h" port: 6379 server: localhost 

En voici un:


 #cache redis: uri: localhost:6379 caches: fiatRates: expireAfterWrite: "1h" 

Et même essayé de supprimer les lettres majuscules dans le nom du cache. Mais en conséquence, j'ai obtenu le même résultat au démarrage de l'application - "Une erreur inattendue s'est produite: aucun cache configuré pour le nom: 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-.


Analyse comparative

Ici, bien sûr, vous devez indiquer avec une douzaine d'avertissements: que je ne suis pas un spécialiste de référence, sur la méthode de démarrage et de mesure de l'heure de début, sur les conditions de l'expérience (charge de la machine, configuration matérielle, OS, etc.).

Cependant, le dernier point:


Système d' exploitation: 16.04.1-Ubuntu x86_64 x86_64 x86_64
CPU GNU / Linux : Intel® Core (TM) i7-7700HQ CPU @ 2,80 GHz
Mem: 2 x 8 Go DDR4, vitesse: 2400 MHz
SSD Disk: PCIe NVMe M SSD 2, 256 Go


Le mien défense technique:


  1. Utiliser une brouette
  2. Allumez la voiture
  3. Début de l'application
  4. Parallèlement à cela, le code client dans une boucle interroge une API, qui renvoie simplement une ligne dans la réponse
  5. Dès qu'une réponse de l'API est reçue, le «timer» s'arrête.
  6. Le résultat en millisecondes est soigneusement saisi sur la tablette

Rest Controller – IoC-, .


“ ” :


MicronautSpring Boot
Avg.(ms)2708.42735.2
cryptonaut (ms)1082-

, – 27 Micronaut . , .


?


. , , , – . . Groovy-, , . SO Spring. , , . — . Spring.


:


  • Micronaut – service-discovery, AWS
  • Java. Kotlin Groovy.

.

Source: https://habr.com/ru/post/fr427595/


All Articles