جرب Micronaut أو Darling ، لقد قمت بتقليل الإطار

جرب Micronaut أو Darling ، لقد قمت بتقليل الإطار


حول إطار ميكرونوت ، مسحت لمحة عن النشرة الإخبارية. وتساءل عن نوع الوحش الذي كان عليه. يتم وضع الإطار على النقيض من الزنبرك المحشو بكل الأدوات اللازمة.


ميكرونوت


في انتظار المؤتمر القادم للمطورين ، حيث سيخبرون ويوضحون كيفية استخدام الميكرونوت في خدماتك الصغيرة ، قررت التحضير مرة واحدة على الأقل وتأتي مع بعض السياق على الأقل في رأسي ، مع مجموعة معينة من المشاكل والأسئلة. أداء الواجبات المنزلية إذا جاز التعبير. قررت أن أقوم ببعض المشاريع الصغيرة للحيوانات الأليفة لبضع أمسيات (كما هو الحال). في نهاية المقال سيكون هناك رابط إلى مستودع جميع مصادر المشروع.


Micronaut هو إطار عمل JVM يدعم ثلاث لغات تطوير: Java و Kotlin و Groovy. تم تطويره من قبل OCI ، وهي نفس الشركة التي قدمها لنا Grails. لديها ضبط في شكل تطبيق cli ومجموعة من المكتبات الموصى بها (عملاء المتفاعلين المتشعبين وقاعدة البيانات المختلفة)

هناك DI الذي ينفذ ويكرر أفكار Spring ، مضيفًا عددًا من رقائقه - التزامن ، دعم AWS Lambda ، موازنة تحميل جانب العميل.

فكرة الخدمة: اشترى أحد أصدقائي في وقت واحد مع أحمق نصف دزينة من جميع أنواع العملات المشفرة المختلفة ، واستثمر فيها إجازة مشربة وخبأ من سترة شتوية. نحن نعلم جميعًا أن تقلب كل هذه المادة المشفرة أمر غريب ، وأن الموضوع نفسه لا يمكن التنبؤ به بشكل عام ، قرر صديق في نهاية المطاف أن يعتني بأعصابه ويغرق ببساطة في ما يحدث مع "أصوله". لكن في بعض الأحيان ما زلت تريد أن تنظر ، ولكن ما هو موجود مع كل هذا ، فجأة أصبح غنياً بالفعل. لذا فإن فكرة لوحة بسيطة (لوحة تحكم ، مثل Grafana أو شيء أبسط) ، وصفحة ويب معينة تحتوي على معلومات جافة ، وكم تكلفتها جميع العملات الورقية (USD ، RUR).


إخلاء المسؤولية


  1. سنترك ملاءمة كتابة حلنا الخاص في البحر ، نحتاج فقط إلى تجربة الإطار الجديد على شيء أكثر دهاء من HelloWorld.
  2. خوارزمية الحساب والأخطاء المتوقعة والأخطاء وما إلى ذلك. (على الأقل للمرحلة الأولى من المنتج) ، فإن صلاحية اختيار تبادل التشفير لاستخراج المعلومات ، وحافظة التشفير "الاستثمارية" لأحد الأصدقاء ستكون أيضًا غير واردة ولا تخضع للمناقشة أو نوع من التحليل المتعمق.

لذا ، مجموعة صغيرة من المتطلبات:


  1. خدمة الويب (الوصول من الخارج ، عبر http)
  2. عرض صفحة في متصفح مع ملخص للقيمة الإجمالية لمحفظة العملات المشفرة
  3. القدرة على تكوين المحفظة (اختر تنسيق JSON لتحميل وتفريغ هيكل المحفظة). واجهة برمجة تطبيقات REST معينة لتحديث المحفظة وتحميلها ، أي 2 API: للحفظ / التحديث - POST ، للتفريغ - GET. بنية المحفظة هي في الأساس لوحة من النوع البسيط
    BTC –  0.00005 . XEM –  4.5 . ... 
  4. نأخذ البيانات من التبادلات المشفرة ومصادر صرف العملات (للعملات الورقية)
  5. قواعد لحساب القيمة الإجمالية للمحفظة:
    صيغ لحساب القيمة الإجمالية للمحفظة


بالطبع ، كل ما هو مكتوب في الفقرة 5 هو موضوع نزاعات وشكوك منفصلة ، ولكن فليكن أن الأعمال التجارية أرادت ذلك.


بدء المشروع


لذلك ، نذهب إلى الموقع الرسمي للإطار ونرى كيف يمكننا البدء في التطوير. يقدم الموقع الرسمي لتثبيت أداة sdkman. قطعة تسهل تطوير وإدارة المشاريع على إطار ميكرونوت (وغيرها من المشاريع بما في ذلك ، على سبيل المثال ، Grails).


