Coba Micronaut atau Sayang, saya mengurangi kerangka

Coba Micronaut atau Sayang, saya mengurangi kerangka


Tentang kerangka kerja mikronot, saya melihat sekilas intisari buletin. Dia bertanya-tanya seperti apa binatang itu. Kerangka kerja ini kontras dengan pegas yang diisi dengan semua alat yang diperlukan.


Micronaut


Mengantisipasi konferensi yang akan datang untuk para pengembang, di mana mereka hanya akan memberi tahu dan menunjukkan cara menggunakan micronaut di layanan-mikro Anda, saya memutuskan untuk mempersiapkan setidaknya satu kali dan datang dengan setidaknya beberapa konteks di kepala saya, dengan sejumlah masalah dan pertanyaan. Lakukan pekerjaan rumah sehingga untuk berbicara. Saya memutuskan untuk meluncurkan beberapa proyek hewan peliharaan kecil di beberapa malam (seperti kelanjutannya). Di akhir artikel akan ada tautan ke repositori semua sumber proyek.


Micronaut adalah kerangka kerja JVM yang mendukung tiga bahasa pengembangan: Java, Kotlin, Groovy. Ini dikembangkan oleh OCI, perusahaan yang sama dengan yang diberikan Grails kepada kami. Ini memiliki penyetelan dalam bentuk aplikasi cli dan satu set perpustakaan yang direkomendasikan (berbagai klien reaktif-http dan basis data)

Ada DI yang mengimplementasikan dan mengulangi ide Spring, menambahkan sejumlah chip - asinkron, dukungan untuk AWS Lambda, Client Side Load Balancing.

Gagasan layanan: salah satu teman saya pada suatu waktu dengan orang bodoh membeli setengah lusin segala macam cryptocurrency, berinvestasi dalam mereka liburan yang diresapi dan simpanan dari jaket musim dingin. Kita semua tahu bahwa volatilitas semua substansi cryptocurrency ini liar, dan topiknya sendiri pada umumnya tidak dapat diprediksi, seorang teman akhirnya memutuskan untuk mengurus sarafnya dan hanya tenggelam dalam apa yang terjadi dengan "aset" -nya. Tapi kadang-kadang Anda masih ingin melihat, tetapi apa yang ada dengan semua ini, tiba-tiba itu sudah kaya. Jadi ide panel sederhana (dashboard, seperti Grafana atau sesuatu yang lebih sederhana), halaman web tertentu dengan informasi kering, berapa banyak biaya dalam beberapa mata uang fiat (USD, RUR) sekarang muncul.


Penafian


  1. Kami akan meninggalkan kelayakan menulis solusi kami sendiri, kami hanya perlu mencoba kerangka kerja baru pada sesuatu yang lebih licik daripada HelloWorld.
  2. Algoritma perhitungan, kesalahan yang diharapkan, kesalahan, dll. (setidaknya untuk tahap pertama produk), validitas pilihan pertukaran kripto untuk menarik informasi, portofolio "kripto" investasi teman juga akan keluar dari pertanyaan dan tidak perlu didiskusikan atau semacam analisis mendalam.

Jadi, satu set kecil persyaratan:


  1. Layanan web (akses dari luar, melalui http)
  2. Menampilkan halaman di browser dengan ringkasan nilai total portofolio cryptocurrency
  3. Kemampuan untuk mengkonfigurasi portofolio (pilih format JSON untuk memuat dan menurunkan struktur portofolio). API REST tertentu untuk memperbarui portofolio dan memuatnya, mis. 2 API: untuk menyimpan / memperbarui - POST, untuk bongkar - GET. Struktur portofolio pada dasarnya adalah plat tipe sederhana
    BTC โ€“  0.00005 . XEM โ€“  4.5 . ... 
  4. Kami mengambil data dari cryptoexchanges dan sumber pertukaran mata uang (untuk mata uang fiat)
  5. Aturan untuk menghitung nilai total portofolio:
    Rumus untuk menghitung nilai total portofolio


Tentu saja, semua yang ditulis dalam paragraf 5 adalah subjek perselisihan dan keraguan yang terpisah, tetapi biarlah bisnis itu menginginkannya.


Proyek dimulai


Jadi, kita pergi ke situs web resmi kerangka kerja dan melihat bagaimana kita bisa mulai berkembang. Situs resmi menawarkan untuk menginstal alat sdkman. Sepotong yang memfasilitasi pengembangan dan pengelolaan proyek pada kerangka kerja mikronaut (dan lainnya termasuk, misalnya, Grails).


Manajer yang sama dari berbagai SDK

Sebuah komentar kecil: Jika Anda baru saja memulai inisialisasi proyek tanpa tombol apa pun, maka kolektor gradle dipilih secara default. Hapus folder, coba lagi, kali ini dengan kunci:
 mn create-app com.room606.cryptonaut -b=maven 

