Tente Micronaut ou Darling, eu reduzi o quadro
Sobre a estrutura do micronauta, tive um vislumbre do resumo do boletim informativo. Ele se perguntou que tipo de animal ele era. A estrutura é colocada em contraste com a mola recheada com todas as ferramentas necessárias.

Antecipando a próxima conferência para desenvolvedores, na qual eles apenas explicam e mostram como usar o micronauta em seus microsserviços, decidi me preparar pelo menos uma vez e apresentar pelo menos algum contexto em minha cabeça, com um certo conjunto de problemas e perguntas. Faça a lição de casa, por assim dizer. Decidi lançar um pequeno projeto para animais de estimação em algumas noites (como é o caso). No final do artigo, haverá um link para o repositório de todas as fontes do projeto.
Micronaut é uma estrutura da JVM que suporta três linguagens de desenvolvimento: Java, Kotlin, Groovy. Foi desenvolvido pela OCI, a mesma empresa que a Grails nos deu. Possui ajuste na forma de um aplicativo cli e um conjunto de bibliotecas recomendadas (vários clientes de http e banco de dados reativos)
Existe uma DI que implementa e repete as idéias do Spring, adicionando vários chips - assincronismo, suporte ao AWS Lambda, Client Side Load Balancing.
A ideia do serviço: um dos meus amigos de uma vez comprou uma meia dúzia de todo tipo de criptomoeda, investindo nelas umas férias impregnadas e um estoque de uma jaqueta de inverno. Todos sabemos que a volatilidade de toda essa substância de criptomoeda é selvagem e o tópico em si é geralmente imprevisível; um amigo acabou decidindo cuidar de seus nervos e simplesmente submergir no que está acontecendo com seus "ativos". Mas às vezes você ainda quer olhar, mas o que há com tudo isso, de repente, já está rico. Então surgiu a ideia de um painel simples (um painel, como o Grafana ou algo mais simples), uma determinada página da Web com informações secas, quanto custa em alguma moeda fiduciária (USD, RUR).
Isenções de responsabilidade
- Vamos deixar a conveniência de escrever nossa própria solução, só precisamos experimentar a nova estrutura em algo mais astuto que o HelloWorld.
- Algoritmo de cálculo, erros esperados, erros, etc. (pelo menos para a primeira fase do produto), a validade da escolha de trocas de criptografia para obter informações, o portfólio de criptografia de "investimento" de um amigo também estará fora de questão e não estará sujeito a discussão ou algum tipo de análise profunda.
Portanto, um pequeno conjunto de requisitos:
- Serviço da Web (acesso externo, via http)
- Exibindo uma página em um navegador com um resumo do valor total do portfólio de criptomoedas
- Capacidade de configurar o portfólio (escolha o formato JSON para carregar e descarregar a estrutura do portfólio). Uma certa API REST para atualizar e carregar um portfólio, ou seja, 2 API: para salvar / atualizar - POST, para descarregar - GET. A estrutura do portfólio é essencialmente uma placa de identificação simples
BTC – 0.00005 . XEM – 4.5 . ...
- Coletamos dados de trocas criptográficas e de fontes de câmbio (para moedas fiduciárias)
- Regras para o cálculo do valor total do portfólio:

Obviamente, tudo o que está escrito no parágrafo 5 é objeto de disputas e dúvidas separadas, mas seja que a empresa o desejasse.
Início do projeto
Então, vamos ao site oficial da estrutura e vemos como podemos começar a nos desenvolver. O site oficial oferece a instalação da ferramenta sdkman. Uma peça que facilita o desenvolvimento e o gerenciamento de projetos na estrutura do micronauta (e outros, incluindo, por exemplo, Grails).