نفس مدير SDKs المختلفة

ملاحظة صغيرة: إذا بدأت للتو تهيئة المشروع بدون أي مفاتيح ، فسيتم تحديد جامع الدرجات افتراضيًا. احذف المجلد وحاول مرة أخرى هذه المرة باستخدام المفتاح:
 mn create-app com.room606.cryptonaut -b=maven 

نقطة مثيرة للاهتمام هي أن sdkman ، مثل Spring Tool Suite ، يقدم لك في مرحلة إنشاء مشروع لتعيين "المكعبات" التي تريد استخدامها في البداية. لم أجرب ذلك كثيرًا ، بل أنشأته أيضًا بإعداد افتراضي افتراضي.


أخيرًا ، نفتح المشروع في Intellij Idea ونعجب بمجموعة المصادر والموارد والأقراص التي زودنا بها المعالج لإنشاء مشروع الميكروتوت.


هيكل المشروع العاري

تمسك العين إلى Dockerfile
 FROM openjdk:8u171-alpine3.7 RUN apk --no-cache add curl COPY target/cryptonaut*.jar cryptonaut.jar CMD java ${JAVA_OPTS} -jar cryptonaut.jar 

حسنًا ، إنه ممتع وجدير بالثناء. تم تزويدنا على الفور بأداة لإخراج التطبيق بسرعة إلى Prod / INT / QA / أيا كانت البيئة. لهذا ، علامة عقلية زائد للمشروع.


يكفي جمع المشروع بواسطة Maven ، ثم جمع صورة Docker ونشرها على سجل Docker الخاص بك ، أو ببساطة تصدير الصورة الثنائية كخيار لنظام CI الخاص بك ، هذه هي الطريقة التي تريدها.


في مجلد الموارد ، قمنا أيضًا بإعداد فراغ يحتوي على معلمات تكوين التطبيق (تناظرية لملف application.properties في Spring) ، بالإضافة إلى ملف تكوين لمكتبة التسجيل. رائع!


نذهب إلى نقطة دخول التطبيق ودراسة الفصل. نرى صورة مألوفة لنا بشكل مؤلم من Spring Boot. هنا ، لم يبدأ مطورو الإطار أيضًا باختراع واختراع أي شيء.


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

قارن مع كود الربيع المألوف.


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

على سبيل المثال نقوم أيضًا برفع حاوية IoC مع جميع الفاصوليا المدرجة في العمل حسب الحاجة. بعد الركض قليلاً وفقًا للوثائق الرسمية ، نبدأ التطوير ببطء.


سنحتاج إلى:


  1. نماذج المجال
  2. وحدات تحكم لتطبيق REST API.
  3. طبقة تخزين البيانات (عميل قاعدة البيانات أو ORM أو أي شيء آخر)
  4. رمز للمستهلكين للبيانات من بورصات العملات المشفرة ، وكذلك البيانات من صرف العملات الورقية. على سبيل المثال نحن بحاجة لكتابة أبسط عملاء لخدمات الطرف الثالث. في فصل الربيع ، يناسب قالب RestTemplate المعروف هذا الدور جيدًا.
  5. الحد الأدنى من التكوين للإدارة المرنة وبدء التطبيق (دعنا نفكر في كيفية وكيفية إجراء التهيئة)
  6. الاختبارات! نعم ، من أجل إعادة صياغة التعليمات البرمجية بأمان وأمان وتنفيذ وظائف جديدة ، نحتاج إلى التأكد من استقرار القديم
  7. التخزين المؤقت. هذا ليس متطلبًا أساسيًا ، ولكن شيئًا لطيفًا من أجل الحصول على أداء جيد ، وفي سيناريونا هناك أماكن يكون فيها التخزين المؤقت أداة جيدة بالتأكيد.
    المفسد: كل شيء سيء للغاية هنا.

نماذج المجال


لأغراضنا ، ستكفي النماذج التالية: نماذج محفظة العملات المشفرة ، وسعر الصرف لزوج من العملات الورقية ، وأسعار العملات المشفرة بالعملة الورقية ، والقيمة الإجمالية للمحفظة.