Poin yang menarik adalah sdkman, seperti Spring Tool Suite, menawarkan Anda pada tahap membuat proyek untuk mengatur "kubus" mana yang ingin Anda gunakan di awal. Saya tidak banyak bereksperimen dengan ini, saya juga membuatnya dengan preset default.


Akhirnya, kami membuka proyek di Intellij Idea dan mengagumi kumpulan sumber dan sumber daya dan cakram bahwa kami diberi panduan untuk membuat proyek mikronaut.


Struktur proyek telanjang

Eye Clings to 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 

Ya, itu menyenangkan dan patut dipuji. Kami segera diberi alat untuk dengan cepat mengeluarkan aplikasi ke Prod / INT / QA / lingkungan apa pun. Untuk ini, tanda tambah mental untuk proyek.


Sudah cukup untuk mengumpulkan proyek oleh Maven, kemudian mengumpulkan gambar Docker dan mempublikasikannya ke registri Docker Anda, atau hanya mengekspor biner gambar sebagai opsi ke sistem CI Anda, itulah yang Anda inginkan.


Di folder sumber daya, kami juga menyiapkan kosong dengan parameter konfigurasi aplikasi (analog dari application.properties di Spring), serta file konfigurasi untuk pustaka logback. Keren!


Kami pergi ke titik masuk aplikasi dan mempelajari kelas. Kami melihat gambar yang sangat familier bagi kami dari Spring Boot. Di sini, para pengembang kerangka kerja juga tidak mulai menciptakan dan menemukan apa pun.


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

Bandingkan dengan kode Spring yang dikenal.


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

Yaitu kami juga meningkatkan wadah IoC dengan semua biji yang termasuk dalam pekerjaan sesuai kebutuhan. Setelah menjalankan sedikit sesuai dengan dokumentasi resmi, kami perlahan memulai pengembangan.


Kami akan membutuhkan:


  1. Model Domain
  2. Pengontrol untuk mengimplementasikan REST API.
  3. Lapisan penyimpanan data (Klien basis data atau ORM atau yang lainnya)
  4. Kode data konsumen dari pertukaran cryptocurrency, serta data dari pertukaran mata uang fiat. Yaitu kita perlu menulis klien yang paling sederhana untuk layanan pihak ke-3. Di Spring, RestTemplate yang terkenal cocok dengan peran ini.
  5. Konfigurasi minimum untuk manajemen yang fleksibel dan permulaan aplikasi (mari kita pikirkan apa dan bagaimana kita akan membuat konfigurasi)
  6. Tes! Ya, untuk dapat dengan aman dan aman mem-refode kode dan mengimplementasikan fungsionalitas baru, kita perlu memastikan stabilitas yang lama
  7. Caching. Ini bukan persyaratan dasar, tetapi sesuatu yang menyenangkan untuk memiliki kinerja yang baik, dan dalam skenario kami ada tempat di mana caching jelas merupakan alat yang baik.
    Spoiler: semuanya akan menjadi sangat buruk di sini.

Model Domain


Untuk tujuan kami, model berikut akan mencukupi: model portofolio cryptocurrency, nilai tukar sepasang mata uang fiat, harga cryptocurrency dalam mata uang fiat, dan nilai total portofolio.


Di bawah ini adalah kode hanya beberapa model, sisanya dapat dilihat di repositori . Dan ya, saya terlalu malas untuk mengacaukan Lombok dalam proyek ini.


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

Pengontrol


Kami mencoba untuk menulis controller yang mengimplementasikan API paling sederhana, mengeluarkan nilai cryptocurrency sesuai dengan kode huruf koin yang diberikan.
Yaitu


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

Harus menghasilkan sesuatu seperti:


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

Menurut dokumentasi , tidak ada yang rumit dan mengingatkan pendekatan dan konvensi Spring sama:


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

