Versuchen Sie Micronaut oder Darling, ich habe das Framework reduziert
Über das Mikronaut-Framework erhaschte ich einen Blick auf die Newsletter-Übersicht. Er fragte sich, was für ein Tier er war. Der Rahmen steht im Gegensatz zur Feder, die mit allen notwendigen Werkzeugen gefüllt ist.

Im Vorgriff auf die bevorstehende Konferenz für Entwickler, auf der sie nur darüber sprechen und zeigen, wie Mikronaut in Ihren Mikrodiensten verwendet wird, habe ich mich entschlossen, mich mindestens einmal fertig zu machen und zumindest einen gewissen Kontext mit einigen Problemen und Fragen im Kopf zu haben. Machen Sie sozusagen Hausaufgaben. Ich beschloss, ein paar Abende lang ein kleines Haustierprojekt zu machen (wie es geht). Am Ende des Artikels befindet sich ein Link zum Repository aller Projektquellen.
Micronaut ist ein JVM-Framework, das drei Entwicklungssprachen unterstützt: Java, Kotlin, Groovy. Es wurde von OCI entwickelt, der gleichen Firma, die Grails uns gegeben hat. Es verfügt über eine Optimierung in Form einer CLI-Anwendung und einer Reihe empfohlener Bibliotheken (verschiedene Reactive-http- und Datenbank-Clients).
Es gibt einen DI, der die Ideen von Spring implementiert und wiederholt und eine Reihe seiner Chips hinzufügt - Asynchronität, Unterstützung für AWS Lambda, Client Side Load Balancing.
Die Idee des Dienstes: Einer meiner Freunde kaufte einmal mit einem Narren ein halbes Dutzend verschiedenster Kryptowährungen und investierte in sie einen imprägnierten Urlaub und einen Vorrat aus einer Winterjacke. Wir alle wissen, dass die Volatilität all dieser Kryptowährungssubstanzen wild ist und das Thema selbst im Allgemeinen unvorhersehbar ist. Ein Freund entschied sich schließlich, sich um seine Nerven zu kümmern und einfach einzutauchen, was mit seinem „Vermögen“ passiert. Aber manchmal willst du immer noch schauen, aber was ist da mit all dem, plötzlich ist es schon reich. So entstand die Idee eines einfachen Panels (eines Dashboards wie Grafana oder etwas Einfacheres), einer bestimmten Webseite mit trockenen Informationen, wie viel das alles in einer Fiat-Währung (USD, RUR) kostet.
Haftungsausschluss
- Wir werden die Zweckmäßigkeit, unsere eigene Lösung zu schreiben, über Bord lassen. Wir müssen nur das neue Framework auf etwas schlauerem als HelloWorld ausprobieren.
- Berechnungsalgorithmus, erwartete Fehler, Fehler usw. (zumindest für die erste Phase des Produkts), die Gültigkeit der Wahl des Krypto-Austauschs zum Abrufen von Informationen, das "Investment" -Krypto-Portfolio eines Freundes, kommt ebenfalls nicht in Frage und wird weder diskutiert noch eingehend analysiert.
Also eine kleine Reihe von Anforderungen:
- Webdienst (Zugriff von außen über http)
- Anzeigen einer Seite in einem Browser mit einer Zusammenfassung des Gesamtwerts des Kryptowährungsportfolios
- Möglichkeit zum Konfigurieren des Portfolios (wählen Sie das JSON-Format zum Laden und Entladen der Portfoliostruktur). Eine bestimmte REST-API zum Aktualisieren und Laden eines Portfolios, d.h. 2 API: zum Speichern / Aktualisieren - POST, zum Entladen - GET. Die Portfoliostruktur ist im Wesentlichen ein einfaches Typenschild
BTC – 0.00005 . XEM – 4.5 . ...
- Wir beziehen Daten aus Krypto-Börsen und Geldwechselquellen (für Fiat-Währungen).
- Regeln zur Berechnung des Gesamtwerts des Portfolios:

Natürlich ist alles, was in Absatz 5 geschrieben steht, Gegenstand gesonderter Streitigkeiten und Zweifel, aber es sollte sein, dass das Unternehmen es so wollte.
Projektstart
Also gehen wir auf die offizielle Website des Frameworks und sehen, wie wir beginnen können, uns zu entwickeln. Die offizielle Seite bietet an, das sdkman-Tool zu installieren. Ein Stück, das die Entwicklung und Verwaltung von Projekten auf dem Mikronaut-Framework (und anderen anderen, einschließlich zum Beispiel Grails) erleichtert.