Uma pequena observação: se você acabou de iniciar a inicialização do projeto sem nenhuma tecla, o coletor de níveis é selecionado por padrão. Exclua a pasta, tente novamente, desta vez com a tecla:
mn create-app com.room606.cryptonaut -b=maven
Um ponto interessante é que o sdkman, como o Spring Tool Suite, oferece a você no estágio de criação de um projeto para definir quais “cubos” você deseja usar no início. Não experimentei muito isso, também criei com uma predefinição padrão.
Por fim, abrimos o projeto no Intellij Idea e admiramos o conjunto de fontes, recursos e discos que foram fornecidos ao assistente para criar o projeto de micronauta.

Olho se apega ao 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
Bem, é divertido e louvável. Foi-nos fornecida imediatamente uma ferramenta para enviar rapidamente o aplicativo para o ambiente Prod / INT / QA / qualquer que seja o ambiente. Para isso, um sinal de mais mental para o projeto.
Basta coletar o projeto pelo Maven, coletar a imagem do Docker e publicá-la no registro do Docker ou simplesmente exportar a imagem binária como uma opção para o sistema de IC, é assim que você deseja.
Na pasta de recursos, também preparamos um espaço em branco com os parâmetros de configuração do aplicativo (analógico de application.properties no Spring), bem como um arquivo de configuração para a biblioteca de logback. Legal!
Vamos ao ponto de entrada da aplicação e estudamos a turma. Vemos uma imagem dolorosamente familiar para nós da Spring Boot. Aqui, os desenvolvedores da estrutura também não começaram a inventar e inventar nada.
public static void main(String[] args) throws IOException { Micronaut.run(Application.class); }
Compare com o código familiar do Spring.
public static void main(String[] args) { SpringApplication.run(Application.class, args); }
I.e. também elevamos o contêiner de IoC com todos os beans incluídos no trabalho conforme necessário. Tendo executado um pouco de acordo com a documentação oficial, começamos lentamente o desenvolvimento.
Vamos precisar de:
- Modelos de domínio
- Controladores para implementar a API REST.
- Camada de armazenamento de dados (cliente de banco de dados ou ORM ou outra coisa)
- O código dos consumidores de dados de trocas criptográficas, bem como dados da troca de moedas fiduciárias. I.e. precisamos escrever os clientes mais simples para serviços de terceiros. Na primavera, o conhecido RestTemplate se adequava bem a essa função.
- Configuração mínima para gerenciamento flexível e início do aplicativo (vamos pensar sobre o que e como faremos a configuração)
- Testes! Sim, para reconfigurar o código com confiança e segurança e implementar novas funcionalidades, precisamos ter certeza da estabilidade do antigo
- Armazenamento em cache. Este não é um requisito básico, mas algo que seria bom ter para um bom desempenho e, em nosso cenário, há lugares em que o cache é definitivamente uma boa ferramenta.
Spoiler: tudo vai ficar muito ruim aqui.
Modelos de domínio
Para nossos propósitos, os seguintes modelos serão suficientes: modelos de um portfólio de criptomoedas, a taxa de câmbio de um par de moedas fiduciárias, preços de criptomoedas em moeda fiduciária e o valor total do portfólio.
Abaixo está o código de apenas alguns modelos, o restante pode ser visualizado no repositório . E sim, eu estava com preguiça de ferrar Lombok
neste projeto.
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 tentando escrever um controlador que implemente a API mais simples, emitindo o valor das criptomoedas de acordo com os códigos de letras das moedas.
I.e.
GET /cryptonaut/restapi/prices.json?coins=BTC&coins=ETH&fiatCurrency=RUR
Deve produzir algo como:
{"prices":[{"coin":"BTC","value":407924.043300000000},{"coin":"ETH","value":13040.638266000000}],"fiatCurrency":"RUR"}
De acordo com a documentação , nada complicado e lembra as mesmas abordagens e convenções do 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); } }
I.e. calmamente especificamos nosso POJO
como o tipo retornado e, sem configurar serializadores / desserializadores, mesmo sem anotar anotações adicionais, o Micronaut criará o corpo http correto com os dados da caixa. Vamos comparar com o modo 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) {
Em geral, não tive problemas com os controladores, eles apenas funcionaram conforme o esperado, de acordo com a documentação. A ortografia deles era intuitiva e simples. Nós seguimos em frente.
Camada de armazenamento de dados
Para a primeira versão do aplicativo, armazenaremos apenas o portfólio do usuário. Em geral, manteremos apenas um portfólio de um usuário. Simplificando, ainda não teremos o suporte de muitos usuários, apenas um usuário principal com seu portfólio de criptomoedas. Isso é demais!
Para implementar a persistência de dados, a documentação oferece opções com conexão JPA, bem como exemplos fragmentários de uso de diferentes clientes para ler no banco de dados (seção “12.1.5 Configurando o Postgres”). JPA
descartado decisivamente e foi dada preferência à escrita e manipulação de consultas. A configuração do banco de dados foi adicionada ao application.yml (o Postgres
foi escolhido como RDBMS), de acordo com a documentação:
postgres: reactive: client: port: 5432 host: localhost database: cryptonaut user: crypto password: r1ch13r1ch maxSize: 5
Dependendo da biblioteca postgres-reactive
foi adicionado. Este é um cliente para trabalhar com o banco de dados de maneira assíncrona e síncrona.
<dependency> <groupId>io.micronaut.configuration</groupId> <artifactId>postgres-reactive</artifactId> <version>1.0.0.M4</version> <scope>compile</scope> </dependency>
E, finalmente, o arquivo docker-compose.yml
foi adicionado à docker-compose.yml
/ docker-compose.yml
para implantar o ambiente futuro do nosso aplicativo, onde o componente do banco de dados foi adicionado:
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
Abaixo está o script de inicialização do banco de dados com uma estrutura de tabela muito simples:
CREATE TABLE portfolio ( id serial CONSTRAINT coin_amt_primary_key PRIMARY KEY, coin varchar(16) NOT NULL UNIQUE, amount NUMERIC NOT NULL );
Agora, vamos tentar lançar um código que atualize o portfólio do usuário. Nosso componente do portfólio ficará assim:
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); }
Observando o conjunto de métodos de Postgres reactive client
do Postgres reactive client
, estamos lançando esta classe:
package com.room606.cryptonaut; import com.room606.cryptonaut.domain.Portfolio; import com.room606.cryptonaut.markets.CryptoMarketDataService; import io.micronaut.context.annotation.Requires; import io.reactiverse.pgclient.Numeric; import io.reactiverse.reactivex.pgclient.*; import javax.inject.Inject; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class PortfolioServiceImpl implements PortfolioService { private final PgPool pgPool; ... private static final String UPDATE_COIN_AMT = "INSERT INTO portfolio (coin, amount) VALUES (?, ?) ON CONFLICT (coin) " + "DO UPDATE SET amount = ?"; ... public Portfolio savePortfolio(Portfolio portfolio) { List<Tuple> records = portfolio.getCoins() .entrySet() .stream() .map(entry -> Tuple.of(entry.getKey(), Numeric.create(entry.getValue()), Numeric.create(entry.getValue()))) .collect(Collectors.toList()); pgPool.preparedBatch(UPDATE_COIN_AMT, records, pgRowSetAsyncResult -> {
Lança um ambiente, tentamos atualizar nosso portfólio por meio de uma API prudentemente implementada com antecedência:
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); }
Execute uma solicitação 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
E ... pegue o erro nos logs:
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) ...
Depois de coçar os nabos, não encontramos nenhuma solução no dock oficial, tentamos fazer o google no dock na própria biblioteca postgres-reactive
do postgres-reactive
, e essa é a solução certa, pois exemplos e sintaxe de consulta correta são fornecidos em detalhes. Foi uma questão de parâmetros de espaço reservado, você precisa usar rótulos numerados no formato $x ($1, $2, etc.)
. Portanto, a correção é reescrever a solicitação 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 o aplicativo, tente a mesma solicitação REST
... Saúde. Os dados são adicionados. Vamos para a leitura.
Somos confrontados com a tarefa mais simples de ler um portfólio de criptomoedas de um usuário no banco de dados e mapeá-las para um objeto POJO. Para esses propósitos, usamos o 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 tudo isso ao controlador responsável pelo portfólio de criptomoedas:
@Controller("/cryptonaut/restapi/") public class ConfigController { ... @Get("/portfolio") @Produces(MediaType.APPLICATION_JSON) public Portfolio loadPortfolio() { return portfolioService.loadPortfolio(); } ...
Reinicie o serviço. Para o teste, primeiro preenchemos esse portfólio com pelo menos alguns dados:
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
Agora, finalmente, teste nossa leitura de código no banco de dados:
curl http://localhost:8080/cryptonaut/restapi/portfolio -v
E ... temos ... algo estranho:
{"coins":{}}
Muito estranho, não é? Verificamos novamente a solicitação dez vezes, tentamos fazer a solicitação novamente, e até reiniciamos o serviço. O resultado ainda é o mesmo selvagem ... Depois de reler a assinatura do método e também lembrar que temos um Reactive Pg client
, chegamos à conclusão de que estamos lidando com a assincronização. Debag pensativo confirmou isso! Valeu a pena um pouco de código de lazer, como voila, temos dados não vazios!
Voltando à estação de bibliotecas novamente, arregaçando as mangas, reescrevemos o código com um código de bloqueio verdadeiro, mas completamente previsível:
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())); }
Agora conseguimos o que esperamos. Decidimos esse problema, siga em frente.
Escrevemos um cliente para obter dados de mercado
Aqui, é claro, eu gostaria de resolver o problema com o menor número de bicicletas. O resultado são duas soluções:
- bibliotecas cliente prontas para acessar trocas criptográficas específicas
- o código de um cliente pequeno para solicitar a taxa de câmbio. O que saiu da caixa é o Micronaut.
Com bibliotecas prontas, nem tudo é tão interessante. Observo apenas que, durante uma pesquisa rápida, o projeto https://github.com/knowm/XChange foi selecionado.
Em princípio, a arquitetura da biblioteca é tão simples quanto três moedas de um centavo - existe um conjunto de interfaces para recebimento de dados, as principais interfaces e classes de modelo, como o Ticker
(você pode encontrar bid
, ask
, todos os tipos de preço de abertura, preço de fechamento etc.), CurrencyPair
, Currency
. Além disso, você inicializa as próprias implementações no código, conectando anteriormente a dependência à implementação que se refere a uma troca de criptografia específica. E a principal classe através da qual operamos é MarketDataService.java
Por exemplo, para nossos experimentos, para iniciantes, estamos satisfeitos com essa "configuração":
<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>
Abaixo está o código que executa uma função principal - calculando o custo de uma determinada criptomoeda em termos fiduciários (consulte Fórmulas descritas no começo do artigo no bloco 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); } } }
Tudo foi feito na medida do possível, usando nossas próprias interfaces para ignorar levemente as implementações específicas fornecidas pelo projeto https://github.com/knowm/XChange .
Considerando que em muitas, se não todas as trocas de criptomoedas, há apenas um conjunto limitado de moedas fiduciárias em circulação (USD, EUR, talvez isso seja tudo ...), para a resposta final à pergunta do usuário, é necessário adicionar outra fonte de dados - taxas de moeda fiduciária e também um conversor opcional. I.e. Para responder à pergunta, quanto custa a criptomoeda WTF em RUR (moeda de destino, moeda de destino) agora, você terá que responder a duas sub-perguntas: WTF / BaseCurrency (consideramos USD como tal), BaseCurrency / RUR, multiplique esses dois valores e forneça como resultado.
Para nossa primeira versão do serviço, ofereceremos suporte apenas a USD e RUR como moedas de destino.
Portanto, para dar suporte ao RUR, seria aconselhável pegar fontes relevantes para a localização geográfica do serviço (nós o hospedaremos e o usaremos exclusivamente na Rússia). Em suma, a taxa do Banco Central nos convém. Um código aberto desses dados foi encontrado na Internet, que pode ser consumido como JSON. Ótimo.
Abaixo está a resposta do serviço à solicitação de taxa de câmbio atual:
{ "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 }, ...
Na verdade, abaixo está o 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); } } }
Aqui injetamos o RxHttpClient
, um componente do Micronaut
. Também nos oferece a opção de processar ou bloquear solicitações assíncronas. Escolhemos o bloqueio clássico:
httpClient.retrieve(req, String.class).blockingSingle();
Configuração
Em um projeto, você pode destacar coisas que mudam e afetam a lógica de negócios ou alguns aspectos específicos. Vamos fazer uma lista de moedas fiduciárias suportadas como uma propriedade e injetá-la no início do aplicativo.
O código a seguir descartará os códigos de moeda para os quais ainda não conseguimos calcular o valor do portfólio:
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); }
Assim, nossa intenção é injetar, de alguma forma, o valor de application.yml
na variável supportedCounterCurrencies
.
Na primeira versão, esse código foi escrito, abaixo do campo da classe FiatExchangeRatesService.java:
@Value("${cryptonaut.currencies:RUR}") private String supportedCurrencies; private final List<String> supportedCounterCurrencies = Arrays.asList(supportedCurrencies.split("[,]", -1));
Aqui, o placeholder
corresponde à seguinte estrutura do 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"
Lançamento do aplicativo, teste rápido de fumaça ... Erro!
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
.
No entanto, tudo foi completamente mal sucedido aqui. A documentação neste aspecto é confusa, onde, depois de rolar por algumas telas, você encontra partes de configurações que se contradizem ( appliction.yml
). Como um cache, redis foi escolhido, também levantado no contêiner do Docker próximo a ele. Aqui está sua configuração: redis: image: 'bitnami/redis:latest' environment: - ALLOW_EMPTY_PASSWORD=yes ports: - '6379:6379'
E aqui está um trecho de código anotado por @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); } }
Mas com application.yml
foi o mistério mais importante. Eu tentei todos os tipos de configurações. Aqui está um:
caches: fiatrates: expireAfterWrite: "1h" redis: caches: fiatRates: expireAfterWrite: "1h" port: 6379 server: localhost
Aqui está um:
#cache redis: uri: localhost:6379 caches: fiatRates: expireAfterWrite: "1h"
E até tentou remover as letras maiúsculas no nome do cache. Mas, como resultado, obtive o mesmo resultado ao iniciar o aplicativo - "Ocorreu um erro inesperado: nenhum cache configurado para o nome: 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-.
Aqui, é claro, você teria que indicar com uma dúzia de Isenções de responsabilidade: que eu não sou especialista em benchmark, sobre o método de iniciar e medir o horário de início, sobre as condições experimentais (carga da máquina, configuração de hardware, sistema operacional, etc.).No entanto, o último ponto:
Sistema operacional: 16.04.1-Ubuntu x86_64 x86_64 x86_64
CPU GNU / Linux : CPU Intel® Core (TM) i7-7700HQ a 2.80GHz
Mem: 2 x 8 Gb DDR4, velocidade: 2400 MHz
Disco SSD: PCIe NVMe M SSD 2, 256 GB
Mina defesa técnica:
- Resgatar um carrinho de mão
- Ligue o carro
- Início da aplicação
- Paralelamente, o código do cliente em um loop pesquisa uma API, que simplesmente retorna uma linha na resposta
- Assim que uma resposta da API é recebida, o "timer" para.
- O resultado em milissegundos é inserido com cuidado no tablet
– 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.
.