Yaitu kami dengan tenang menentukan POJO kami sebagai tipe yang dikembalikan, dan tanpa mengonfigurasi serializer / deserializer apa pun, bahkan tanpa menggantung anotasi tambahan, Micronaut akan membuat badan http yang benar dengan data dari kotak. Mari kita bandingkan dengan cara 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) { 

Secara umum, saya tidak punya masalah dengan controller, mereka hanya bekerja seperti yang diharapkan dari mereka, menurut dokumentasi. Ejaan mereka intuitif dan sederhana. Kami melanjutkan.


Lapisan penyimpanan data


Untuk versi pertama aplikasi, kami hanya akan menyimpan portofolio pengguna. Secara umum, kami hanya akan menyimpan satu portofolio dari satu pengguna. Sederhananya, kami belum akan mendapat dukungan dari banyak pengguna, hanya satu pengguna utama dengan portofolio cryptocurrency-nya. Ini luar biasa!


Untuk menerapkan kegigihan data, dokumentasi menawarkan opsi dengan koneksi JPA, serta contoh terpisah menggunakan klien yang berbeda untuk membaca dari database (bagian "12.1.5 Mengkonfigurasi Postgres"). JPA tegas dibuang dan preferensi diberikan untuk menulis dan memanipulasi pertanyaan sendiri. Konfigurasi database ditambahkan ke application.yml, ( Postgres dipilih sebagai RDBMS), sesuai dengan dokumentasi:


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

Bergantung pada pustaka postgres-reactive ditambahkan. Ini adalah klien untuk bekerja dengan database baik secara asinkron maupun sinkron.


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

Dan akhirnya, file docker-compose.yml ditambahkan ke docker-compose.yml / docker-compose.yml untuk menyebarkan lingkungan aplikasi kita di masa depan, di mana komponen basisdata ditambahkan:


 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 

Di bawah ini adalah skrip inisialisasi untuk database dengan struktur tabel yang sangat sederhana:


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

Sekarang mari kita coba melempar kode yang memperbarui portofolio pengguna. Komponen portofolio kami akan terlihat seperti ini:


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

Melihat serangkaian metode Postgres reactive client , kami melempar kelas ini:


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

Meluncurkan lingkungan, kami mencoba memperbarui portofolio kami melalui API yang diterapkan secara hati-hati sebelumnya:


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

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

Dan ... tangkap kesalahan di log:


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

Setelah menggaruk lobak kami, kami tidak menemukan solusi apa pun di dock resmi, kami mencoba untuk google dock di perpustakaan postgres-reactive itu sendiri, dan ini ternyata menjadi solusi yang tepat, karena contoh dan sintaks kueri yang benar diberikan secara detail. Itu soal parameter placeholder, ternyata, Anda perlu menggunakan label bernomor dalam bentuk $x ($1, $2, etc.) . $x ($1, $2, etc.) . Jadi, perbaikannya adalah menulis ulang permintaan target:


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

Kami restart aplikasi, coba permintaan REST sama ... tepuk tangan. Data ditambahkan. Mari kita beralih ke membaca.


Kami dihadapkan dengan tugas paling sederhana yaitu membaca portofolio cryptocurrency pengguna dari database dan memetakannya ke objek POJO. Untuk tujuan ini, kami menggunakan metode 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; } 

Kami menghubungkan semua ini bersama-sama dengan pengontrol yang bertanggung jawab atas portofolio cryptocurrency:


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

Mulai ulang layanan. Untuk pengujian, pertama-tama kami mengisi portofolio ini dengan setidaknya beberapa data:


 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 

Sekarang akhirnya uji pembacaan kode kami dari database:


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

Dan ... kita dapatkan ... sesuatu yang aneh:


 {"coins":{}} 

Cukup aneh bukan? Kami memeriksa ulang permintaan sepuluh kali, mencoba melakukan permintaan curl lagi, bahkan memulai kembali layanan kami. Hasilnya masih sama liar ... Setelah membaca kembali tanda tangan metode, dan juga mengingat bahwa kami memiliki Reactive Pg client , kami sampai pada kesimpulan bahwa kami berurusan dengan sinkronisasi. Debag bijaksana mengkonfirmasi ini! Itu layak sedikit santai kode, seperti voila, kami mendapat data tidak kosong!


Beralih ke dok perpustakaan lagi, menggulung lengan baju kami, kami menulis ulang kode dengan kode pemblokiran yang benar, tetapi sepenuhnya dapat diprediksi:


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

Sekarang kita mendapatkan apa yang kita harapkan. Kami memutuskan masalah ini, teruskan.


Kami menulis klien untuk mendapatkan data pasar


Di sini, tentu saja, saya ingin menyelesaikan masalah dengan jumlah sepeda paling sedikit. Hasilnya adalah dua solusi:


  • pustaka klien siap pakai untuk mengakses pertukaran kripto tertentu
  • kode kecil klien sendiri untuk menerapkan nilai tukar. Yang keluar dari kotak adalah Micronaut.

Dengan perpustakaan yang sudah jadi, semuanya tidak begitu menarik. Saya hanya mencatat bahwa selama pencarian cepat, proyek https://github.com/knowm/XChange dipilih.


Pada prinsipnya, arsitektur perpustakaan sesederhana tiga sen - ada satu set antarmuka untuk menerima data, antarmuka utama dan kelas model seperti Ticker (Anda dapat mengetahui bid , ask , segala macam harga terbuka, harga tutup dll.), CurrencyPair , Currency . Selanjutnya, Anda menginisialisasi implementasi itu sendiri dalam kode, setelah sebelumnya menghubungkan dependensi dengan implementasi yang merujuk pada cryptoexchange tertentu. Dan kelas utama di mana kami beroperasi adalah MarketDataService.java


