Prueba Micronaut o Darling, reduje el marco
Sobre el marco de micronaut, vislumbré el resumen del boletín. Se preguntó qué tipo de bestia era. El marco se pone en contraste con el resorte relleno con todas las herramientas necesarias.

Anticipando la próxima conferencia para desarrolladores, donde solo contarán y mostrarán cómo usar micronaut en sus microservicios, decidí prepararme al menos una vez y tener al menos algo de contexto en mi cabeza, con un cierto conjunto de problemas y preguntas. Realizar la tarea, por así decirlo. Decidí hacer un pequeño proyecto para mascotas durante un par de noches (como se dice). Al final del artículo habrá un enlace al repositorio de todas las fuentes del proyecto.
Micronaut es un marco JVM que admite tres lenguajes de desarrollo: Java, Kotlin, Groovy. Fue desarrollado por OCI, la misma compañía que nos dio Grails. Tiene ajuste en forma de una aplicación cli y un conjunto de bibliotecas recomendadas (varios clientes reactivos-http y de base de datos)
Hay un DI que implementa y repite las ideas de Spring, agregando varios de sus chips: asincronismo, soporte para AWS Lambda, equilibrio de carga lateral del cliente.
La idea del servicio: uno de mis amigos en un momento con un tonto compró media docena de todo tipo de criptomonedas diferentes, invirtiendo en ellas unas vacaciones impregnadas y un escondite de una chaqueta de invierno. Todos sabemos que la volatilidad de toda esta sustancia de criptomonedas es salvaje, y el tema en sí mismo es generalmente impredecible, un amigo finalmente decidió cuidar sus nervios y simplemente sumergirse en lo que está sucediendo con sus "activos". Pero a veces todavía quieres mirar, pero qué hay con todo esto, de repente ya es rico. Entonces surgió la idea de un panel simple (un tablero de instrumentos, como Grafana o algo más simple), una determinada página web con información seca, cuánto cuesta todo en alguna moneda fiduciaria (USD, RUR).
Renuncias
- Dejaremos la conveniencia de escribir nuestra propia solución por la borda, solo tenemos que probar el nuevo marco en algo más astuto que HelloWorld.
- Algoritmo de cálculo, errores esperados, errores, etc. (al menos para la primera fase del producto), la validez de la elección de los intercambios de cifrado para obtener información, la cartera de cifrado de "inversión" de un amigo también estará fuera de discusión y no estará sujeta a discusión o algún tipo de análisis profundo.
Entonces, un pequeño conjunto de requisitos:
- Servicio web (acceso desde el exterior, a través de http)
- Mostrar una página en un navegador con un resumen del valor total de la cartera de criptomonedas
- Capacidad para configurar la cartera (elija el formato JSON para cargar y descargar la estructura de la cartera). Una determinada API REST para actualizar una cartera y cargarla, es decir 2 API: para guardar / actualizar - POST, para descargar - GET. La estructura de la cartera es esencialmente una placa de tipo simple
BTC – 0.00005 . XEM – 4.5 . ...
- Tomamos datos de criptointercambios y fuentes de cambio de divisas (para monedas fiduciarias)
- Reglas para calcular el valor total de la cartera:

Por supuesto, todo lo que está escrito en el párrafo 5 es objeto de disputas y dudas por separado, pero que la empresa así lo quisiera.
Inicio del proyecto
Entonces, vamos al sitio web oficial del marco y vemos cómo podemos comenzar a desarrollarnos. El sitio oficial ofrece instalar la herramienta sdkman. Una pieza que facilita el desarrollo y la gestión de proyectos en el marco de micronautos (y otros que incluyen, por ejemplo, Grails).

Una pequeña observación: si solo comienza la inicialización del proyecto sin ninguna clave, el gradle collector se selecciona de forma predeterminada. Elimine la carpeta, intente nuevamente, esta vez con la clave:
mn create-app com.room606.cryptonaut -b=maven
Un punto interesante es que sdkman, como Spring Tool Suite, le ofrece en la etapa de creación de un proyecto para establecer qué "cubos" desea usar al principio. No experimenté mucho con esto, también lo creé con un valor predeterminado predeterminado.
Finalmente, abrimos el proyecto en Intellij Idea y admiramos el conjunto de fuentes, recursos y discos que nos proporcionaron el asistente para crear el proyecto de micronautos.