يوجد أدناه رمز نموذجين فقط ، ويمكن مشاهدة الباقي في المستودع . ونعم ، لقد كنت كسولًا جدًا لدرجة أنني لم أفسد Lombok في هذا المشروع.


 Portfolio.java package com.room606.cryptonaut.domain; import java.math.BigDecimal; import java.util.Collections; import java.util.Map; import java.util.TreeMap; public class Portfolio { private Map<String, BigDecimal> coins = Collections.emptyMap(); public Map<String, BigDecimal> getCoins() { return new TreeMap<>(coins); } public void setCoins(Map<String, BigDecimal> coins) { this.coins = coins; } 

 FiatRate.java package com.room606.cryptonaut.domain; import java.math.BigDecimal; public class FiatRate { private String base; private String counter; private BigDecimal value; public FiatRate(String base, String counter, BigDecimal value) { this.base = base; this.counter = counter; this.value = value; } public String getBase() { return base; } public void setBase(String base) { this.base = base; } public String getCounter() { return counter; } public void setCounter(String counter) { this.counter = counter; } public BigDecimal getValue() { return value; } public void setValue(BigDecimal value) { this.value = value; } } 

 Price.java ... Prices.java () ... Total.java ... 

وحدات تحكم


نحن نحاول كتابة وحدة تحكم تنفذ أبسط واجهة برمجة التطبيقات ، وتصدر قيمة العملات المشفرة وفقًا لرموز الحروف المعينة للعملات المعدنية.
على سبيل المثال


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

يجب أن ينتج شيئًا مثل:


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

وفقًا للوثائق ، لا يوجد شيء معقد ويذكر نفس نهج Spring والاتفاقيات:


 package com.room606.cryptonaut.rest; import com.room606.cryptonaut.domain.Price; import com.room606.cryptonaut.domain.Prices; import com.room606.cryptonaut.markets.FiatExchangeRatesService; import com.room606.cryptonaut.markets.CryptoMarketDataService; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.*; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @Controller("/cryptonaut/restapi/") public class MarketDataController { private final CryptoMarketDataService cryptoMarketDataService; private final FiatExchangeRatesService fiatExchangeRatesService; public MarketDataController(CryptoMarketDataService cryptoMarketDataService, FiatExchangeRatesService fiatExchangeRatesService) { this.cryptoMarketDataService = cryptoMarketDataService; this.fiatExchangeRatesService = fiatExchangeRatesService; } @Get("/prices.json") @Produces(MediaType.APPLICATION_JSON) public Prices pricesAsJson(@QueryValue("coins") String[] coins, @QueryValue("fiatCurrency") String fiatCurrency) { return getPrices(coins, fiatCurrency); } private Prices getPrices(String[] coins, String fiatCurrency) { List<Price> prices = Stream.of(coins) .map(coin -> new Price(coin, cryptoMarketDataService.getPrice(coin, fiatCurrency))) .collect(Collectors.toList()); return new Prices(prices, fiatCurrency); } } 

على سبيل المثال نحدد POJO بنا بهدوء كنوع مرتجع ، وبدون تكوين أي مُسلسل / مُسلسل ، حتى بدون تعليق تعليقات توضيحية إضافية ، سيقوم Micronaut ببناء نص http الصحيح مع البيانات من الصندوق. دعونا نقارن طريقة Spring :


 @RequestMapping(value = "/prices.json", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity<Prices> pricesAsJson(@RequestParam("userId") final String[] coins, @RequestParam("fiatCurrency") String fiatCurrency) { 

بشكل عام ، لم يكن لدي أي مشاكل مع وحدات التحكم ، فقد عملوا فقط كما هو متوقع منهم ، وفقًا للوثائق. كان تهجئتهم بديهية وبسيطة. ننتقل.


طبقة تخزين البيانات


بالنسبة للنسخة الأولى من التطبيق ، سنقوم فقط بتخزين محفظة المستخدم. بشكل عام ، سنحتفظ بمحفظة واحدة فقط لمستخدم واحد. ببساطة ، لن يكون لدينا حتى الآن دعم العديد من المستخدمين ، مستخدم رئيسي واحد فقط مع مجموعته من العملات المشفرة. هذا رائع!


لتنفيذ استمرارية البيانات ، تقدم الوثائق خيارات مع اتصال JPA ، بالإضافة إلى أمثلة مجزأة لاستخدام عملاء مختلفين للقراءة من قاعدة البيانات (القسم "12.1.5 تكوين Postgres"). JPA تجاهل JPA بشكل حاسم وتم إعطاء الأفضلية لكتابة الاستفسارات والتلاعب بها بنفسك. تمت إضافة تكوين قاعدة البيانات إلى application.yml ، (تم اختيار Postgres كـ RDBMS) ، وفقًا للوثائق:


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

اعتمادا على مكتبة postgres-reactive . هذا هو عميل للعمل مع قاعدة البيانات بطريقة غير متزامنة وبطريقة متزامنة.


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

وأخيرًا ، تمت إضافة ملف docker-compose.yml إلى docker-compose.yml / docker-compose.yml لنشر البيئة المستقبلية لتطبيقنا ، حيث تمت إضافة مكون قاعدة البيانات:


 db: image: postgres:9.6 restart: always environment: POSTGRES_USER: crypto POSTGRES_PASSWORD: r1ch13r1ch POSTGRES_DB: cryptonaut ports: - 5432:5432 volumes: - ${PWD}/../db/init_tables.sql:/docker-entrypoint-initdb.d/1.0.0_init_tables.sql 

فيما يلي نص برنامج التهيئة لقاعدة البيانات ببنية جدول بسيطة للغاية:


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

الآن دعنا نحاول وضع رمز يحدّث محفظة المستخدم. سيبدو مكون المحفظة لدينا على النحو التالي:


 package com.room606.cryptonaut; import com.room606.cryptonaut.domain.Portfolio; import java.math.BigDecimal; import java.util.Optional; public interface PortfolioService { Portfolio savePortfolio(Portfolio portfolio); Portfolio loadPortfolio(); Optional<BigDecimal> calculateTotalValue(Portfolio portfolio, String fiatCurrency); } 

بالنظر إلى مجموعة طرق Postgres reactive client من Postgres reactive client ، فإننا نرمي هذه الفئة:


 package com.room606.cryptonaut; import com.room606.cryptonaut.domain.Portfolio; import com.room606.cryptonaut.markets.CryptoMarketDataService; import io.micronaut.context.annotation.Requires; import io.reactiverse.pgclient.Numeric; import io.reactiverse.reactivex.pgclient.*; import javax.inject.Inject; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class PortfolioServiceImpl implements PortfolioService { private final PgPool pgPool; ... private static final String UPDATE_COIN_AMT = "INSERT INTO portfolio (coin, amount) VALUES (?, ?) ON CONFLICT (coin) " + "DO UPDATE SET amount = ?"; ... public Portfolio savePortfolio(Portfolio portfolio) { List<Tuple> records = portfolio.getCoins() .entrySet() .stream() .map(entry -> Tuple.of(entry.getKey(), Numeric.create(entry.getValue()), Numeric.create(entry.getValue()))) .collect(Collectors.toList()); pgPool.preparedBatch(UPDATE_COIN_AMT, records, pgRowSetAsyncResult -> { //   pgRowSetAsyncResult.cause().printStackTrace(); }); return portfolio; } ... } 

إطلاق بيئة ، نحاول تحديث محفظتنا من خلال واجهة برمجة تطبيقات تم تنفيذها بحكمة مقدمًا:


 package com.room606.cryptonaut.rest; import com.room606.cryptonaut.PortfolioService; import com.room606.cryptonaut.domain.Portfolio; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.*; import javax.inject.Inject; @Controller("/cryptonaut/restapi/") public class ConfigController { @Inject private PortfolioService portfolioService; @Post("/portfolio") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Portfolio savePortfolio(@Body Portfolio portfolio) { return portfolioService.savePortfolio(portfolio); } 

تنفيذ طلب curl :


 curl http://localhost:8080/cryptonaut/restapi/portfolio -X POST -H "Content-Type: application/json" --data '{"coins": {"XRP": "35.5", "LSK": "5.03", "XEM": "16.23"}}' -v 

و ... قبض على الخطأ في السجلات:


 io.reactiverse.pgclient.PgException: syntax error at or near "," at io.reactiverse.pgclient.impl.PrepareStatementCommand.handleErrorResponse(PrepareStatementCommand.java:74) at io.reactiverse.pgclient.impl.codec.decoder.MessageDecoder.decodeError(MessageDecoder.java:250) at io.reactiverse.pgclient.impl.codec.decoder.MessageDecoder.decodeMessage(MessageDecoder.java:139) ... 

بعد حكّ اللفت ، لا نجد أي حل في الرصيف الرسمي ، نحاول البحث في الرصيف في مكتبة postgres-reactive نفسها ، وتبين أن هذا هو الحل الصحيح ، حيث يتم إعطاء أمثلة وصيغة الاستعلام الصحيحة بالتفصيل. كان الأمر يتعلق بمعلمات العنصر النائب ، كما اتضح ، فأنت بحاجة إلى استخدام تسميات مرقمة للنموذج $x ($1, $2, etc.) . لذا ، فإن الإصلاح هو إعادة كتابة الطلب الهدف:


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

نعيد تشغيل التطبيق ، حاول نفس طلب REST ... هتاف. يتم إضافة البيانات. دعنا ننتقل إلى القراءة.


نحن نواجه أبسط مهمة لقراءة مجموعة من العملات المشفرة للمستخدم من قاعدة البيانات وتعيينها إلى كائن POJO. لهذه الأغراض ، نستخدم طريقة pgPool.query (SELECT_COINS_AMTS ، pgRowSetAsyncResult):


 public Portfolio loadPortfolio() { Map<String, BigDecimal> coins = new HashMap<>(); pgPool.query(SELECT_COINS_AMTS, pgRowSetAsyncResult -> { if (pgRowSetAsyncResult.succeeded()) { PgRowSet rows = pgRowSetAsyncResult.result(); PgIterator pgIterator = rows.iterator(); while (pgIterator.hasNext()) { Row row = pgIterator.next(); coins.put(row.getString("coin"), new BigDecimal(row.getFloat("amount"))); } } else { System.out.println("Failure: " + pgRowSetAsyncResult.cause().getMessage()); } }); Portfolio portfolio = new Portfolio(); portfolio.setCoins(coins); return portfolio; } 

نربط كل هذا مع وحدة التحكم المسؤولة عن محفظة العملات المشفرة:


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

أعد تشغيل الخدمة. للاختبار ، نقوم أولاً بملء هذه المحفظة ببعض البيانات على الأقل:


 curl http://localhost:8080/cryptonaut/restapi/portfolio -X POST -H "Content-Type: application/json" --data '{"coins": {"XRP": "35.5", "LSK": "5.03", "XEM": "16.23"}}' -v 

أخيرًا الآن ، اختبر رمزنا من قاعدة البيانات:


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

و ... نحصل على ... شيء غريب:


 {"coins":{}} 

غريب جدا أليس كذلك؟ نعيد التحقق من الطلب عشر مرات ، ونحاول إجراء طلب curl مرة أخرى ، حتى إعادة تشغيل خدمتنا. والنتيجة لا تزال كما هي ... بعد إعادة قراءة توقيع الطريقة ، وتذكر أيضًا أن لدينا Reactive Pg client ، نصل إلى استنتاج مفاده أننا نتعامل مع التزامن. أكد debag مدروس هذا! كان الأمر يستحق القليل من فك الشفرة على مهل ، مثل فويلا ، حصلنا على بيانات غير فارغة!


بالعودة إلى إرساء المكتبة مرة أخرى ، ولفّ سواعدنا ، نعيد كتابة الرمز برمز حظر حقيقي ، ولكن يمكن التنبؤ به تمامًا:


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

الآن نحصل على ما نتوقعه. قررنا هذه المشكلة ، المضي قدما.


نكتب عميل للحصول على بيانات السوق


هنا ، بالطبع ، أود حل المشكلة بأقل عدد من الدراجات. والنتيجة حلين:


  • مكتبات عملاء جاهزة للوصول إلى تبادلات تشفير محددة
  • رمز عميل صغير للتقدم بطلب للحصول على سعر الصرف. ما خرج من العلبة هو ميكرونوت.

مع المكتبات الجاهزة ، كل شيء ليس مثيرًا للاهتمام. ألاحظ فقط أنه أثناء البحث السريع ، تم اختيار المشروع https://github.com/knowm/XChange .


من حيث المبدأ ، فإن بنية المكتبة بسيطة مثل ثلاثة بنسات - هناك مجموعة من الواجهات لاستلام البيانات ، والواجهات الرئيسية وفئات النماذج مثل Ticker (يمكنك معرفة bid ، ask ، جميع أنواع الأسعار المفتوحة ، سعر الإغلاق وما إلى ذلك) ، CurrencyPair ، Currency . علاوة على ذلك ، تقوم بتهيئة عمليات التنفيذ نفسها في التعليمات البرمجية ، بعد ربط التبعية مسبقًا بالتطبيق الذي يشير إلى تبادل تشفير محدد. MarketDataService.java الرئيسية التي نعمل من خلالها هي MarketDataService.java


على سبيل المثال ، بالنسبة لتجاربنا ، بالنسبة للمبتدئين ، نحن راضون عن هذا "التكوين":


 <dependency> <groupId>org.knowm.xchange</groupId> <artifactId>xchange-core</artifactId> <version>4.3.10</version> </dependency> <dependency> <groupId>org.knowm.xchange</groupId> <artifactId>xchange-bittrex</artifactId> <version>4.3.10</version> </dependency> 

فيما يلي الكود الذي يؤدي وظيفة رئيسية - حساب تكلفة عملة مشفرة معينة بشروط فيات (انظر الصيغ الموضحة في بداية المقالة في كتلة المتطلبات):


 package com.room606.cryptonaut.markets; import com.room606.cryptonaut.exceptions.CryptonautException; import org.knowm.xchange.currency.Currency; import org.knowm.xchange.currency.CurrencyPair; import org.knowm.xchange.dto.marketdata.Ticker; import org.knowm.xchange.exceptions.CurrencyPairNotValidException; import org.knowm.xchange.service.marketdata.MarketDataService; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; import java.math.BigDecimal; @Singleton public class CryptoMarketDataService { private final FiatExchangeRatesService fiatExchangeRatesService; private final MarketDataService marketDataService; @Inject public CryptoMarketDataService(FiatExchangeRatesService fiatExchangeRatesService, MarketDataServiceFactory marketDataServiceFactory) { this.fiatExchangeRatesService = fiatExchangeRatesService; this.marketDataService = marketDataServiceFactory.getMarketDataService(); } public BigDecimal getPrice(String coinCode, String fiatCurrencyCode) throws CryptonautException { BigDecimal price = getPriceForBasicCurrency(coinCode, Currency.USD.getCurrencyCode()); if (Currency.USD.equals(new Currency(fiatCurrencyCode))) { return price; } else { return price.multiply(fiatExchangeRatesService.getFiatPrice(Currency.USD.getCurrencyCode(), fiatCurrencyCode)); } } private BigDecimal getPriceForBasicCurrency(String coinCode, String fiatCurrencyCode) throws CryptonautException { Ticker ticker = null; try { ticker = marketDataService.getTicker(new CurrencyPair(new Currency(coinCode), new Currency(fiatCurrencyCode))); return ticker.getBid(); } catch (CurrencyPairNotValidException e) { ticker = getTicker(new Currency(coinCode), Currency.BTC); Ticker ticker2 = getTicker(Currency.BTC, new Currency(fiatCurrencyCode)); return ticker.getBid().multiply(ticker2.getBid()); } catch (IOException e) { throw new CryptonautException("Failed to get price for Pair " + coinCode + "/" + fiatCurrencyCode + ": " + e.getMessage(), e); } } private Ticker getTicker(Currency base, Currency counter) throws CryptonautException { try { return marketDataService.getTicker(new CurrencyPair(base, counter)); } catch (CurrencyPairNotValidException | IOException e) { throw new CryptonautException("Failed to get price for Pair " + base.getCurrencyCode() + "/" + counter.getCurrencyCode() + ": " + e.getMessage(), e); } } } 

تم عمل كل شيء إلى أقصى حد ممكن باستخدام واجهاتنا الخاصة من أجل تجاهل بعض التطبيقات المحددة التي يوفرها المشروع https://github.com/knowm/XChange .


في ضوء حقيقة أنه في العديد من ، إن لم يكن جميع بورصات العملات المشفرة ، هناك فقط مجموعة محدودة من العملات الورقية المتداولة (الدولار الأمريكي ، اليورو ، ربما هذا كل شيء ..) ، للإجابة النهائية على سؤال المستخدم ، من الضروري إضافة مصدر بيانات آخر - أسعار العملات الورقية ، و أيضا محول اختياري. على سبيل المثال للإجابة على السؤال ، كم تكلف WTF العملة المشفرة في RUR (العملة المستهدفة ، العملة المستهدفة) الآن ، سيكون عليك الإجابة على سؤالين فرعيين: WTF / BaseCurrency (نعتبر USD على هذا النحو) ، BaseCurrency / RUR ، ثم اضرب هاتين القيمتين وتنتج نتيجة لذلك.


بالنسبة لإصدارنا الأول من الخدمة ، سوف ندعم فقط الدولار الأمريكي والعملة المحلية (RUR) كعملات مستهدفة.
لذلك ، لدعم RUR ، من المستحسن أخذ المصادر ذات الصلة بالموقع الجغرافي للخدمة (سنستضيفها ونستخدمها حصريًا في روسيا). باختصار ، سوف يناسبنا سعر البنك المركزي. تم العثور على مصدر مفتوح لمثل هذه البيانات على الإنترنت ، والتي يمكن استهلاكها كـ JSON. عظيم.


فيما يلي استجابة الخدمة لطلب سعر الصرف الحالي:


 { "Date": "2018-10-16T11:30:00+03:00", "PreviousDate": "2018-10-13T11:30:00+03:00", "PreviousURL": "\/\/www.cbr-xml-daily.ru\/archive\/2018\/10\/13\/daily_json.js", "Timestamp": "2018-10-15T23:00:00+03:00", "Valute": { "AUD": { "ID": "R01010", "NumCode": "036", "CharCode": "AUD", "Nominal": 1, "Name": "ђІЃ‚Ђ°»№Ѓє№ ґѕ»»°Ђ", "Value": 46.8672, "Previous": 46.9677 }, "AZN": { "ID": "R01020A", "NumCode": "944", "CharCode": "AZN", "Nominal": 1, "Name": "ђ·µЂ±°№ґ¶°ЅЃє№ ј°Ѕ°‚", "Value": 38.7567, "Previous": 38.8889 }, "GBP": { "ID": "R01035", "NumCode": "826", "CharCode": "GBP", "Nominal": 1, "Name": "¤ѓЅ‚ Ѓ‚µЂ»ЅіѕІ ЎѕµґЅµЅЅѕіѕ єѕЂѕ»µІЃ‚І°", "Value": 86.2716, "Previous": 87.2059 }, ... 

في الواقع ، أدناه هو CbrExchangeRatesClient العميل CbrExchangeRatesClient :


 package com.room606.cryptonaut.markets.clients; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.room606.cryptonaut.exceptions.CryptonautException; import io.micronaut.http.HttpRequest; import io.micronaut.http.client.Client; import io.micronaut.http.client.RxHttpClient; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; import java.math.BigDecimal; import java.util.*; @Singleton public class CbrExchangeRatesClient { private static final String CBR_DATA_URI = "https://www.cbr-xml-daily.ru/daily_json.js"; @Client(CBR_DATA_URI) @Inject private RxHttpClient httpClient; private final ObjectReader objectReader = new ObjectMapper().reader(); public Map<String, BigDecimal> getRates() { try { //return ratesCache.get("fiatRates"); HttpRequest<?> req = HttpRequest.GET(""); String response = httpClient.retrieve(req, String.class).blockingSingle(); JsonNode json = objectReader.readTree(response); String usdPrice = json.get("Valute").get("USD").get("Value").asText(); String eurPrice = json.get("Valute").get("EUR").get("Value").asText(); String gbpPrice = json.get("Valute").get("GBP").get("Value").asText(); Map<String, BigDecimal> prices = new HashMap<>(); prices.put("USD", new BigDecimal(usdPrice)); prices.put("GBP", new BigDecimal(gbpPrice)); prices.put("EUR", new BigDecimal(eurPrice)); return prices; } catch (IOException e) { throw new CryptonautException("Failed to obtain exchange rates: " + e.getMessage(), e); } } } 

هنا نقوم بحقن RxHttpClient ، وهو مكون من Micronaut . كما يمنحنا خيار القيام بمعالجة أو حظر الطلب غير المتزامن. نختار المنع الكلاسيكي:


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

التكوين


في المشروع ، يمكنك تسليط الضوء على الأشياء التي تغير وتؤثر على منطق الأعمال أو بعض الجوانب المحددة. لنقم بعمل قائمة بالعملات الورقية المدعومة كممتلكات ونحقنها في بداية التطبيق.


سوف يتجاهل الرمز التالي رموز العملات التي لم نتمكن بعد من حساب قيمة المحفظة:


 public BigDecimal getFiatPrice(String baseCurrency, String counterCurrency) throws NotSupportedFiatException { if (!supportedCounterCurrencies.contains(counterCurrency)) { throw new NotSupportedFiatException("Counter currency not supported: " + counterCurrency); } Map<String, BigDecimal> rates = cbrExchangeRatesClient.getRates(); return rates.get(baseCurrency); } 

وبناءً على ذلك ، فإن هدفنا هو إدخال القيمة من application.yml بطريقة أو بأخرى في متغيرCounterCurrencies supportedCounterCurrencies .


في الإصدار الأول ، تمت كتابة هذا الرمز ، أسفل حقل فئة FiatExchangeRatesService.java:


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

هنا ، placeholder يتوافق مع الهيكل التالي لوثيقة application.yml :


 micronaut: application: name: cryptonaut #Uncomment to set server port server: port: 8080 postgres: reactive: client: port: 5432 host: localhost database: cryptonaut user: crypto password: r1ch13r1ch maxSize: 5 # app / business logic specific properties cryptonaut: currencies: "RUR" 

إطلاق التطبيق ، اختبار الدخان السريع ... خطأ!


 Caused by: io.micronaut.context.exceptions.BeanInstantiationException: Error instantiating bean of type [com.room606.cryptonaut.markets.CryptoMarketDataService] Path Taken: new MarketDataController([CryptoMarketDataService cryptoMarketDataService],FiatExchangeRatesService fiatExchangeRatesService) --> new CryptoMarketDataService([FiatExchangeRatesService fiatExchangeRatesService],MarketDataServiceFactory marketDataServiceFactory) at io.micronaut.context.DefaultBeanContext.doCreateBean(DefaultBeanContext.java:1266) at io.micronaut.context.DefaultBeanContext.createAndRegisterSingleton(DefaultBeanContext.java:1677) at io.micronaut.context.DefaultBeanContext.getBeanForDefinition(DefaultBeanContext.java:1447) at io.micronaut.context.DefaultBeanContext.getBeanInternal(DefaultBeanContext.java:1427) at io.micronaut.context.DefaultBeanContext.getBean(DefaultBeanContext.java:852) at io.micronaut.context.AbstractBeanDefinition.getBeanForConstructorArgument(AbstractBeanDefinition.java:943) ... 36 common frames omitted Caused by: java.lang.NullPointerException: null at com.room606.cryptonaut.markets.FiatExchangeRatesService.<init>(FiatExchangeRatesService.java:20) at com.room606.cryptonaut.markets.$FiatExchangeRatesServiceDefinition.build(Unknown Source) at io.micronaut.context.DefaultBeanContext.doCreateBean(DefaultBeanContext.java:1252) ... 41 common frames omitted 

Micronaut Spring , compile time . , :


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

, – javax.annotation.PostConstruct , , , , . .


, , Spring. micronaut @Property Map<String, String> , @Configuration , Random Properties (, ID , , - ) PropertySourceLoader , .. . 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 .


مخبأ

ومع ذلك ، كان كل شيء فاشلًا تمامًا هنا. الوثائق في هذا الجانب مربكة ، حيث بعد التمرير عبر شاشتين تجد أجزاء من التكوينات التي تتعارض مع بعضها البعض ( appliction.yml). كذاكرة تخزين مؤقت ، تم اختيار redis ، ورفعها أيضًا في حاوية Docker المجاورة لها. هنا تكوينه:
 redis: image: 'bitnami/redis:latest' environment: - ALLOW_EMPTY_PASSWORD=yes ports: - '6379:6379' 

وإليك جزء من الكود المشروح بواسطةCacheable:


 @Cacheable("fiatRates") public Map<String, BigDecimal> getRates() { HttpRequest<?> req = HttpRequest.GET(""); String response = httpClient.retrieve(req, String.class).blockingSingle(); try { JsonNode json = objectReader.readTree(response); String usdPrice = json.get("Valute").get("USD").get("Value").asText(); String eurPrice = json.get("Valute").get("EUR").get("Value").asText(); String gbpPrice = json.get("Valute").get("GBP").get("Value").asText(); Map<String, BigDecimal> prices = new HashMap<>(); prices.put("USD", new BigDecimal(usdPrice)); prices.put("GBP", new BigDecimal(gbpPrice)); prices.put("EUR", new BigDecimal(eurPrice)); return prices; } catch (IOException e) { throw new RuntimeException(e); } } 

ولكن مع application.ymlكان اللغز الأكثر أهمية. حاولت كل أنواع التكوينات. هنا واحد:


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

هنا واحد:


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

وحاول حتى إزالة الأحرف الكبيرة في اسم ذاكرة التخزين المؤقت. ولكن نتيجة لذلك ، حصلت على نفس النتيجة عند بدء التطبيق - "حدث خطأ غير متوقع: لم يتم تكوين ذاكرة تخزين مؤقت للاسم: fiatRates":


 ERROR imhsnetty.RoutingInBoundHandler - Unexpected error occurred: No cache configured for name: fiatRates io.micronaut.context.exceptions.ConfigurationException: No cache configured for name: fiatRates at io.micronaut.cache.DefaultCacheManager.getCache(DefaultCacheManager.java:67) at io.micronaut.cache.interceptor.CacheInterceptor.interceptSync(CacheInterceptor.java:176) at io.micronaut.cache.interceptor.CacheInterceptor.intercept(CacheInterceptor.java:128) at io.micronaut.aop.MethodInterceptor.intercept(MethodInterceptor.java:41) at io.micronaut.aop.chain.InterceptorChain.proceed(InterceptorChain.java:147) at com.room606.cryptonaut.markets.clients.$CbrExchangeRatesClientDefinition$Intercepted.getRates(Unknown Source) at com.room606.cryptonaut.markets.FiatExchangeRatesService.getFiatPrice(FiatExchangeRatesService.java:30) at com.room606.cryptonaut.rest.MarketDataController.index(MarketDataController.java:34) at com.room606.cryptonaut.rest.$MarketDataControllerDefinition$$exec2.invokeInternal(Unknown ... 

GitHub - SO . . , . , . boilerplate-, - Redis - , , Spring Boot , .



, Micronaut – , Spring-.


قياس الأداء

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


All Articles