Misalnya, untuk percobaan kami, sebagai permulaan, kami puas dengan "konfigurasi" seperti itu:


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

Di bawah ini adalah kode yang menjalankan fungsi utama - menghitung biaya cryptocurrency tertentu dalam istilah fiat (lihat Rumus yang dijelaskan di awal artikel di blok persyaratan):


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

Semuanya telah dilakukan sejauh mungkin menggunakan antarmuka kita sendiri untuk sedikit mengabaikan implementasi spesifik yang disediakan oleh proyek https://github.com/knowm/XChange .


Mengingat fakta bahwa pada banyak, jika tidak semua pertukaran mata uang digital, hanya ada satu set mata uang fiat terbatas yang beredar (USD, EUR, mungkin itu saja ..), untuk jawaban akhir untuk pertanyaan pengguna, perlu menambahkan sumber data lain - nilai tukar mata uang fiat, dan juga merupakan konverter opsional. Yaitu Untuk menjawab pertanyaan, berapa biaya cryptocurrency WTF dalam RUR (mata uang target, mata uang target) sekarang, Anda harus menjawab dua sub-pertanyaan: WTF / BaseCurrency (kami menganggap USD sebagai itu), BaseCurrency / RUR, lalu gandakan kedua nilai ini dan berikan sebagai hasilnya.


Untuk versi pertama layanan kami, kami hanya akan mendukung USD dan RUR sebagai mata uang target.
Jadi, untuk mendukung RUR, disarankan untuk mengambil sumber yang relevan dengan lokasi geografis layanan (kami akan menyelenggarakan dan menggunakannya secara eksklusif di Rusia). Singkatnya, kurs Bank Sentral akan sesuai untuk kita. Sumber terbuka dari data tersebut ditemukan di Internet, yang dapat dikonsumsi sebagai JSON. Bagus


Di bawah ini adalah respons layanan terhadap permintaan nilai tukar saat ini:


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

Sebenarnya, di bawah ini adalah CbrExchangeRatesClient klien 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); } } } 

Di sini kita menyuntikkan RxHttpClient , komponen dari Micronaut . Ini juga memberi kita pilihan untuk melakukan pemrosesan atau pemblokiran permintaan tidak sinkron. Kami memilih pemblokiran klasik:


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

Konfigurasi


Dalam sebuah proyek, Anda dapat menyoroti hal-hal yang mengubah dan memengaruhi logika bisnis atau beberapa aspek tertentu. Mari kita membuat daftar mata uang fiat yang didukung sebagai properti dan menyuntikkannya di awal aplikasi.


Kode berikut akan membuang kode mata uang yang kami belum dapat menghitung nilai portofolio:


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

Karenanya, tujuan kami adalah untuk menyuntikkan nilai dari application.yml ke dalam variabel yang supportedCounterCurrencies .


Di versi pertama, kode tersebut ditulis, di bawah bidang kelas FiatExchangeRatesService.java:


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

Di sini, placeholder terkait dengan struktur dokumen 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" 

Peluncuran aplikasi, uji asap cepat ... Kesalahan!


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


Tes


, 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

Namun, semuanya benar-benar gagal di sini. Dokumentasi dalam aspek ini membingungkan, di mana setelah menggulir beberapa layar Anda menemukan potongan-potongan konfigurasi yang saling bertentangan ( appliction.yml). Sebagai cache, redis dipilih, juga diangkat dalam wadah Docker di sebelahnya. Ini konfigurasinya:
 redis: image: 'bitnami/redis:latest' environment: - ALLOW_EMPTY_PASSWORD=yes ports: - '6379:6379' 

Dan di sini adalah sepotong kode yang dijelaskan oleh @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); } } 

Tetapi dengan application.ymladalah misteri yang paling penting. Saya mencoba segala macam konfigurasi. Ini satu:


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

Ini satu:


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

Dan bahkan mencoba untuk menghapus huruf besar dalam nama cache. Tetapi sebagai hasilnya, saya mendapatkan hasil yang sama ketika memulai aplikasi - "Terjadi kesalahan tak terduga: Tidak ada cache yang dikonfigurasi untuk nama: 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-.


Benchmarking

Disclaimer-: , -, , ( , , , ).

, :


OS: 16.04.1-Ubuntu x86_64 x86_64 x86_64 GNU/Linux
CPU: Intelยฎ Core(TM) i7-7700HQ CPU @ 2.80GHz
Mem: 2 8 Gb DDR4, Speed: 2400 MHz
SSD Disk: PCIe NVMe M.2, 256


:


  1. API,
  2. API โ€“ โ€œโ€ .

โ€“ 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/id427595/


All Articles