Eye se aferra a 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
Bueno, es divertido y encomiable. Inmediatamente se nos proporcionó una herramienta para enviar rápidamente la aplicación al Prod / INT / QA / cualquier entorno. Para esto, un signo más mental para el proyecto.
Es suficiente recopilar el proyecto de Maven, luego recopilar la imagen de Docker y publicarla en su registro de Docker, o simplemente exportar la imagen binaria como una opción a su sistema de CI, así es como lo desea.
En la carpeta de recursos, también preparamos un espacio en blanco con los parámetros de configuración de la aplicación (análogo de application.properties en Spring), así como un archivo de configuración para la biblioteca logback. Genial!
Vamos al punto de entrada de la aplicación y estudiamos la clase. Vemos una imagen dolorosamente familiar de Spring Boot. Aquí, los desarrolladores del marco tampoco comenzaron a inventar ni a inventar nada.
public static void main(String[] args) throws IOException { Micronaut.run(Application.class); }
Compare con el código familiar de Spring.
public static void main(String[] args) { SpringApplication.run(Application.class, args); }
Es decir También elevamos el contenedor de IoC con todos los beans que se incluyen en el trabajo cuando son necesarios. Habiendo corrido un poco de acuerdo con la documentación oficial, comenzamos lentamente el desarrollo.
Necesitaremos:
- Modelos de dominio
- Controladores para implementar la API REST.
- Capa de almacenamiento de datos (cliente de base de datos u ORM u otra cosa)
- Un código de consumidores de datos de intercambios de criptomonedas, así como datos de intercambio de moneda fiduciaria. Es decir Necesitamos escribir los clientes más simples para servicios de terceros. En primavera, el conocido RestTemplate se adaptó bien a este papel.
- Configuración mínima para la administración flexible y el inicio de la aplicación (pensemos qué y cómo haremos la configuración)
- Pruebas! Sí, para alcanzar un código seguro y seguro e implementar una nueva funcionalidad, debemos estar seguros de la estabilidad del antiguo
- Almacenamiento en caché. Este no es un requisito básico, pero es algo que sería bueno tener para un buen rendimiento, y en nuestro escenario hay lugares donde el almacenamiento en caché es definitivamente una buena herramienta.
Spoiler: todo irá muy mal aquí.
Modelos de dominio
Para nuestros propósitos, los siguientes modelos serán suficientes: modelos de una cartera de criptomonedas, el tipo de cambio de un par de monedas fiduciarias, los precios de las criptomonedas en moneda fiduciaria y el valor total de la cartera.
A continuación se muestra el código de solo un par de modelos, el resto se puede ver en el repositorio . Y sí, era demasiado vago para meter a Lombok
en este proyecto.
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 ...
Controladores
Estamos tratando de escribir un controlador que implemente la API más simple, emitiendo el valor de las criptomonedas de acuerdo con los códigos de letras dados.
Es decir
GET /cryptonaut/restapi/prices.json?coins=BTC&coins=ETH&fiatCurrency=RUR
Debería producir algo como:
{"prices":[{"coin":"BTC","value":407924.043300000000},{"coin":"ETH","value":13040.638266000000}],"fiatCurrency":"RUR"}
De acuerdo con la documentación , nada complicado y recuerda los mismos enfoques y convenciones de 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); } }
Es decir especificamos con calma nuestro POJO
como el tipo devuelto, y sin configurar ningún serializador / deserializador, incluso sin colgar anotaciones adicionales Micronaut construirá el cuerpo http correcto con los datos del cuadro. Comparemos con Spring
way:
@RequestMapping(value = "/prices.json", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity<Prices> pricesAsJson(@RequestParam("userId") final String[] coins, @RequestParam("fiatCurrency") String fiatCurrency) {
En general, no tuve problemas con los controladores, solo funcionaron como se esperaba de ellos, según la documentación. Su ortografía era intuitiva y simple. Seguimos adelante.
Capa de almacenamiento de datos
Para la primera versión de la aplicación, solo almacenaremos la cartera del usuario. En general, mantendremos solo una cartera de un usuario. En pocas palabras, todavía no tendremos el soporte de muchos usuarios, solo un usuario principal con su cartera de criptomonedas. ¡Esto es asombroso!
Para implementar la persistencia de datos, la documentación ofrece opciones con conexión JPA, así como ejemplos fragmentarios del uso de diferentes clientes para leer desde la base de datos (sección "12.1.5 Configuración de Postgres"). JPA
descartó decisivamente y se dio preferencia a escribir y manipular consultas usted mismo. La configuración de la base de datos se agregó a application.yml (se eligió Postgres
como RDBMS), de acuerdo con la documentación:
postgres: reactive: client: port: 5432 host: localhost database: cryptonaut user: crypto password: r1ch13r1ch maxSize: 5
Dependiendo de la biblioteca postgres-reactive
se agregó. Este es un cliente para trabajar con la base de datos tanto de manera asíncrona como sincrónica.
<dependency> <groupId>io.micronaut.configuration</groupId> <artifactId>postgres-reactive</artifactId> <version>1.0.0.M4</version> <scope>compile</scope> </dependency>
Y finalmente, el archivo docker-compose.yml
se agregó a la docker-compose.yml
/ docker-compose.yml
para implementar el entorno futuro de nuestra aplicación, donde se agregó el componente de la base de datos:
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
A continuación se muestra el script de inicialización para la base de datos con una estructura de tabla muy simple:
CREATE TABLE portfolio ( id serial CONSTRAINT coin_amt_primary_key PRIMARY KEY, coin varchar(16) NOT NULL UNIQUE, amount NUMERIC NOT NULL );
Ahora intentemos lanzar un código que actualice la cartera del usuario. Nuestro componente de cartera se verá así:
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); }
Mirando el conjunto de métodos de Postgres reactive client
de Postgres reactive client
, estamos lanzando esta clase:
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 -> {
Lanza un entorno, intentamos actualizar nuestra cartera a través de una API implementada con prudencia por adelantado:
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); }
Realizar una solicitud de curl
:
curl http://localhost:8080/cryptonaut/restapi/portfolio -X POST -H "Content-Type: application/json" --data '{"coins": {"XRP": "35.5", "LSK": "5.03", "XEM": "16.23"}}' -v
Y ... detecta el error en los registros:
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) ...
Después de rascar nuestros nabos, no encontramos ninguna solución en el muelle oficial, tratamos de googlear el muelle en la biblioteca postgres-reactive
, y esta resulta ser la solución correcta, ya que se dan ejemplos y la sintaxis de consulta correcta en detalle. Resultó que era una cuestión de parámetros de marcador de posición, debe usar etiquetas numeradas de la forma $x ($1, $2, etc.)
. Entonces, la solución es reescribir la solicitud de destino:
private static final String UPDATE_COIN_AMT = "INSERT INTO portfolio (coin, amount) VALUES ($1, $2) ON CONFLICT (coin) " + "DO UPDATE SET amount = $3";
Reiniciamos la aplicación, intentamos la misma solicitud REST
... saludos. Los datos se suman. Pasemos a la lectura.
Nos enfrentamos a la tarea más simple de leer una cartera de criptomonedas de un usuario de la base de datos y asignarlas a un objeto POJO. Para estos fines, utilizamos el método 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; }
Conectamos todo esto junto con el controlador responsable de la cartera de criptomonedas:
@Controller("/cryptonaut/restapi/") public class ConfigController { ... @Get("/portfolio") @Produces(MediaType.APPLICATION_JSON) public Portfolio loadPortfolio() { return portfolioService.loadPortfolio(); } ...
Reiniciar el servicio. Para las pruebas, primero llenamos esta cartera con al menos algunos datos:
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
Ahora finalmente pruebe nuestra lectura de código de la base de datos:
curl http://localhost:8080/cryptonaut/restapi/portfolio -v
Y ... obtenemos ... algo extraño:
{"coins":{}}
Bastante extraño, ¿no? Verificamos la solicitud diez veces, intentamos volver a hacer la solicitud de curl
, incluso reiniciamos nuestro servicio. El resultado sigue siendo el mismo salvaje ... Después de volver a leer la firma del método, y también recordando que tenemos un Reactive Pg client
, llegamos a la conclusión de que estamos tratando con la asincronización. ¡Un debate reflexivo confirmó esto! Valió la pena descifrar un poco, como voila, ¡obtuvimos datos no vacíos!
Volviendo al muelle de la biblioteca nuevamente, arremangándonos las mangas, reescribimos el código con un código de bloqueo verdadero, pero completamente predecible:
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())); }
Ahora obtenemos lo que esperamos. Decidimos este problema, sigue adelante.
Escribimos un cliente para obtener datos del mercado.
Aquí, por supuesto, me gustaría resolver el problema con el menor número de bicicletas. El resultado son dos soluciones:
- bibliotecas de cliente listas para usar para acceder a intercambios de cifrado específicos
- el propio código de un pequeño cliente para solicitar el tipo de cambio. Lo que salió de la caja es Micronaut.
Con bibliotecas listas para usar, no todo es tan interesante. Solo noto que durante una búsqueda rápida, se seleccionó el proyecto https://github.com/knowm/XChange .
En principio, la arquitectura de la biblioteca es tan simple como tres centavos: hay un conjunto de interfaces para recibir datos, las principales interfaces y clases de modelos como Ticker
(puede encontrar bid
, ask
, todo tipo de precio de apertura, precio de cierre, etc.), CurrencyPair
, Currency
. Además, inicializa las implementaciones en el código, ya que previamente ha conectado la dependencia con la implementación que se refiere a un intercambio de cifrado específico. Y la clase principal a través de la cual operamos es MarketDataService.java
Por ejemplo, para nuestros experimentos, para empezar, estamos satisfechos con dicha "configuración":
<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>
A continuación se muestra el código que realiza una función clave: calcular el costo de una criptomoneda particular en términos fiduciarios (consulte las Fórmulas descritas al principio del artículo en el bloque de requisitos):
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); } } }
Todo se ha hecho en la medida de lo posible utilizando nuestras propias interfaces para ignorar ligeramente las implementaciones específicas proporcionadas por el proyecto https://github.com/knowm/XChange .
En vista del hecho de que en muchos, si no en todos los intercambios de criptomonedas, solo hay un conjunto limitado de monedas fiduciarias en circulación (USD, EUR, tal vez eso es todo ...), para la respuesta final a la pregunta del usuario, es necesario agregar otra fuente de datos: tasas de moneda fiduciaria, y También un convertidor opcional. Es decir Para responder a la pregunta, cuánto cuesta ahora la criptomoneda WTF en RUR (moneda objetivo, moneda objetivo), deberá responder dos subpreguntas: WTF / BaseCurrency (consideramos USD como tal), BaseCurrency / RUR, luego multiplique estos dos valores y produzca como resultado.
Para nuestra primera versión del servicio, solo admitiremos USD y RUR como monedas objetivo.
Por lo tanto, para respaldar RUR, sería aconsejable tomar fuentes que sean relevantes para la ubicación geográfica del servicio (lo alojaremos y lo utilizaremos exclusivamente en Rusia). En resumen, la tasa del Banco Central nos conviene. Se encontró una fuente abierta de dichos datos en Internet, que se puede consumir como JSON. Genial
A continuación se muestra la respuesta del servicio a la solicitud de tipo de cambio actual:
{ "Date": "2018-10-16T11:30:00+03:00", "PreviousDate": "2018-10-13T11:30:00+03:00", "PreviousURL": "\/\/www.cbr-xml-daily.ru\/archive\/2018\/10\/13\/daily_json.js", "Timestamp": "2018-10-15T23:00:00+03:00", "Valute": { "AUD": { "ID": "R01010", "NumCode": "036", "CharCode": "AUD", "Nominal": 1, "Name": "ђІЃ‚Ђ°»№Ѓє№ ґѕ»»°Ђ", "Value": 46.8672, "Previous": 46.9677 }, "AZN": { "ID": "R01020A", "NumCode": "944", "CharCode": "AZN", "Nominal": 1, "Name": "ђ·µЂ±°№ґ¶°ЅЃє№ ј°Ѕ°‚", "Value": 38.7567, "Previous": 38.8889 }, "GBP": { "ID": "R01035", "NumCode": "826", "CharCode": "GBP", "Nominal": 1, "Name": "¤ѓЅ‚ Ѓ‚µЂ»ЅіѕІ ЎѕµґЅµЅЅѕіѕ єѕЂѕ»µІЃ‚І°", "Value": 86.2716, "Previous": 87.2059 }, ...
En realidad, a continuación se CbrExchangeRatesClient
el CbrExchangeRatesClient
cliente 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); } } }
Aquí RxHttpClient
, un componente del Micronaut
. También nos da la opción de realizar el procesamiento o bloqueo de solicitudes asíncronas. Elegimos el bloqueo clásico:
httpClient.retrieve(req, String.class).blockingSingle();
Configuracion
En un proyecto, puede resaltar cosas que cambian y afectan la lógica de negocios o algunos aspectos específicos. Hagamos una lista de las monedas fiduciarias admitidas como propiedad e inyectemosla al inicio de la aplicación.
El siguiente código descartará los códigos de moneda para los que aún no podemos calcular el valor de la cartera:
public BigDecimal getFiatPrice(String baseCurrency, String counterCurrency) throws NotSupportedFiatException { if (!supportedCounterCurrencies.contains(counterCurrency)) { throw new NotSupportedFiatException("Counter currency not supported: " + counterCurrency); } Map<String, BigDecimal> rates = cbrExchangeRatesClient.getRates(); return rates.get(baseCurrency); }
En consecuencia, nuestra intención es inyectar de alguna manera el valor de application.yml
en la variable supportedCounterCurrencies
.
En la primera versión, dicho código fue escrito, debajo del campo de la clase FiatExchangeRatesService.java:
@Value("${cryptonaut.currencies:RUR}") private String supportedCurrencies; private final List<String> supportedCounterCurrencies = Arrays.asList(supportedCurrencies.split("[,]", -1));
Aquí, el placeholder
corresponde a la siguiente estructura del documento 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"
Lanzamiento de la aplicación, prueba rápida de humo ... ¡Error!
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
.

, . , (
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"
. — “Unexpected error occurred: No cache configured for name: 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
:
- API,
- API – “” .
– Rest Controller
– IoC-, .
“ ” :
| Micronaut | Spring Boot |
---|
Avg.(ms) | 2708.4 | 2735.2 |
cryptonaut (ms) | 1082 | - |
, – 27 Micronaut
. , .
?
. , , , – . . Groovy-, , . SO
Spring. , , . — . Spring.
:
- Micronaut – service-discovery, AWS
- Java. Kotlin Groovy.
.