Arquivo
Python: Funções Parciais
Suponha que você precise chamar frequentemente uma função que receba cinco parâmetros:
def func(p1, p2, p3, p4, p5):
pass
# Vendo essa assinatura, o Uncle Bob (Clean Code) chora!
É muito chato chamar essa função muitas vezes variando apenas um ou dois desses parâmetros. Para encapsular uma função fixando os parâmetros que você precisar, pode utilizar functools.partial.
from functools import partial func_partial = partial(func, 1, 2, 3, 4)
Sempre que precisarmos da função “func” variando apenas o quinto parâmetro, é só chamar assim:
func_partial(7) func_partial(8) func_partial(9)
Python: Decorador Condicional
Decorators são muito úteis para desacoplar partes da implementação de uma função. É possível deixar um decorator em Python bastante versátil usando condicionais. Nesse exemplo, criei um decorator que recebe um bool por parâmetro. Esse parâmetro permite condicionarmos o cálculo do tempo de execução de uma função:
import time
def duration(bypass):
def decorator(func):
def wrapper(*args, **kwargs):
print(f'FUNCAO: {func.__name__}')
if bypass:
print("O tempo de execucao nao foi calculado")
return func(*args)
else:
start = time.time()
ret = func(*args)
end = time.time()
print(f'Tempo de execucao: {(end - start):.10f}')
return ret
return wrapper
return decorator
@duration(bypass=True)
def add(x, y):
return x + y
@duration(bypass=False)
def sub(x, y):
return x - y
add(10, 5)
sub(10, 5)
Como Exibir uma Barra de Progresso no Console
Você já se perguntou como é gerada uma barra de progresso no console? Basicamente, você imprime uma sequência de caracteres que exibe o quanto falta para acabar e vai substituindo cada um desses caracteres por algum caractere que indica avanço. Achei um código bem simples que faz isso:
import java.util.stream.Stream;
public class JavaConsole
{
public static void printMsgWithProgressBar(String message, int length, long timeInterval)
{
char incomplete = '░'; // U+2591 Unicode Character
char complete = '█'; // U+2588 Unicode Character
StringBuilder builder = new StringBuilder();
Stream.generate(() -> incomplete).limit(length).forEach(builder::append);
System.out.println(message);
for(int i = 0; i < length; i++)
{
builder.replace(i,i+1,String.valueOf(complete));
String progressBar = "\r"+builder;
System.out.print(progressBar);
try
{
Thread.sleep(timeInterval);
}
catch (InterruptedException ignored)
{
}
}
}
public static void main(String[] args)
{
printMsgWithProgressBar("Loading", 55, 120);
}
}
Claro, para calcular o total de tempo e o progresso, você vai fazer algum cálculo baseado no tamanho do arquivo, largura da banda, processamento ou alguma outra medida, mas no final, você vai exibir alguma coisa assim no console:
Python: Primeiras Impressões
Faz algumas semanas, comecei a estudar Python. Eu já estava vendo algumas coisas de Python, mas quando foquei meus estudos, comecei a gostar bastante do que essa linguagem oferece. Trabalho faz muitos anos com Java e em alguns momentos já precisei atuar com outras linguagens (Javascript não conta). Sempre é bom se aventurar em coisas novas para aumentar nossas opções para resolver problemas.
Você pode programar em Python com estilo funcional, orientado a objetos ou ainda pode combinar os dois! Nesse artigo, vou mostrar algumas coisas interessantes que descobri utilizando o estilo funcional. Talvez, no futuro, eu escreva um post com alguma coisa interessante do Python orientado a objetos. Utilizei o IDE PyCharm, que é bem parecido com o IntelliJ, mas você poderia utilizar VSCode, Jupyter Notebook, etc.
O código abaixo retorna o n-ésimo elemento da sequência de Fibonacci de forma recursiva:
def fibonacci(n: int):
if n == 1:
return 0
if n == 2:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
Não é relevante, mas por curiosidade a regra de formação dessa sequência é essa:
F(n) = 0, F(n) = 1, F(n) = F(n-1) + F(n-2)
No código anterior, note que a declaração da função é bem limpa. É necessário respeitar a identação se você estiver utilizando alguma estrutura que possa ter uma série de instruções subordinadas (declaração de função, condicionais, estruturas de repetição, etc). O parâmetro “n” seria avaliado por Duck Typing se não tivéssemos informado que o tipo dele é “int” de forma coercitiva. Dependendo do nível de abstração, acho interessante tipar os parâmetros e as variáveis que estivermos trabalhando na função.
Pensei em gerar uma lista com o primeiros 5 elementos da sequência de Fibonacci. Automaticamente, eu utilizaria uma estrutura de repetição para isso, mas preferi criar um gerador para a sequência de índices. O gerador me dará o próximo número da sequência através do comando “yield” sempre que eu chamar a função. Esse comando suspende a execução da função – não é um sleep – até que ela seja chamada novamente se eu não tiver executado um “return”. Para um valor baixo, gerar uma lista em memória (com list comprehension, por exemplo) não faz diferença, mas se você tentar gerar uma lista com milhões de elementos, pode ser que a memória disponível acabe:
def index_generator(limit: int):
n: int = 0
while True:
n += 1
if n > limit:
return
yield n
O código abaixo utiliza uma lambda e a função map() para gerar uma lista de 5 elementos da sequência de Fibonacci:
sequence: List[int] = list(map(lambda n: fibonacci(n), index_generator(5))) Output: [0, 1, 1, 2, 3]
O código abaixo utiliza a função zip() para combinar a lista de índices e a sequência de Fibonacci 1 a 1 em um dicionário:
indexed_sequence = list(zip(index_generator(5), sequence)) Output: [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3)]
O código abaixo gera um Json com o dicionário anterior. Concordo que dificilmente eu criaria um Json com informações modeladas assim:
js = json.dumps(dict(indexed_sequence))
Output:
{"1": 0, "2": 1, "3": 1, "4": 2, "5": 3}
List comprehension é uma característica que existe em outras linguagens. Consiste em criar uma lista à partir de outras listas. Nesse exemplo, à partir da combinação de uma lista com o tempo de empresa de algumas pessoas e outra com os nomes dessas pessoas, criamos uma terceira lista com uma mensagem informando quem já tem mais de 30 anos de empresa:
lista_tempo_empresa = [10, 20, 27, 28, 31, 37]
lista_nomes = ['José', 'Mário', 'Ana', 'Adolfo', 'Leopoldo', 'Francisco']
lista_funcionarios = list(zip(lista_nomes, lista_tempo_empresa))
lista_funcionarios_antigos = [f'{nome} já tem {tempo} anos de empresa!' for nome, tempo in lista_funcionarios if tempo > 30]
print(lista_funcionarios_antigos)
Output:
['Leopoldo já tem 31 anos de empresa!', 'Francisco já tem 37 anos de empresa!']
Um último assunto interessante que eu queria destacar é o uso do decorator. Imagine que a função abaixo está demorando muito para executar:
def somar(x, y):
return x + y
Em princípio, alteraríamos esse código para inserir informações que pudessem ser coletadas e analisadas posteriormente. O Decorator é um padrão que pode ser implementado de formas diferentes em linguagens diferentes. A forma como o decorator funciona em Python é bem interessante e o resultado final me fez lembrar de Aspectos:
def time_decorator(funcao, *args):
def time_wrapper(*args):
inicio = time.time()
soma = funcao(*args)
fim = time.time()
print(f'Demorou {(fim - inicio):.10f}')
return soma
return time_wrapper
Para criar um decorator em Python, primeiro é necessário criar uma função que receba a função que será decorada e os argumentos que essa função decorada precise receber. Dentro do decorator, precisamos criar uma função wrapper que retornará uma referência para ela mesma. É na implementação desse wrapper que chamaremos a função original e onde adicionaremos o código que servirá para análise do tempo de processamento. E para chamar a função, fazemos assim:
soma_temporizada = time_decorator(somar)
print(f'Resultado da soma: {soma_temporizada(2, 3)}')
Output:
Resultado da soma: 5
Mas fica meio feio, não? Para cada função que quisermos monitorar será necessário repetir esse código de chamada. Felizmente, o Python permite que chamemos o decorator através de uma anotação. Inclusive, essa técnica também é utilizada pelo framework Flask, por exemplo:
@time_decorator
def somar(x, y):
return x + y
E o resultado será o mesmo, mas dessa vez chamamos diretamente a função original:
print(f'Resultado da soma: {somar(2, 3)}')
Output:
Resultado da soma: 5
Referências
1. WILL. Aula 3 – Geradores: você pode travar qualquer PC com 1 linha de Python, sabia?.. HashLDash. Disponível em: [https://www.youtube.com/watch?v=bjebnCnwwJg]. Acesso em 27 nov. 2022.
2. ______. Aula 6 – Python Avançado – Decoradores e como utilizá-los.. HashLDash. Disponível em: [https://www.youtube.com/watch?v=AF8AKk9RYUQ]. Acesso em 27 nov. 2022.
Como Gerar Dados para Testes com Java Faker
Muitas vezes, nossos testes unitários precisam apenas de um conjunto de caracteres ou números para passarem. Porém, às vezes não estamos nos sentindo muito criativos e acabamos utilizando a velha sequência “123” e o famoso “José da Silva”. Se você não estiver se sentindo muito criativo, pode utilizar o Java Faker, que é uma biblioteca para gerar dados para testes.
Vamos ver com o Java Faker funciona. Adicione a dependência abaixo:
<dependency> <groupId>com.github.javafaker</groupId> <artifactId>javafaker</artifactId> <version>1.0.2</version> <scope>test</scope> </dependency>
Se você precisa preencher um endereço, pode utilizar o “address”. Em sua versão localizada, você pode gerar nomes em seu idioma de preferência:
@Test
void addressFaker() {
Faker faker = new Faker(new Locale("pt-BR"));
String firstName = faker.address().firstName();
String lastName = faker.address().lastName();
String streetName = faker.address().streetName();
System.out.println("Nome: " + firstName);
System.out.println("Sobrenome: " + lastName);
System.out.println("Logradouro: " + streetName);
}
Nome: Elisa Sobrenome: Batista Logradouro: Rodovia Clara
Se você for fã de alguma série, livro, etc., pode ser que exista um faker específico, como no caso do Harry Potter Faker:
@Test
void harryPotterFaker() {
Faker faker = new Faker();
String character = faker.harryPotter().character();
String book = faker.harryPotter().book();
System.out.println("Personagem: " + character);
System.out.println("Livro: " + book);
}
Personagem: Norbert Livro: Harry Potter and the Half-Blood Prince
Se precisar gerar um número qualquer:
@Test
void fakeValuesServiceNumerify() {
FakeValuesService fakeValuesService = new FakeValuesService(new Locale("pt-BR"), new RandomService());
String randomNumber = fakeValuesService.numerify("##");
System.out.println("Numero gerado: " + randomNumber);
}
Numero gerado: 67
Se precisar gerar uma letra qualquer:
@Test
void fakeValuesServiceLetterify() {
FakeValuesService fakeValuesService = new FakeValuesService(new Locale("pt-BR"), new RandomService());
String randomLetters = fakeValuesService.letterify("?");
System.out.println("Letra gerada: " + randomLetters);
}
Letra gerada: g
Se precisar gerar uma string combinando letras e números gerados:
@Test
void fakeValuesServiceBothify() {
FakeValuesService fakeValuesService = new FakeValuesService(new Locale("pt-BR"), new RandomService());
String randomNumbersLetters = fakeValuesService.bothify("numeros ## e letras ??");
System.out.println("Valores gerados: " + randomNumbersLetters);
}
Valores gerados: numeros 06 e letras ry
Se precisar gerar uma string à partir de um padrão:
@Test
void fakeValuesServiceRegexify() {
FakeValuesService fakeValuesService = new FakeValuesService(new Locale("pt-BR"), new RandomService());
String randomBasedRegex = fakeValuesService.regexify("[abc]+\\s\\d");
System.out.println("String gerada a partir do padrao informado: " + randomBasedRegex);
}
String gerada a partir do padrao informado: cab 7
Referências
1. ROTSAERT, Gunter. How to Generate Fake Test Data.. DZone. Disponível em:[https://dzone.com/articles/how-to-generate-fake-test-data]. Acesso em 12 maio. 2022.
Resilience4J: Integrando Circuit Breaker e Retry
Em outro artigo, expliquei o funcionamento de um Circuit Breaker utilizando o Hystrix, mas essa biblioteca foi descontinuada. Nesse artigo, vou mostrar um exemplo de uso de Circuit Breaker e Retry com Resilience4J.
Resilience4j é uma biblioteca para implementação de APIs tolerantes à falhas projetado para Java 8+ e programação funcional. Adicione as dependências abaixo:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
Retry
Primeiro, vamos implementar um Retry. Crie uma classe de serviço que faz acesso a uma outra URL qualquer. Defini uma URL local onde eu poça subir algum servlet container:
@org.springframework.stereotype.Service
public class Service {
private String serviceUrl = "http://localhost:8081/pessoa/buscar";
@Retry(name = "pessoaRetry", fallbackMethod = "fallback")
public String buscar() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(serviceUrl, String.class);
}
public String fallback(Exception e) {
return "Sem comunicacao com o servidor";
}
}
Realize as configurações abaixo para o seu Retry:
management:
endpoints:
web:
exposure:
include: "*"
resilience4j:
retry:
instances:
pessoaRetry:
max-attempts: 3
wait-duration: 4s
enable-exponential-backoff: true
exponential-backoff-multiplier: 2
retry-exceptions:
- org.springframework.web.client.RestClientException
ignore-exceptions:
- br.com.app.MinhaEception
Nesse exemplo, serão realizadas 3 tentativas de acesso ao serviço, mas o tempo de espera entre cada requisição será de 4s apenas na primeira tentativa. O tempo de espera entre cada requisição recebe um fator de multiplicação 2 (exponential-backoff-multiplier) em função da interação:
4 * 2^0 = 4s
4 x 2^1 = 8s
4 x 2^2 = 16s
retry-exceptions indica quais exceções causarão uma nova tentativa de requisição (retry) e ignore-exceptions indica quais exceções não causarão um novo retry. Faça algumas chamadas ao seu serviço o observe os 3 retries sendo executados de acordo com os tempos de espera calculados.
Retry Escorado por um Circuit Breaker
Implementando um Retry, aumentamos as chances de que uma determinada requisição de um client possa ser atendida, pois tentamos acessar um determinado endpoint algumas vezes antes que ele falhe, porém, imagine que o endpoint de destino realmente esteja indisponível ou esteja enfrentando algum outro problema. Seria interessante cortar a conexão com esse endpoint para impedir a propagação de falhas em cascata. É aqui que entra o Circuit Breaker. Vamos combinar o Retry e o Circuit Breaker para criar um sistema tolerante a falhas mais robusto. Modifique a classe de serviço criada:
@org.springframework.stereotype.Service
public class Service {
private String serviceUrl = "http://localhost:8081/pessoa/buscar";
@CircuitBreaker(name = "pessoaCircuitBreaker", fallbackMethod = "fallback")
@Retry(name = "pessoaRetry")
public String buscar() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(serviceUrl, String.class);
}
public String fallback(Exception e) {
return "Sem comunicacao com o servidor";
}
}
O Retry pode coexistir com o CircuitBreaker. Por padrão, o CircuitBreaker é executado primeiro no Spring AOP, mas se você quiser que o Retry seja executado primeiro, basta definir para o Retry uma aspect-order maior do que a aspect-order definida pelo CircuitBreaker:
management:
endpoints:
web:
exposure:
include: "*"
resilience4j:
retry:
retry-aspect-order: 2
instances:
pessoaRetry:
max-attempts: 3
wait-duration: 4s
enable-exponential-backoff: true
exponential-backoff-multiplier: 2
retry-exceptions:
- org.springframework.web.client.RestClientException
ignore-exceptions:
- br.com.app.MinhaEception
circuitbreaker:
circuit-breaker-aspect-order: 1
instances:
pessoaCircuitBreaker:
wait-duration-in-open-state: 1m
permitted-number-of-calls-in-half-open-state: 3
sliding-window-type: count-based
sliding-window-size: 5
minimum-number-of-calls: 5
slow-call-duration-threshold: 10s
slow-call-rate-threshold: 60
failure-rate-threshold: 60
O Retry é executado 3 vezes conforme configuramos anteriormente, mas depois dessas 3 tentativas, o CircuitBreaker entra em ação e invoca o fallbackMethod depois de executar a configuração que fizemos.
Nesse exemplo, configuramos uma sliding-window de tamanho 5 e do tipo count – outro tipo é time. Isso significa que 5 eventos serão armazenados de forma rotativa. Após o mínimo de 5 eventos (minimum-number-of-calls), as demais restrições serão analisadas.
Se 60% das falhas (failure-rate-threshold) – o que representa 3 dos 5 eventos sequenciais observados (minimum-number-of-calls) – ou 60% das requisições demorarem mais que 10 segundos para responder (slow-call-rate-threshold e slow-call-duration-threshold), o estado muda de CLOSED para OPEN e permanece nesse estado por 1m (slow-call-duration-threshold) antes de mudar para o estado HALF-OPEN.
No estado OPEN, não serão feitas novas requisições para o endpoint de destino. HALF-OPEN é um estado de avaliação. 3 requisições são permitidas nesse estado (permitted-number-of-calls-in-half-open-state) e os percentuais de falha ou chamadas lentas serão calculados com base nessas 3 chamadas. Se 2 das 3 chamadas forem lentas ou resultarem em falha, o Circuit Breaker passa para o estado OPEN. Caso contrário, passa para o estado CLOSED.
Analisando Logs de Eventos
Você pode interceptar os eventos gerados pelo Retry e pelo Circuit Breaker
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.core.registry.EntryAddedEvent;
import io.github.resilience4j.core.registry.EntryRemovedEvent;
import io.github.resilience4j.core.registry.EntryReplacedEvent;
import io.github.resilience4j.core.registry.RegistryEventConsumer;
import io.github.resilience4j.retry.Retry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Config {
@Bean
public RegistryEventConsumer<Retry> myRetryRegistryEventConsumer() {
return new RegistryEventConsumer<Retry>() {
@Override
public void onEntryAddedEvent(EntryAddedEvent<Retry> entryAddedEvent) {
entryAddedEvent.getAddedEntry().getEventPublisher()
.onEvent(event -> System.out.println(event.toString()));
}
@Override
public void onEntryRemovedEvent(EntryRemovedEvent<Retry> entryRemoveEvent) {
}
@Override
public void onEntryReplacedEvent(EntryReplacedEvent<Retry> entryReplacedEvent) {
}
};
}
@Bean
public RegistryEventConsumer<CircuitBreaker> myCBRegistryEventConsumer() {
return new RegistryEventConsumer<CircuitBreaker>() {
@Override
public void onEntryAddedEvent(EntryAddedEvent<CircuitBreaker> entryAddedEvent) {
entryAddedEvent.getAddedEntry().getEventPublisher()
.onEvent(event -> System.out.println(event.toString()));
}
@Override
public void onEntryRemovedEvent(EntryRemovedEvent<CircuitBreaker> entryRemoveEvent) {
}
@Override
public void onEntryReplacedEvent(EntryReplacedEvent<CircuitBreaker> entryReplacedEvent) {
}
};
}
}
Porém, acho mais interessantes acessar os eventos do Retry disponíveis pelo Actuator:
http://localhost:8080/actuator/retryevents
{
retryEvents: [
{
retryName: "pessoaRetry",
type: "RETRY",
creationTime: "2022-01-02T13:00:56.158253-03:00[America/Sao_Paulo]",
errorMessage: "org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8081/pessoa/buscar": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused)",
numberOfAttempts: 1
},
...
]
}
Porém, acho mais interessantes acessar os eventos do Circuit Breaker disponíveis pelo Actuator:
http://localhost:8080/actuator/circuitbreakerevents
{
circuitBreakerEvents: [
{
circuitBreakerName: "pessoaCircuitBreaker",
type: "ERROR",
creationTime: "2022-01-02T13:01:00.184692-03:00[America/Sao_Paulo]",
errorMessage: "org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8081/pessoa/buscar": Connection
refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused)",
durationInMs: 4135,
stateTransition: null
},
...
]
}
Referências
1. PRABHU, Amrut. Circuit Breaker And Retry with Spring Cloud Resiliance4j.. DZone. Disponível em: [https://dzone.com/articles/circuit-breaker-and-retry-with-spring-cloud-resili]. Acesso em 01 jan. 2022.
2. _____. Circuit Breaker And Retry with Spring Cloud Resiliance4j.. RefactorFirst. Disponível em: [https://refactorfirst.com/spring-cloud-resiliance4j-circuitbreaker-and-retry.html]. Acesso em 01 jan. 2022.
ArchUnit – Introdução
Testes são as garantias mínimas de qualidade que um desenvolvedor entrega junto com seu código. Ele pode fazer testes unitários em determinadas funcionalidades, testes de cobertura, etc. Mas como garantir que as peças que compõe o sistema estão nos lugares certos? Como garantir que as diretrizes arquiteturais estão sendo seguidas pelos desenvolvedores sem precisar ficar onerando outros colegas para fazer code review? Se permitirmos exceções, imagine o caos que essas “janelas quebradas” causaram no código quando mais e mais desenvolvedores ingressarem no projeto.
ArchUnit é uma biblioteca simples e extensível para verificar se o código Java está aderente às diretrizes arquiteturais definidas, como as dependências entre os pacotes e as classes, camadas, etc.
Vamos ver o ArchUnit funcionando. Adicione a dependência abaixo:
<dependency> <groupId>com.tngtech.archunit</groupId> <artifactId>archunit-junit5</artifactId> <version>0.18.0</version> <scope>test</scope> </dependency>
Crie uma classe de teste e informe para o ArchUnit onde estão as classes que ele deve scanear:
@AnalyzeClasses(
packages = "br.arch",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
public class CodingRulesTest {
(...)
}
Podemos definir regras gerais para codificação:
- Nenhuma classes deve lançar exceções genéricas
- Nenhuma classe deve utilizar java.util.logging
- Nenhuma classe deve utilizar Jodatime
- Nenhuma classe deve receber dependências por injeção
@ArchTest
static final ArchRule coding_rules =
CompositeArchRule.of(GeneralCodingRules.NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS)
.and(GeneralCodingRules.NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING)
.and(GeneralCodingRules.NO_CLASSES_SHOULD_USE_JODATIME)
.and(GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION)
.because("Foram encontradas violacoes em regras gerais de codificacao!");
Podemos definir regras específicas para as classes utilitárias:
- Todos os métodos devem ser públicos
- Todos os métodos devem ser estáticos
@ArchTest
static final ArchRule util_rules =
methods().that().areDeclaredInClassesThat()
.resideInAnyPackage("..util..")
.should().bePublic()
.andShould().beStatic()
.because("Todas os metodos utilitarios devem ser publicos e estaticos!");
Podemos definir regras para reforçar a composição das camadas em nossa arquitetura:
- Controllers não podem ser acessados por nenhuma outra classe
- Services só podem ser acessados por controllers
- Repositories só podem ser acessados por services
@ArchTest
public static final ArchRule layer_rules =
layeredArchitecture()
.layer("Controllers").definedBy("..controller..")
.layer("Services").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
.whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Services")
.because("Foram encontradas violacoes nas regras de acesso as camadas!");
Podemos definir que nenhum serviço pode acessar controllers:
@ArchTest
public static void service_outbound_reject_rules(JavaClasses classes) {
noClasses().that().resideInAPackage("..service..").should().accessClassesThat()
.resideInAPackage("..controller..").check(classes);
}
Podemos definir que serviços só podem ser acessados por controllers e outros serviços:
@ArchTest
public static void service_inbound_accept_rules(JavaClasses classes) {
classes().that().resideInAPackage("..service..").should().onlyBeAccessed()
.byAnyPackage("..controller..", "..service..").check(classes);
}
Podemos definir que repositórios não podem acessar serviços:
@ArchTest
public static void repository_outbound_reject_rules(JavaClasses classes) {
noClasses().that().resideInAPackage("..repository..").should().accessClassesThat()
.resideInAPackage("..service..").check(classes);
}
Se você precisar customizar uma condição, é simples. Vamos definir uma condição para verificar se o Controller anotado com @RestController também possui uma anotação @RequestMapping e essa mapeia o contexto “/pedido”:
private static final ArchCondition<JavaClass> controller_mapping_rules =
new ArchCondition<JavaClass>("Todo Controller deve ser anotado com @RequestMapping e mapear /pedido") {
@Override
public void check(JavaClass javaClass, ConditionEvents conditionEvents) {
String[] values = javaClass.getAnnotationOfType(RequestMapping.class).path();
boolean result = Arrays.stream(values).anyMatch(v -> v.startsWith("/pedido"));
if (!result) {
conditionEvents.add(SimpleConditionEvent.violated(javaClass,
javaClass.getSimpleName() + " Nao contem mapeamento para /pedido "));
}
}
};
@ArchTest
public static void controller_general_rules(JavaClasses classes) {
classes().that().resideInAnyPackage("..controller..")
.and()
.areAnnotatedWith(RestController.class)
.should(controller_mapping_rules)
.check(classes);
}
Uma limitação conhecida do ArchUnit é seu uso combinado com anotações marcadas com Retention.SOURCE, como as anotações do Lombok, por exemplo.
Referências
1. TARTE, Pravin. Introducing Coding Constraints Using ArchUnit.. DZone. Disponível em: [https://dzone.com/articles/introducing-coding-constraints-using-archunit]. Acesso em 01 jan. 2022.
2. TURPIN, Cindy. Unit Testing Your Architecture With ArchUnit.. DZone. Disponível em: [https://dzone.com/articles/unit-testing-your-architecture-with-archunit]. Acesso em 01 jan. 2022.
SpringBoot: Estratégias de Teste de Bancos da Dados em Memória
Vamos ver algumas estratégias que podem ser úteis para seus testes de integração. Vamos utilizar um banco de dados MySQL que configuramos em outro artigo.
Primeiro, configure um projeto SpringBoot:
Adicione as propriedades abaixo ao seu application.yml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/PESSOA
username: rmartins
password: 123456
jpa:
packages-to-scan: 'br.com'
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
properties:
hibernate:
hbm2ddl:
auto: none
show_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
format_sq: true
Crie o repositório:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PessoaRepository extends JpaRepository<Pessoa, Long> {
}
Defina a entidade Pessoa:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@Table(name = "TBL_PESSOA")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Pessoa {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "PK_PESSOA")
private Long id;
@Column(name = "NM_PESSOA")
private String nome;
}
Por último, crie o controller:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
@RestController
@RequestMapping("/pessoa")
public class PessoaController {
@Autowired
private PessoaRepository repository;
@GetMapping("/{id}")
public ResponseEntity<?> findPessoa(@PathVariable Long id) {
Optional<Pessoa> pessoa = repository.findById(id);
if (pessoa.isPresent()) {
return ResponseEntity.of(pessoa);
}
return ResponseEntity.notFound().build();
}
}
Teste com H2
Para utilizar o banco de dados H2, adicione a dependência abaixo ao build.gradle:
testImplementation 'com.h2database:h2'
Crie o arquivo application-h2test.yml
spring:
jpa:
packages-to-scan: 'br.com'
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
properties:
hibernate:
hbm2ddl:
auto: create-drop
show_sql: true
format_sq: true
Crie a classe de testes:
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("h2test")
class DebtestApplicationH2Tests {
@Autowired
private MockMvc mockMvc;
@Autowired
private PessoaRepository repository;
@BeforeEach
private void setup(){
repository.save(new Pessoa(1l, "ALEXANDRE"));
}
@Test
void givenPessoa_whenGetPessoa_thenStatus200() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/pessoa/1"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.nome").value("ALEXANDRE"));
}
}
Teste com Zonky
O Zonky foi o que me motivou a escrever esse artigo: o teste fica independente da conexão com o banco de dados original. A ideia é utilizar imagens Docker para emular os containers dos bancos de dados. Vamos utilizar uma imagem de MySQL. Adicione a dependência abaixo ao build.gradle:
testImplementation 'io.zonky.test:embedded-database-spring-test:2.0.1'
Crie o arquivo application-zonkytest.yml:
spring:
jpa:
packages-to-scan: 'br.com'
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
properties:
hibernate:
hbm2ddl:
auto: create-drop
show_sql: true
format_sq: true
zonky:
test:
database:
postgres:
docker:
image: mysql:5.7
Crie a classe de testes:
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureEmbeddedDatabase
@ActiveProfiles("zonkytest")
class DebtestApplicationZonkyTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private PessoaRepository repository;
@BeforeEach
private void setup(){
repository.save(new Pessoa(1l, "ALEXANDRE"));
}
@Test
void givenPessoa_whenGetPessoa_thenStatus200() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/pessoa/1"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.nome").value("ALEXANDRE"));
}
}
Micronaut: Introdução
O Micronaut é um framework baseado na JVM cujas APIs têm forte inspiração em Spring e Grails e é utilizado para a criação de aplicações utilizando Java, Kotlin ou Groovy. Ele é bastante útil para criação de microsserviços que precisam inicializar rapidamente porque ele usa o conceito de compilação AOT (Ahead Of Time) para determinar o que a aplicação precisa. Essa estratégia de compilação permite que a aplicação utilize pouca memória, seja iniciada em pouco tempo e não dependa de reflection. Principais features:
- Injeção de dependência por IOC e Anotações sem utilizar reflection
- Roteamento HTTP
- HTTP Client
- Auto-Configuration
- Service Discovery
- Facilidade para a implementação de testes
- Facilidade para desenvolvimento de aplicações serverless
- Aspect-Oriented Programming (AOP)
Com o Micronaut, podemos criar aplicações em linha de comando, servidores HTTP e aplicações do tipo event-driven. O Micronaut disponibiliza algumas anotações bem parecidas com aquelas do Spring:
@controller
@repository
@singleton
@prototype
@requestscope
Embora o Micronaut seja inspirado no Spring, não há um mapeamento um-para-um de anotações de um para outro. Não há, por exemplo, as anotações @Service e @Component, mas há uma anotação chamada @Infrastructure para definir um bean que é crítico para a aplicação em tempo de execução que não pode ser sobrescrito, e a anotação @Threadlocal para definir um escopo de thread para o bean. O mecanismo de injeção de dependências é similar, mas ao invés de utilizar @Autorired, você deve utilizar @Inject.
Para esse artigo, utilizaremos o banco de dados MySQL criado a partir de uma imagem do Docker que configuramos nesse artigo. Acesse o Micronaut Launch e configure um projeto conforme a imagem abaixo. Criar um projeto dessa forma é equivalente a criar um projeto com o Spring Initlr do SpringBoot:
Configure o application.properties:
micronaut:
application:
name: micro
datasources:
default:
url: jdbc:mysql://172.17.0.2:3306/PESSOA
username: rmartins
password: 123456
jpa:
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
physical_naming_strategy: "org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl"
show_sql: true
ddl-auto: none
Defina uma entidade para representar a Pessoa. Você pode utilizar as anotações da JPA às quais você já está acostumado. @Data é do Lombok:
@Entity
@Table(name = "TBL_PESSOA")
@Data
public class Pessoa {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "PK_PESSOA")
private Long id;
@Column(name = "NM_PESSOA")
private String nome;
}
Vamos configurar nosso repositório:
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.jpa.repository.JpaRepository;
@Repository
public interface PessoaRepository extends JpaRepository<Pessoa, Long> {
}
Vamos definir nosso controller. Atenção para o uso das anotações do Micronaut aqui, como @Controller e @Get, por exemplo:
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import jakarta.inject.Inject;
import java.util.Optional;
@Controller("/pessoa")
public class PessoaController {
@Inject
private PessoaRepository repository;
@Get("/{id}")
public HttpResponse<?> findPessoa(@PathVariable("id") Long id) {
Optional<Pessoa> pessoa = repository.findById(id);
if (pessoa.isPresent()) {
return HttpResponse.created(pessoa.get());
}
return HttpResponse.notFound();
}
}
Por fim, vamos testar nosso endpoint:
user:~$ curl -s -X GET http://localhost:8080/pessoa/1 | json_pp
{
"id" : 1,
"nome" : "ALEXANDRE"
}
O Micronaut também permite criar aplicações reativas e oferece uma API para criarmos um circuit breaker.
Comparação com o Quarkus
Achei uma comparação entre uma aplicação SpringBoot, Micronaut e Quarkus:
Referências
1. PRABHU, Amrut. A SpringBoot Developer’s Guide To Micronaut.. RefactorFirst. Disponível em: [https://refactorfirst.com/springboot-developers-guide-to-micronaut.html]. Acesso em 19 set. 2021.
Quarkus: Introdução
Ao longo dos anos, o Java e seu ecossistema evoluíram para melhorar o tempo de execução. Se você precisar de um serviço, use uma anotação para injetá-lo onde for necessário e a dependência será resolvida em tempo de execução. Essa estratégia requer verificação do classpath, que é uma operação custosa em termos de processamento e memória, o que aumenta o tempo de resposta.
O Quarkus trouxe uma outra abordagem para esse problema: operações custosas (proxyng, por exemplo) passam para a etapa de compilação, o que faz com que a inicialização da aplicação seja mais rápida e o consumo de recursos seja menor, o que torna essa tecnologia interessante para uso em nuvem. Quarkus é um conjunto de tecnologias open source adaptadas para GraalVM e HotSpot para escrever aplicações Java que iniciam mais rápido e utilizam melhor os recursos disponíveis. O GraalVM é uma máquina virtual universal e poliglota que torna possível a compilação ahead-of-time (AOT) convertendo bytecodes em código de máquina nativo. Durante esse processo, o GraalVM remove o “dead code” e faz uma série de otimizações agressivas.
Um dos problemas de gerar aplicações nativas está relacionado ao uso de reflections e JNDI: por causa do processo de otimizações agressivas do AOT, é provável que a dependência necessária simplesmente tenha sido removida, o que causará falha de inicialização da aplicação.
Mesmo assim, como já estava acostumado com o tempo de inicialização de aplicações SpringBoot, notei que uma aplicação Quarkus equivalente iniciava muito mais rápido: por alto, umas três vezes mais rápido! E isso porque testei a versão JVM e não a versão nativa com GraalVM. Outra coisa interessante é que você não precisa de uma classe Main: só precisamos criar as classes que realmente agregam valor, como controllers, repositories e etc.
Nesse momento você poderia estar pensando que o Quarkus trouxe um monte de tecnologias novas para dar suporte a essa estratégia de otimização. Que nada! Ele usa JAX-RS, CDI, JPA, Reactive e todas as bibliotecas e frameworks que você está acostumado a usar, como Kafka, Apache Camel, Hibernate, etc. E você também pode utilizar Maven ou Gradle.
Vamos criar uma aplicação Quarkus bem simples. Configure o esqueleto de uma aplicação Gradle como o mostrado abaixo lá no site do Quarkus. O processo é equivalente ao Init do SpringBoot:
Crie a API abaixo para retornar uma lista de nomes de pessoas:
@Path("/pessoa")
public class PessoaController {
@GET
@Path("/listar")
@Produces(MediaType.APPLICATION_JSON)
public List<String> listar() {
return Arrays.asList("Marcos", "Alberto");
}
}
Execute o target quarkusDev. Acesse o endpoint:
localhost:8080/pessoa/listar
Quando Utilizar o Quarkus?
Achei um guideline bem simples para orientar na escolha do Quarkus:
Se a aplicação ficará muito tempo disponível, talvez seja melhor utilizar a versão JVM (não nativa) devido à melhor performance do Garbage Collector quando comparado ao Garbage Collector do GraalVM.
Referências
1. CORTEZ, Roberto. Introdução ao Quarkus.. InfoQ. Disponível em: [https://www.infoq.com/br/articles/getting-started-with-quarkus/]. Acesso em 19 set. 2021.
2. SCOSSI, Gabriel. Quando usar o Quarkus.. HypeFlame. Disponível em: [https://hypeflame.blog/2020/12/14/quando-usar-o-quarkus/]. Acesso em 19 set. 2021.