Eine kleine Bemerkung: Wenn Sie die Projektinitialisierung nur ohne Schlüssel starten, ist standardmäßig der Gradle-Kollektor ausgewählt. Löschen Sie den Ordner, versuchen Sie es erneut, diesmal mit dem Schlüssel:
mn create-app com.room606.cryptonaut -b=maven
Ein interessanter Punkt ist, dass sdkman Ihnen wie die Spring Tool Suite bereits in der Phase der Erstellung eines Projekts die Möglichkeit bietet, festzulegen, welche „Cubes“ Sie zu Beginn verwenden möchten. Ich habe nicht viel damit experimentiert, ich habe es auch mit einer Standardvoreinstellung erstellt.
Schließlich öffnen wir das Projekt in Intellij Idea und bewundern die Quellen, Ressourcen und Datenträger, die uns mit dem Assistenten zum Erstellen des Mikronaut-Projekts zur Verfügung gestellt wurden.

Auge klammert sich an 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
Nun, es macht Spaß und ist lobenswert. Wir erhielten sofort ein Tool zur schnellen Ausgabe der Anwendung an die Prod / INT / QA / egal-Umgebung. Dafür ein mentales Pluszeichen für das Projekt.
Es reicht aus, das Projekt von Maven zu sammeln, dann das Docker-Image zu sammeln und es in Ihrer Docker-Registrierung zu veröffentlichen oder einfach die Image-Binärdatei als Option in Ihr CI-System zu exportieren.
Im Ressourcenordner haben wir außerdem ein Leerzeichen mit Anwendungskonfigurationsparametern (analog zu application.properties in Spring) sowie eine Konfigurationsdatei für die Logback-Bibliothek vorbereitet. Cool!
Wir gehen zum Einstiegspunkt der Bewerbung und studieren die Klasse. Wir sehen ein Bild, das uns von Spring Boot schmerzlich vertraut ist. Auch hier haben die Entwickler des Frameworks nichts erfunden und erfunden.
public static void main(String[] args) throws IOException { Micronaut.run(Application.class); }
Vergleichen Sie mit dem bekannten Spring-Code.
public static void main(String[] args) { SpringApplication.run(Application.class, args); }
Das heißt, Wir erhöhen auch den IoC-Container mit allen Bohnen, die in der Arbeit enthalten sind, wenn sie benötigt werden. Nachdem wir gemäß der offiziellen Dokumentation ein wenig gelaufen sind, beginnen wir langsam mit der Entwicklung.
Wir werden brauchen:
- Domänenmodelle
- Controller zur Implementierung der REST-API.
- Datenspeicherschicht (Datenbankclient oder ORM oder etwas anderes)
- Ein Code für Verbraucher von Daten aus dem Austausch von Kryptowährungen sowie von Daten aus dem Austausch von Fiat-Währungen. Das heißt, Wir müssen die einfachsten Clients für Dienste von Drittanbietern schreiben. Im Frühjahr passte das bekannte RestTemplate gut zu dieser Rolle.
- Minimale Konfiguration für flexibles Management und Anwendungsstart (überlegen wir, was und wie wir die Konfiguration vornehmen)
- Tests! Ja, um Code sicher neu zu refachen und neue Funktionen zu implementieren, müssen wir uns der Stabilität der alten sicher sein
- Caching. Dies ist keine Grundvoraussetzung, aber etwas, das für eine gute Leistung gut wäre, und in unserem Szenario gibt es Orte, an denen das Caching definitiv ein gutes Werkzeug ist.
Spoiler: Hier wird alles sehr schlecht.
Domänenmodelle
Für unsere Zwecke reichen die folgenden Modelle aus: Modelle eines Kryptowährungsportfolios, der Wechselkurs eines Paares von Fiat-Währungen, Kryptowährungspreise in Fiat-Währung und der Gesamtwert des Portfolios.
Unten ist der Code nur einiger Modelle aufgeführt, der Rest kann im Repository angezeigt werden. Und ja, ich war zu faul, um Lombok
in diesem Projekt zu verarschen.
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 ...
Controller
Wir versuchen, einen Controller zu schreiben, der die einfachste API implementiert und den Wert von Kryptowährungen gemäß den angegebenen Buchstabencodes von Münzen ausgibt.
Das heißt,
GET /cryptonaut/restapi/prices.json?coins=BTC&coins=ETH&fiatCurrency=RUR
Sollte etwas produzieren wie:
{"prices":[{"coin":"BTC","value":407924.043300000000},{"coin":"ETH","value":13040.638266000000}],"fiatCurrency":"RUR"}
Laut Dokumentation ist nichts kompliziert und erinnert an die gleichen Spring
Ansätze und Konventionen:
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); } }
Das heißt, Wir geben unser POJO
ruhig als zurückgegebenen Typ an und ohne Serializer / Deserializer zu konfigurieren, auch ohne zusätzliche Anmerkungen aufzuhängen. Micronaut erstellt den richtigen http-Body mit den Daten aus der Box. Vergleichen wir mit 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) {
Im Allgemeinen hatte ich keine Probleme mit den Controllern, sie funktionierten laut Dokumentation nur wie erwartet. Ihre Schreibweise war intuitiv und einfach. Wir gehen weiter.
Datenspeicherschicht
Für die erste Version der Anwendung speichern wir nur das Portfolio des Benutzers. Im Allgemeinen behalten wir nur ein Portfolio eines Benutzers. Einfach ausgedrückt, wir werden noch nicht die Unterstützung vieler Benutzer haben, nur eines Hauptbenutzers mit seinem Portfolio an Kryptowährungen. Das ist großartig!
Um die Datenpersistenz zu implementieren, bietet die Dokumentation Optionen mit JPA-Verbindung sowie fragmentarische Beispiele für die Verwendung verschiedener Clients zum Lesen aus der Datenbank (Abschnitt „12.1.5 Konfigurieren von Postgres“). JPA
entschieden verworfen, und es wurde bevorzugt, Abfragen selbst zu schreiben und zu bearbeiten. Die Datenbankkonfiguration wurde zu application.yml hinzugefügt ( Postgres
wurde als RDBMS ausgewählt), gemäß der Dokumentation:
postgres: reactive: client: port: 5432 host: localhost database: cryptonaut user: crypto password: r1ch13r1ch maxSize: 5
Abhängig von der postgres-reactive
Bibliothek wurde hinzugefügt. Dies ist ein Client für die asynchrone und synchrone Arbeit mit der Datenbank.
<dependency> <groupId>io.micronaut.configuration</groupId> <artifactId>postgres-reactive</artifactId> <version>1.0.0.M4</version> <scope>compile</scope> </dependency>
Schließlich wurde die Datei docker-compose.yml
zum docker-compose.yml
/ docker-compose.yml
hinzugefügt, um die zukünftige Umgebung unserer Anwendung docker-compose.yml
, in der die Datenbankkomponente hinzugefügt wurde:
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
Unten finden Sie das Initialisierungsskript für die Datenbank mit einer sehr einfachen Tabellenstruktur:
CREATE TABLE portfolio ( id serial CONSTRAINT coin_amt_primary_key PRIMARY KEY, coin varchar(16) NOT NULL UNIQUE, amount NUMERIC NOT NULL );
Versuchen wir nun, einen Code zu werfen, der das Portfolio des Benutzers aktualisiert. Unsere Portfoliokomponente sieht folgendermaßen aus:
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); }
Mit Blick auf die Postgres reactive client
Methoden von Postgres reactive client
werfen wir diese Klasse:
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 -> {
Startet eine Umgebung, versuchen wir, unser Portfolio im Voraus über eine umsichtig implementierte API zu aktualisieren:
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); }
Führen Sie eine curl
Anfrage durch:
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
Und ... den Fehler in den Protokollen abfangen:
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) ...
Nachdem wir unsere Rüben zerkratzt haben, finden wir im offiziellen Dock keine Lösung. Wir versuchen, das Dock in der postgres-reactive
Bibliothek selbst zu googeln. Dies stellt sich als die richtige Lösung heraus, da Beispiele und die richtige Abfragesyntax im Detail angegeben sind. Es stellte sich heraus, dass es sich um Platzhalterparameter handelte. Sie müssen nummerierte Beschriftungen der Form $x ($1, $2, etc.)
. Die Lösung besteht also darin, die Zielanforderung neu zu schreiben:
private static final String UPDATE_COIN_AMT = "INSERT INTO portfolio (coin, amount) VALUES ($1, $2) ON CONFLICT (coin) " + "DO UPDATE SET amount = $3";
Wir starten die Anwendung neu, versuchen die gleiche REST
Anfrage ... Prost. Die Daten werden addiert. Fahren wir mit dem Lesen fort.
Wir stehen vor der einfachsten Aufgabe, ein Portfolio von Kryptowährungen eines Benutzers aus der Datenbank zu lesen und sie einem POJO-Objekt zuzuordnen. Für diese Zwecke verwenden wir die Methode 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; }
All dies verbinden wir mit dem Controller, der für das Kryptowährungsportfolio verantwortlich ist:
@Controller("/cryptonaut/restapi/") public class ConfigController { ... @Get("/portfolio") @Produces(MediaType.APPLICATION_JSON) public Portfolio loadPortfolio() { return portfolioService.loadPortfolio(); } ...
Starten Sie den Dienst neu. Zum Testen füllen wir zunächst genau dieses Portfolio mit mindestens einigen Daten:
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
Testen Sie nun endlich unseren Code aus der Datenbank:
curl http://localhost:8080/cryptonaut/restapi/portfolio -v
Und ... wir bekommen ... etwas Seltsames:
{"coins":{}}
Ziemlich seltsam, nicht wahr? Wir überprüfen die Anfrage zehnmal, versuchen erneut, die curl
Anfrage zu stellen, und starten sogar unseren Service neu. Das Ergebnis ist immer noch dasselbe ... Nachdem wir die Signatur der Methode erneut gelesen und uns daran erinnert haben, dass wir einen Reactive Pg client
, kommen wir zu dem Schluss, dass es sich um Asynchronisation handelt. Nachdenkliches Debag bestätigte dies! Es war ein wenig gemächliches De-Code wert, wie voila, wir haben nicht leere Daten!
Wenn wir uns wieder dem Bibliotheksdock zuwenden und die Ärmel hochkrempeln, schreiben wir den Code mit einem echten Blockierungscode neu, der jedoch vollständig vorhersehbar ist:
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())); }
Jetzt bekommen wir, was wir erwarten. Wir haben dieses Problem entschieden, fahren Sie fort.
Wir schreiben einen Kunden, um Marktdaten zu erhalten
Hier möchte ich natürlich das Problem mit der geringsten Anzahl von Fahrrädern lösen. Das Ergebnis sind zwei Lösungen:
- vorgefertigte Client-Bibliotheken für den Zugriff auf bestimmte Krypto-Austausche
- einen eigenen Kundencode für die Beantragung des Wechselkurses. Was aus der Box kam, ist Micronaut.
Mit vorgefertigten Bibliotheken ist nicht alles so interessant. Ich stelle nur fest, dass während einer Schnellsuche das Projekt https://github.com/knowm/XChange ausgewählt wurde.
Im Prinzip ist die Bibliotheksarchitektur so einfach wie drei Cent - es gibt eine Reihe von Schnittstellen zum Empfangen von Daten, die Hauptschnittstellen und Modellklassen wie Ticker
(Sie können bid
, ask
, alle Arten von Open Price, Close Price usw. herausfinden), CurrencyPair
, Currency
. Außerdem initialisieren Sie die Implementierungen selbst im Code, nachdem Sie zuvor die Abhängigkeit mit der Implementierung verbunden haben, die sich auf einen bestimmten Kryptoaustausch bezieht. Und die Hauptklasse, über die wir operieren, ist MarketDataService.java
Zum Beispiel sind wir für unsere Experimente zunächst einmal mit einer solchen „Konfiguration“ zufrieden:
<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>
Im Folgenden finden Sie den Code, der eine Schlüsselfunktion ausführt - die Berechnung der Kosten einer bestimmten Kryptowährung in Fiat-Begriffen (siehe Formeln, die am Anfang des Artikels im Anforderungsblock beschrieben sind):
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); } } }
Alles wurde so weit wie möglich über unsere eigenen Schnittstellen durchgeführt, um die spezifischen Implementierungen des Projekts https://github.com/knowm/XChange leicht zu ignorieren.
Angesichts der Tatsache, dass an vielen, wenn nicht allen Kryptowährungsbörsen nur ein begrenzter Satz von Fiat-Währungen im Umlauf ist (USD, EUR, vielleicht ist das alles ..), muss für die endgültige Antwort auf die Frage des Benutzers eine weitere Datenquelle hinzugefügt werden - Fiat-Wechselkurse und auch ein optionaler Konverter. Das heißt, Um die Frage zu beantworten, wie viel WTF-Kryptowährung jetzt in RUR (Zielwährung, Zielwährung) kostet, müssen Sie zwei Unterfragen beantworten: WTF / BaseCurrency (wir betrachten USD als solchen), BaseCurrency / RUR, multiplizieren Sie diese beiden Werte und geben Sie sie als Ergebnis an.
Für unsere erste Version des Dienstes unterstützen wir nur USD und RUR als Zielwährungen.
Um RUR zu unterstützen, wäre es daher ratsam, Quellen zu verwenden, die für den geografischen Standort des Dienstes relevant sind (wir werden ihn ausschließlich in Russland hosten und nutzen). Kurz gesagt, der Leitzins der Zentralbank wird zu uns passen. Im Internet wurde eine Open Source für solche Daten gefunden, die als JSON verwendet werden können. Großartig.
Nachfolgend finden Sie die Antwort des Dienstes auf die aktuelle Wechselkursanforderung:
{ "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 }, ...
Im Folgenden finden Sie den CbrExchangeRatesClient
Clientcode:
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); } } }
Hier injizieren wir RxHttpClient
, eine Komponente aus dem Micronaut
. Es gibt uns auch die Wahl, asynchrone Anforderungsverarbeitung oder Blockierung durchzuführen. Wir wählen die klassische Blockierung:
httpClient.retrieve(req, String.class).blockingSingle();
Konfiguration
In einem Projekt können Sie Dinge hervorheben, die sich ändern und die Geschäftslogik oder bestimmte Aspekte beeinflussen. Lassen Sie uns eine Liste der unterstützten Fiat-Währungen als Eigenschaft erstellen und diese zu Beginn der Anwendung einfügen.
Mit dem folgenden Code werden Währungscodes verworfen, für die wir den Portfoliowert noch nicht berechnen können:
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); }
Dementsprechend ist es unsere Absicht, den Wert aus application.yml
irgendwie in die Variable supportCounterCurrencies einzufügen.
In der ersten Version wurde ein solcher Code unter das Feld der Klasse FiatExchangeRatesService.java geschrieben:
@Value("${cryptonaut.currencies:RUR}") private String supportedCurrencies; private final List<String> supportedCounterCurrencies = Arrays.asList(supportedCurrencies.split("[,]", -1));
Hier entspricht der placeholder
der folgenden Struktur des Dokuments 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"
Anwendungsstart, schneller Rauchtest ... Fehler!
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() {
, 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
.
Hier war jedoch alles völlig erfolglos. Die Dokumentation in diesem Aspekt ist verwirrend. Nach dem Scrollen durch einige Bildschirme finden Sie Konfigurationen, die sich widersprechen ( appliction.yml
). Als Cache wurde Redis ausgewählt, das ebenfalls im Docker-Container daneben angehoben wurde. Hier ist seine Konfiguration: redis: image: 'bitnami/redis:latest' environment: - ALLOW_EMPTY_PASSWORD=yes ports: - '6379:6379'
Und hier ist ein Code, der von @Cacheable kommentiert wurde:
@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); } }
Aber mit application.yml
war das wichtigste Rätsel. Ich habe alle möglichen Konfigurationen ausprobiert. Hier ist einer:
caches: fiatrates: expireAfterWrite: "1h" redis: caches: fiatRates: expireAfterWrite: "1h" port: 6379 server: localhost
Hier ist einer:
#cache redis: uri: localhost:6379 caches: fiatRates: expireAfterWrite: "1h"
Und sogar versucht, die Großbuchstaben im Cache-Namen zu entfernen. Als Ergebnis habe ich beim Starten der Anwendung das gleiche Ergebnis erhalten: "Unerwarteter Fehler aufgetreten: Kein Cache für Name konfiguriert: 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-.
Hier sollten Sie natürlich mit einem Dutzend Haftungsausschlussklauseln angeben: Ich bin kein Benchmark-Spezialist, was die Methode zum Starten und Messen der Startzeit, die experimentellen Bedingungen (Maschinenlast, Hardwarekonfiguration, Betriebssystem usw.) betrifft.Der letzte Punkt:
Betriebssystem: 16.04.1-Ubuntu x86_64 x86_64 x86_64 GNU / Linux-
CPU: Intel® Core (TM) i7-7700HQ-CPU bei 2,80 GHz
Mem: 2 x 8 GB DDR4, Geschwindigkeit: 2400 MHz
SSD-Festplatte: PCIe NVMe M SSD 2, 256 GB
Meins Verteidigung Technik:
- Eine Schubkarre einlösen
- Schalten Sie das Auto ein
- Bewerbungsstart
- Parallel dazu fragt der Client-Code in einer Schleife eine API ab, die einfach eine Zeile in der Antwort zurückgibt
- Sobald eine Antwort von der API empfangen wird, stoppt der „Timer“.
- Das Ergebnis in Millisekunden wird sorgfältig auf dem Tablet eingegeben
– 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.
.