1. Groovy — Closures Avançadas
Uma closure é um bloco de código de primeira classe: pode ser armazenada em variáveis, passada como argumento e retornada de métodos. Ao contrário de lambdas Java, closures capturam o contexto (this, variáveis locais) e têm um delegate configurável — o que habilita DSLs poderosas.
// Definição e chamada básica
def saudar = { String nome -> "Olá, $nome!" }
println saudar("Rafael") // Olá, Rafael!
// Parâmetro implícito 'it' para closures de um argumento
def dobrar = { it * 2 }
println dobrar(5) // 10
// Closure com múltiplos parâmetros
def calcularFrete = { BigDecimal peso, String regiao ->
def taxa = regiao == "SP" ? 2.5 : 4.0
(peso * taxa).setScale(2, BigDecimal.ROUND_HALF_UP)
}
println calcularFrete(3.5, "SP") // 8.75
// curry — fixa parâmetros parcialmente, retorna nova closure
def calcularFreteRegiao = calcularFrete.curry(_, "RJ") // fixa regiao = "RJ"
println calcularFreteRegiao(3.5) // 14.00
println calcularFreteRegiao(10.0) // 40.00
// Currying posicional — rcurry fixa pelo fim
def somar = { a, b, c -> a + b + c }
def somarMais10 = somar.rcurry(10) // fixa c = 10
println somarMais10(1, 2) // 13memoize — cache automático de resultado:
// Sem memoize — recalcula toda vez (custoso para Fibonacci)
def fib
fib = { long n -> n <= 1 ? n : fib(n - 1) + fib(n - 2) }
// Com memoize — resultado armazenado na primeira chamada
fib = { long n -> n <= 1 ? n : fib(n - 1) + fib(n - 2) }.memoize()
println fib(40) // rápido mesmo para n grande
// memoizeAtMost — limita tamanho do cache (evita vazamento de memória)
def calcularDesconto = { String clienteId, BigDecimal valor ->
// simulação de chamada custosa
clienteService.buscarPercentualDesconto(clienteId) * valor / 100
}.memoizeAtMost(500)delegate — base para DSLs:
// O delegate de uma closure determina quem responde às chamadas de método
class PedidoBuilder {
String clienteId
List<Map> itens = []
BigDecimal desconto = BigDecimal.ZERO
void item(String sku, int qty, BigDecimal preco) {
itens << [sku: sku, quantidade: qty, preco: preco]
}
void desconto(BigDecimal percentual) {
this.desconto = percentual
}
}
def pedido(Closure config) {
def builder = new PedidoBuilder()
config.delegate = builder // redireciona chamadas para o builder
config.resolveStrategy = Closure.DELEGATE_FIRST
config.call()
return builder
}
def p = pedido {
clienteId = "cliente-42"
item("SKU-001", 2, 49.90)
item("SKU-002", 1, 99.90)
desconto(10.0)
}
println p.itens.size() // 22. Groovy — GDK: Métodos em Coleções
O Groovy enriches todas as coleções Java com métodos funcionais via GDK (Groovy Development Kit). Esses métodos recebem closures e eliminam a necessidade de loops explícitos.
def pedidos = [
[id: "p1", cliente: "Ana", total: 150.00, status: "pago", itens: 3],
[id: "p2", cliente: "Bruno", total: 89.90, status: "pendente", itens: 1],
[id: "p3", cliente: "Ana", total: 299.00, status: "pago", itens: 5],
[id: "p4", cliente: "Carlos", total: 49.90, status: "cancelado", itens: 2],
[id: "p5", cliente: "Bruno", total: 120.00, status: "pago", itens: 4],
]
// each — itera (sem retorno útil)
pedidos.each { println "$it.id: R$ $it.total" }
// collect — transforma cada elemento (equivale ao map)
def totais = pedidos.collect { it.total } // [150.00, 89.90, 299.00, 49.90, 120.00]
def resumos = pedidos.collect { "$it.id ($it.status)" }
// findAll — filtra (mantém os que retornam true)
def pagos = pedidos.findAll { it.status == "pago" } // [p1, p3, p5]
def grandes = pedidos.findAll { it.total > 100 } // [p1, p3, p5]
// find — retorna o PRIMEIRO que satisfaz a condição (ou null)
def primeiroAnaPago = pedidos.find { it.cliente == "Ana" && it.status == "pago" }
// inject — fold/reduce (acumulador)
BigDecimal totalGeral = pedidos.inject(BigDecimal.ZERO) { acc, p -> acc + p.total }
// 708.80
// sum com closure — atalho para inject com soma
def totalPagos = pedidos.findAll { it.status == "pago" }.sum { it.total }
// 569.00
// min e max
def maisBarato = pedidos.min { it.total } // [id: "p4", ...]
def maisValioso = pedidos.max { it.total } // [id: "p3", ...]
// groupBy — agrupa em Map[chave → lista]
Map<String, List> porCliente = pedidos.groupBy { it.cliente }
// [Ana: [p1, p3], Bruno: [p2, p5], Carlos: [p4]]
Map<String, List> porStatus = pedidos.groupBy { it.status }
// sort e sortBy
def ordenados = pedidos.sort { a, b -> b.total <=> a.total } // decrescente por total
def porNome = pedidos.sort { it.cliente } // crescente por cliente
// countBy — conta por critério
def contagemPorStatus = pedidos.countBy { it.status }
// [pago:3, pendente:1, cancelado:1]
// any e every
boolean temCancelado = pedidos.any { it.status == "cancelado" } // true
boolean todosPagos = pedidos.every { it.status == "pago" } // false
// flatten e collectMany (flatMap)
def todosSkus = pedidos.collectMany { p ->
(1..p.itens).collect { "SKU-${p.id}-${it}" }
}
// unique
def clientesUnicos = pedidos.collect { it.cliente }.unique() // [Ana, Bruno, Carlos]
// intersect, disjoint, minus
def listaA = [1, 2, 3, 4, 5]
def listaB = [3, 4, 5, 6, 7]
println listaA.intersect(listaB) // [3, 4, 5]
println listaA.minus(listaB) // [1, 2]3. Groovy — Meta-Programação
Meta-programação permite modificar o comportamento de classes em tempo de execução ou de compilação. É a base de frameworks como Grails, Spock e de muitos DSLs.
// methodMissing — intercepta chamadas a métodos inexistentes
class PedidoQuery {
Map<String, Object> criterios = [:]
def methodMissing(String nome, args) {
if (nome.startsWith("porStatus")) {
criterios.status = args[0]
return this // permite encadeamento
}
if (nome.startsWith("porCliente")) {
criterios.clienteId = args[0]
return this
}
throw new MissingMethodException(nome, PedidoQuery, args)
}
List executar() {
// filtra usando os criterios coletados
pedidoRepo.buscarPor(criterios)
}
}
def query = new PedidoQuery()
def resultado = query.porStatus("pago").porCliente("Ana").executar()// propertyMissing — intercepta acesso a propriedades inexistentes
class FlexivelConfig {
Map<String, String> valores = [:]
def propertyMissing(String nome) {
valores[nome] ?: "não configurado"
}
def propertyMissing(String nome, valor) {
valores[nome] = valor.toString()
}
}
def cfg = new FlexivelConfig()
cfg.host = "localhost"
cfg.porta = 5432
println cfg.host // localhost
println cfg.user // não configurado// ExpandoMetaClass — adiciona métodos a classes existentes em runtime
// Útil para enriquecer classes Java ou de terceiros
// Adicionar método a String
String.metaClass.paraSlug = { ->
delegate.toLowerCase().replaceAll(/\s+/, "-").replaceAll(/[^a-z0-9-]/, "")
}
println "Meu Pedido Especial".paraSlug() // meu-pedido-especial
// Adicionar método a uma classe específica
BigDecimal.metaClass.formatarBRL = { ->
"R\$ ${delegate.setScale(2, BigDecimal.ROUND_HALF_UP)}"
}
println 149.9.formatarBRL() // R$ 149.90
// Adicionar método estático
Integer.metaClass.static.diasUteisDoMes = { ->
// simplificado
20
}
println Integer.diasUteisDoMes() // 20
// ExpandoMetaClass em instância específica (não afeta outras instâncias)
def pedido = new Pedido(id: "p1", total: 100.0)
pedido.metaClass.calcularComDesconto = { BigDecimal perc ->
pedido.total * (1 - perc / 100)
}
println pedido.calcularComDesconto(10) // 90.04. Groovy — Anotações de Transformação AST
As anotações @groovy.transform.* injetam código em tempo de compilação via AST (Abstract Syntax Tree) transformations. Eliminam boilerplate mantendo código legível.
import groovy.transform.*
// @Canonical — gera toString, equals, hashCode e construtor com argumentos posicionais
@Canonical
class Produto {
String sku
String nome
BigDecimal preco
String categoria
}
def p1 = new Produto("SKU-001", "Notebook", 3499.99, "Eletronicos")
def p2 = new Produto(sku: "SKU-001", nome: "Notebook", preco: 3499.99, categoria: "Eletronicos")
println p1 == p2 // true (equals por valor)
println p1 // Produto(SKU-001, Notebook, 3499.99, Eletronicos)// @Builder — gera builder fluente
@Builder
class Pedido {
String clienteId
String enderecoEntrega
List<ItemPedido> itens
BigDecimal desconto
LocalDate dataEntrega
}
def pedido = Pedido.builder()
.clienteId("cliente-42")
.enderecoEntrega("Rua das Flores, 123")
.itens([new ItemPedido("SKU-1", 2)])
.desconto(10.0)
.build()// @Delegate — delega métodos para um campo interno (composição sem herança)
class LoggingPedidoService {
@Delegate PedidoService service = new PedidoServiceImpl()
// Todos os métodos de PedidoService ficam disponíveis aqui
// Podemos sobrescrever apenas os que queremos interceptar
PedidoResponse criar(CriarPedidoRequest req) {
log.info("Criando pedido para cliente {}", req.clienteId)
def resp = service.criar(req)
log.info("Pedido {} criado com sucesso", resp.id)
resp
}
}// @TypeChecked — ativa checagem de tipos em tempo de compilação
// Útil para pegar erros antes de rodar
@TypeChecked
class CalculadoraDesconto {
BigDecimal calcular(BigDecimal valor, int percentual) {
// O compilador verifica tipos aqui
valor * (100 - percentual) / 100
}
}
// @CompileStatic — como @TypeChecked mas também gera bytecode eficiente (como Java)
@CompileStatic
class ProcessadorPedido {
Pedido processar(CriarPedidoRequest req) {
def pedido = new Pedido()
pedido.clienteId = req.clienteId
// Erros de tipo são pegos em compilação
pedido
}
}
// @Immutable — value object imutável completo
@Immutable
class Dinheiro {
BigDecimal valor
String moeda
Dinheiro somar(Dinheiro outro) {
assert moeda == outro.moeda : "Moedas diferentes: $moeda x $outro.moeda"
new Dinheiro(valor + outro.valor, moeda)
}
}
def preco = new Dinheiro(99.90, "BRL")
def frete = new Dinheiro(15.00, "BRL")
def total = preco.somar(frete)
println total // Dinheiro(114.90, BRL)
// preco.valor = 50.00 // MissingPropertyException — imutável!5. Groovy — DSLs com Closures e Builders
O delegate de closure é o mecanismo central para criar DSLs internas em Groovy. Frameworks como Gradle, Spock e Ratpack usam extensivamente esse padrão.
// DSL para definir regras de frete
class RegrasFreteBuilder {
List<Map> regras = []
BigDecimal freteGratisMinimoDefault = 150.0
void para(String regiao, Closure config) {
def regraBuilder = new RegraBuilder(regiao: regiao)
config.delegate = regraBuilder
config.resolveStrategy = Closure.DELEGATE_FIRST
config.call()
regras << regraBuilder.build()
}
void freteGratisAcimaDe(BigDecimal valor) {
freteGratisMinimoDefault = valor
}
}
class RegraBuilder {
String regiao
BigDecimal taxa
int prazoEmDias
BigDecimal maximoPesoKg
void taxa(BigDecimal t) { this.taxa = t }
void prazo(int dias) { this.prazoEmDias = dias }
void maxPeso(BigDecimal kg) { this.maximoPesoKg = kg }
Map build() { [regiao: regiao, taxa: taxa, prazo: prazoEmDias, maxPeso: maximoPesoKg] }
}
def configurarFrete(Closure config) {
def builder = new RegrasFreteBuilder()
config.delegate = builder
config.resolveStrategy = Closure.DELEGATE_FIRST
config.call()
return builder
}
// Uso da DSL — lê como configuração, não como código
def regras = configurarFrete {
freteGratisAcimaDe 200.0
para("SP") {
taxa 8.50
prazo 2
maxPeso 30
}
para("RJ") {
taxa 12.00
prazo 3
maxPeso 25
}
para("OUTROS") {
taxa 18.00
prazo 7
maxPeso 20
}
}
println regras.regras.size() // 3
println regras.freteGratisMinimoDefault // 200.0DSL para testes (estilo Spock/BDD):
// Mini-framework BDD com closures
class Cenario {
String descricao
Closure dado, quando, entao
void dado(Closure c) { this.dado = c }
void quando(Closure c) { this.quando = c }
void entao(Closure c) { this.entao = c }
void executar() {
println "Cenário: $descricao"
dado?.call()
def resultado = quando?.call()
entao?.call(resultado)
}
}
def cenario(String desc, Closure config) {
def c = new Cenario(descricao: desc)
config.delegate = c
config.resolveStrategy = Closure.DELEGATE_FIRST
config.call()
c.executar()
}
cenario("Pedido com desconto deve ter valor correto") {
def pedido
dado { pedido = new Pedido(total: 100.0) }
quando { pedido.aplicarDesconto(10) }
entao { assert pedido.total == 90.0 }
}6. Groovy — SQL com groovy.sql.Sql
groovy.sql.Sql simplifica operações JDBC eliminando o boilerplate de try/catch/close. Funciona com qualquer driver JDBC.
import groovy.sql.Sql
// Conectar via DataSource (preferível em aplicações reais)
def sql = Sql.newInstance(dataSource)
// Também é possível conectar diretamente
def sql2 = Sql.newInstance(
"jdbc:postgresql://localhost:5432/pedidos",
"dev", "dev",
"org.postgresql.Driver"
)
// SELECT — eachRow itera sem carregar tudo na memória
sql.eachRow("SELECT id, cliente_id, total FROM pedidos WHERE status = ?", ["pago"]) { row ->
println "Pedido ${row.id}: R$ ${row.total}"
}
// SELECT — rows carrega todos os resultados como List<GroovyRowResult>
List resultados = sql.rows("SELECT * FROM pedidos WHERE criado_em > ?", [dataInicio])
// SELECT um único registro
def pedido = sql.firstRow("SELECT * FROM pedidos WHERE id = ?", [pedidoId])
println pedido?.cliente_id // null-safe
// INSERT
sql.execute("""
INSERT INTO pedidos (id, cliente_id, status, total, criado_em)
VALUES (?, ?, ?, ?, NOW())
""", [UUID.randomUUID().toString(), "cliente-1", "aberto", 99.90])
// Batch INSERT — performance para múltiplos registros
sql.withBatch(50) { stmt -> // flush a cada 50 registros
pedidos.each { p ->
stmt.addBatch([p.id, p.clienteId, p.status, p.total] as Object[])
}
}
// UPDATE com execUpdate — retorna número de linhas afetadas
int afetadas = sql.executeUpdate(
"UPDATE pedidos SET status = ? WHERE id = ? AND status = ?",
["pago", pedidoId, "pendente"]
)
println "Pedidos atualizados: $afetadas"
// Transação manual
sql.withTransaction {
sql.execute("INSERT INTO pedidos ...", params1)
sql.execute("UPDATE estoque ...", params2)
// rollback automático se qualquer Exception for lançada
}
// Named parameters — mais legível para muitos parâmetros
sql.eachRow("""
SELECT * FROM pedidos
WHERE cliente_id = :clienteId
AND status = :status
AND total >= :minTotal
""", [clienteId: "cliente-42", status: "pago", minTotal: 100.0]) { row ->
println row.id
}
// Fechamento automático com use
Sql.withInstance(dataSource) { sqlInstance ->
sqlInstance.eachRow("SELECT count(*) as total FROM pedidos") {
println "Total de pedidos: ${it.total}"
}
}
// connection fechada automaticamente7. Vert.x — Verticles e Deployment Options
Um Verticle é a unidade básica de Vert.x. Roda em um event loop (thread dedicada) e deve ser não-bloqueante. Para código bloqueante (banco legado, arquivos), use Worker Verticle ou execute em vertx.executeBlocking.
// Verticle padrão — roda no event loop; NUNCA bloquear aqui
public class PedidoVerticle extends AbstractVerticle {
private PedidoRepository repository;
private Router router;
@Override
public Future<?> start() {
repository = new PedidoRepository(vertx);
router = Router.router(vertx);
configurarRotas();
return vertx.createHttpServer()
.requestHandler(router)
.listen(8080)
.onSuccess(server ->
System.out.println("Pedidos API na porta " + server.actualPort()));
}
private void configurarRotas() {
router.get("/api/pedidos/:id").handler(this::buscarPedido);
router.post("/api/pedidos").handler(BodyHandler.create()).handler(this::criarPedido);
}
private void buscarPedido(RoutingContext ctx) {
String id = ctx.pathParam("id");
repository.findById(id)
.onSuccess(pedido -> ctx.json(pedido))
.onFailure(err -> ctx.fail(404, err));
}
private void criarPedido(RoutingContext ctx) {
JsonObject body = ctx.body().asJsonObject();
repository.save(body)
.onSuccess(id -> ctx.response().setStatusCode(201).end(id))
.onFailure(ctx::fail);
}
}// Worker Verticle — para código bloqueante (I/O síncrono, CPU intensivo)
public class RelatorioWorkerVerticle extends AbstractVerticle {
@Override
public Future<?> start() {
// Worker pode bloquear — roda em thread pool separada
vertx.eventBus().<JsonObject>consumer("relatorio.gerar", msg -> {
try {
// Operação bloqueante — OK aqui porque é Worker
byte[] pdf = gerarPdfBloqueante(msg.body());
msg.reply(Buffer.buffer(pdf));
} catch (Exception e) {
msg.fail(500, e.getMessage());
}
});
return Future.succeededFuture();
}
}
// Deployment com opções
public class Main {
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
// Múltiplas instâncias do mesmo Verticle (uma por CPU)
DeploymentOptions webOptions = new DeploymentOptions()
.setInstances(Runtime.getRuntime().availableProcessors());
// Deploying como worker
DeploymentOptions workerOptions = new DeploymentOptions()
.setThreadingModel(ThreadingModel.WORKER)
.setInstances(4);
vertx.deployVerticle(PedidoVerticle.class.getName(), webOptions)
.compose(id -> vertx.deployVerticle(RelatorioWorkerVerticle.class.getName(), workerOptions))
.onSuccess(id -> System.out.println("Todos os verticles deployados"))
.onFailure(err -> {
System.err.println("Falha no deploy: " + err.getMessage());
vertx.close();
});
}
}Executar código bloqueante a partir do event loop:
// Quando você não pode usar Worker Verticle mas precisa de operação bloqueante
vertx.executeBlocking(() -> {
// Este código roda em thread pool — pode bloquear
return arquivoService.processarCsvBloqueante(caminho);
}).onSuccess(resultado -> {
// De volta ao event loop — não-bloqueante
ctx.json(resultado);
}).onFailure(err -> ctx.fail(500, err));8. Vert.x — HTTP Server e Router
O Router do Vert.x Web organiza handlers por rota e método HTTP. Suporta path params, body parsing, static files, CORS, autenticação e muito mais.
// Configuração completa de router
Router router = Router.router(vertx);
// Body handler — necessário antes de ler o body
router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 1MB
// CORS
router.route().handler(CorsHandler.create()
.addOrigin("https://meusite.com.br")
.allowedMethod(HttpMethod.GET)
.allowedMethod(HttpMethod.POST)
.allowedHeader("Content-Type")
.allowedHeader("Authorization"));
// Logging de requests
router.route().handler(LoggerHandler.create());
// Static files
router.route("/static/*").handler(StaticHandler.create("webroot"));
// Rotas por método HTTP
router.get("/api/pedidos").handler(pedidoHandler::listar);
router.get("/api/pedidos/:id").handler(pedidoHandler::buscarPorId);
router.post("/api/pedidos").handler(pedidoHandler::criar);
router.put("/api/pedidos/:id").handler(pedidoHandler::atualizar);
router.patch("/api/pedidos/:id/status").handler(pedidoHandler::atualizarStatus);
router.delete("/api/pedidos/:id").handler(pedidoHandler::deletar);
// Sub-router por prefixo
Router adminRouter = Router.router(vertx);
adminRouter.get("/usuarios").handler(adminHandler::listarUsuarios);
router.mountSubRouter("/api/admin", adminRouter);
// Handler de erro global
router.errorHandler(404, ctx ->
ctx.response()
.setStatusCode(404)
.putHeader("Content-Type", "application/json")
.end(new JsonObject().put("error", "Recurso não encontrado").encode()));
router.errorHandler(500, ctx -> {
ctx.failure().printStackTrace();
ctx.response().setStatusCode(500)
.end(new JsonObject().put("error", "Erro interno").encode());
});// Handler de pedidos — exemplo completo
public class PedidoHandler {
private final PedidoService service;
public PedidoHandler(PedidoService service) {
this.service = service;
}
public void buscarPorId(RoutingContext ctx) {
String id = ctx.pathParam("id");
String clienteId = ctx.queryParam("clienteId").stream().findFirst().orElse(null);
service.buscar(id)
.onSuccess(pedido -> {
ctx.response()
.setStatusCode(200)
.putHeader("Content-Type", "application/json")
.end(JsonObject.mapFrom(pedido).encode());
})
.onFailure(err -> {
if (err instanceof PedidoNaoEncontradoException) {
ctx.fail(404, err);
} else {
ctx.fail(500, err);
}
});
}
public void criar(RoutingContext ctx) {
JsonObject body = ctx.body().asJsonObject();
// Validação básica
if (body == null || !body.containsKey("clienteId")) {
ctx.fail(400, new IllegalArgumentException("clienteId é obrigatório"));
return;
}
service.criar(body)
.onSuccess(pedido -> {
ctx.response()
.setStatusCode(201)
.putHeader("Location", "/api/pedidos/" + pedido.getString("id"))
.putHeader("Content-Type", "application/json")
.end(pedido.encode());
})
.onFailure(ctx::fail);
}
public void listar(RoutingContext ctx) {
int page = Integer.parseInt(ctx.queryParam("page").stream().findFirst().orElse("0"));
int size = Integer.parseInt(ctx.queryParam("size").stream().findFirst().orElse("20"));
service.listar(page, size)
.onSuccess(lista -> ctx.json(lista))
.onFailure(ctx::fail);
}
}9. Vert.x — HTTP Client para Chamadas Externas
O WebClient do Vert.x é um cliente HTTP não-bloqueante com API fluente. Ideal para chamadas a APIs externas, microsserviços e webhooks.
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.ext.web.codec.BodyCodec;
// Criação do cliente — reutilize a instância (não crie por request)
WebClient client = WebClient.create(vertx, new WebClientOptions()
.setDefaultHost("api.pagamento.com")
.setDefaultPort(443)
.setSsl(true)
.setConnectTimeout(3000)
.setIdleTimeout(10));
// GET simples
client.get("/v2/status")
.putHeader("Authorization", "Bearer " + token)
.send()
.onSuccess(response -> {
System.out.println("Status: " + response.statusCode());
JsonObject body = response.bodyAsJsonObject();
})
.onFailure(err -> System.err.println("Erro: " + err.getMessage()));
// POST com JSON
JsonObject payload = new JsonObject()
.put("pedidoId", pedidoId)
.put("valor", 99.90)
.put("moeda", "BRL")
.put("metodoPagamento", "pix");
client.post("/v2/cobrancas")
.putHeader("Content-Type", "application/json")
.putHeader("Authorization", "Bearer " + token)
.sendJsonObject(payload)
.onSuccess(response -> {
if (response.statusCode() == 201) {
String cobrancaId = response.bodyAsJsonObject().getString("id");
processarCobranca(cobrancaId);
} else {
log.warn("Cobrança rejeitada: {} - {}", response.statusCode(), response.bodyAsString());
}
})
.onFailure(err -> log.error("Falha ao criar cobrança", err));
// GET com BodyCodec — desserializa automaticamente
client.get("/v2/pedidos/" + pedidoId)
.as(BodyCodec.jsonObject())
.send()
.map(HttpResponse::body)
.onSuccess(pedido -> System.out.println("Cliente: " + pedido.getString("clienteId")));
// Retry com backoff usando Vert.x Future
Future<HttpResponse<JsonObject>> retentativa = client
.get("/v2/status")
.as(BodyCodec.jsonObject())
.send()
.expecting(HttpResponseExpectation.SC_SUCCESS)
.recover(err -> {
log.warn("Tentativa 1 falhou, retentando...");
return client.get("/v2/status").as(BodyCodec.jsonObject()).send();
});10. Vert.x — Future e Promise — Composição Assíncrona
Future<T> representa o resultado de uma operação assíncrona. Promise<T> é o produtor que completa um Future. Use compose para encadear e Future.all/Future.join para paralelismo.
// Fluxo de checkout — encadeamento de operações assíncronas
public Future<ConfirmacaoPedido> checkout(String pedidoId, DadosPagamento dados) {
return pedidoRepo.findById(pedidoId)
// compose — transforma o resultado em outro Future (flatMap)
.compose(pedido -> {
if (pedido == null) return Future.failedFuture("Pedido não encontrado");
return estoqueService.reservar(pedido.getItens())
.map(reservaId -> new PedidoComReserva(pedido, reservaId));
})
// compose — pagamento
.compose(pedidoReserva ->
pagamentoService.cobrar(dados, pedidoReserva.getPedido().getTotal())
.map(pagId -> new PedidoPago(pedidoReserva, pagId))
)
// compose — confirmar
.compose(pedidoPago ->
pedidoRepo.confirmar(pedidoPago.getPedido().getId(), pedidoPago.getPagamentoId())
)
// map — transformação síncrona (sem I/O)
.map(pedido -> new ConfirmacaoPedido(pedido.getId(), Instant.now()))
// recover — fallback ou tratamento de erro
.recover(err -> {
log.error("Checkout falhou para pedido {}: {}", pedidoId, err.getMessage());
// Tentativa de estornar reserva se necessário
estoqueService.liberarReserva(pedidoId).onFailure(e -> log.error("Falha ao liberar reserva", e));
return Future.failedFuture(new CheckoutException(err.getMessage()));
});
}
// Execução em paralelo — Future.all espera todos concluírem
public Future<DetalhePedido> buscarDetalhes(String pedidoId, String clienteId) {
Future<Pedido> pedidoFuture = pedidoRepo.findById(pedidoId);
Future<Cliente> clienteFuture = clienteRepo.findById(clienteId);
Future<List<Pagamento>> pagFuture = pagamentoRepo.findByPedidoId(pedidoId);
// Executa os três em paralelo, aguarda todos
return Future.all(pedidoFuture, clienteFuture, pagFuture)
.map(r -> new DetalhePedido(
r.resultAt(0), // Pedido
r.resultAt(1), // Cliente
r.resultAt(2) // List<Pagamento>
));
}
// Promise — quando você controla quando o Future completa
public Future<String> processarComCallback(String dados) {
Promise<String> promise = Promise.promise();
// Simula callback de biblioteca legada
legacyLib.processar(dados, new Callback() {
@Override public void onSuccess(String resultado) { promise.complete(resultado); }
@Override public void onError(Throwable t) { promise.fail(t); }
});
return promise.future();
}11. Vert.x — Event Bus
O Event Bus é o sistema de mensagens interno do Vert.x. Permite que Verticles se comuniquem sem referências diretas, habilitando baixo acoplamento e facilidade de teste.
// ========== CONSUMIDOR (receiver) ==========
// Point-to-point — recebe mensagens enviadas com send()
vertx.eventBus().<JsonObject>consumer("pedido.processar", message -> {
JsonObject body = message.body();
String pedidoId = body.getString("pedidoId");
pedidoService.processar(pedidoId)
.onSuccess(resultado -> {
// Responder ao remetente (apenas para request/reply)
message.reply(new JsonObject()
.put("status", "processado")
.put("pedidoId", pedidoId));
})
.onFailure(err -> {
message.fail(500, err.getMessage());
});
});
// Publish/subscribe — recebe mensagens enviadas com publish()
vertx.eventBus().<JsonObject>consumer("pedido.criado", message -> {
String clienteId = message.body().getString("clienteId");
emailService.enviarConfirmacao(clienteId);
// Não precisa responder — publish é fire-and-forget
});
// Segundo consumidor do mesmo endereço (pub/sub — ambos recebem)
vertx.eventBus().<JsonObject>consumer("pedido.criado", message -> {
estoqueService.reservar(message.body().getString("pedidoId"));
});// ========== PUBLICADOR / REMETENTE ==========
JsonObject payload = new JsonObject()
.put("pedidoId", pedidoId)
.put("clienteId", clienteId)
.put("total", 149.90);
// send — point-to-point (entrega a UM consumidor em round-robin)
vertx.eventBus().send("pedido.processar", payload);
// publish — broadcast (entrega a TODOS os consumidores registrados)
vertx.eventBus().publish("pedido.criado", payload);
// request/reply — envia e aguarda resposta como Future
vertx.eventBus().<JsonObject>request("pedido.processar", payload)
.onSuccess(reply -> {
String status = reply.body().getString("status");
log.info("Pedido processado com status: {}", status);
})
.onFailure(err -> log.error("Processamento falhou: {}", err.getMessage()));Codecs para tipos customizados:
// Codec para enviar objetos Java (não apenas JsonObject)
public class PedidoCodec implements MessageCodec<Pedido, Pedido> {
@Override
public void encodeToWire(Buffer buffer, Pedido pedido) {
// Serializa para bytes
byte[] bytes = objectMapper.writeValueAsBytes(pedido);
buffer.appendInt(bytes.length);
buffer.appendBytes(bytes);
}
@Override
public Pedido decodeFromWire(int pos, Buffer buffer) {
int length = buffer.getInt(pos);
byte[] bytes = buffer.getBytes(pos + 4, pos + 4 + length);
return objectMapper.readValue(bytes, Pedido.class);
}
@Override
public Pedido transform(Pedido pedido) { return pedido; } // local delivery
@Override public String name() { return "PedidoCodec"; }
@Override public byte systemCodecID() { return -1; }
}
// Registrar o codec
vertx.eventBus().registerCodec(new PedidoCodec());
// Enviar objeto tipado
vertx.eventBus().send("pedido.processar", pedido,
new DeliveryOptions().setCodecName("PedidoCodec"));12. Vert.x — SQL Client PostgreSQL
O PgBuilder cria um pool de conexões não-bloqueante. Todas as operações retornam Future<T>.
import io.vertx.pgclient.PgBuilder;
import io.vertx.pgclient.PgConnectOptions;
import io.vertx.sqlclient.*;
// Configuração do pool
PgConnectOptions conectOptions = new PgConnectOptions()
.setHost(System.getenv().getOrDefault("DB_HOST", "localhost"))
.setPort(5432)
.setDatabase("pedidos")
.setUser("dev")
.setPassword("dev")
.setReconnectAttempts(3)
.setReconnectInterval(1000);
PoolOptions poolOptions = new PoolOptions()
.setMaxSize(10)
.setMaxWaitQueueSize(30);
Pool pool = PgBuilder.pool()
.with(poolOptions)
.connectingTo(conectOptions)
.using(vertx)
.build();// SELECT com parâmetros preparados (evita SQL injection)
pool.preparedQuery("SELECT id, cliente_id, status, total FROM pedidos WHERE id = $1")
.execute(Tuple.of(pedidoId))
.onSuccess(rows -> {
if (rows.size() == 0) {
// não encontrado
return;
}
Row row = rows.iterator().next();
String clienteId = row.getString("cliente_id");
String status = row.getString("status");
BigDecimal total = row.getBigDecimal("total");
})
.onFailure(err -> log.error("Erro ao buscar pedido", err));
// INSERT
pool.preparedQuery("""
INSERT INTO pedidos (id, cliente_id, status, total, criado_em)
VALUES ($1, $2, $3, $4, NOW())
""")
.execute(Tuple.of(UUID.randomUUID().toString(), clienteId, "aberto", total))
.onSuccess(result -> log.info("Pedido inserido, rows: {}", result.rowCount()))
.onFailure(err -> log.error("Falha ao inserir pedido", err));
// Batch INSERT — insere múltiplos registros com um único preparedQuery
List<Tuple> batch = itens.stream()
.map(item -> Tuple.of(UUID.randomUUID().toString(), pedidoId, item.getSku(), item.getQuantidade()))
.toList();
pool.preparedQuery("INSERT INTO itens_pedido (id, pedido_id, sku, quantidade) VALUES ($1, $2, $3, $4)")
.executeBatch(batch)
.onSuccess(result -> log.info("{} itens inseridos", result.rowCount()))
.onFailure(err -> log.error("Falha no batch insert", err));
// Transação — garante atomicidade
pool.withTransaction(conn ->
conn.preparedQuery("UPDATE pedidos SET status = $1 WHERE id = $2")
.execute(Tuple.of("pago", pedidoId))
.compose(r -> conn.preparedQuery(
"INSERT INTO pagamentos (id, pedido_id, valor) VALUES ($1, $2, $3)")
.execute(Tuple.of(UUID.randomUUID().toString(), pedidoId, valor)))
).onSuccess(result -> log.info("Pedido pago e pagamento registrado"))
.onFailure(err -> log.error("Falha na transação de pagamento", err));13. Vert.x — Circuit Breaker
O Circuit Breaker protege serviços externos de falhas em cascata. Quando o número de falhas ultrapassa o limite, o circuito “abre” e chamadas subsequentes falham imediatamente sem tentar o serviço externo, dando tempo para ele se recuperar.
import io.vertx.circuitbreaker.CircuitBreaker;
import io.vertx.circuitbreaker.CircuitBreakerOptions;
// Usando Resilience4j (recomendado com Vert.x 5)
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.vertx.circuitbreaker.VertxCircuitBreaker;
// Configuração do Circuit Breaker
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.minimumNumberOfCalls(5) // mínimo de chamadas para avaliar
.failureRateThreshold(50) // % de falhas para abrir o circuito
.waitDurationInOpenState(Duration.ofSeconds(10)) // tempo em estado aberto
.slidingWindowSize(10) // janela de avaliação
.build();
io.github.resilience4j.circuitbreaker.CircuitBreaker cb =
io.github.resilience4j.circuitbreaker.CircuitBreaker.of("pagamento-gateway", config);
// Uso no handler HTTP
router.post("/api/pagamentos").handler(ctx -> {
JsonObject dados = ctx.body().asJsonObject();
VertxCircuitBreaker.executeFuture(cb, () ->
webClient.post("/cobrar")
.sendJsonObject(dados)
.expecting(HttpResponseExpectation.SC_SUCCESS)
)
.onSuccess(response ->
ctx.response()
.setStatusCode(201)
.end(response.bodyAsString()))
.onFailure(err -> {
if (err instanceof io.github.resilience4j.circuitbreaker.CallNotPermittedException) {
// Circuito aberto — serviço indisponível
ctx.response()
.setStatusCode(503)
.end(new JsonObject().put("error", "Gateway de pagamento temporariamente indisponível").encode());
} else {
ctx.fail(500, err);
}
});
});Circuit Breaker nativo do Vert.x (alternativa mais simples):
CircuitBreaker breaker = CircuitBreaker.create("estoque-service", vertx,
new CircuitBreakerOptions()
.setMaxFailures(5)
.setTimeout(3000)
.setFallbackOnFailure(true)
.setResetTimeout(10000) // tenta fechar após 10s
);
breaker.execute(promise -> {
estoqueClient.verificarDisponibilidade(skuList)
.onSuccess(promise::complete)
.onFailure(promise::fail);
})
.onSuccess(disponivel -> ctx.json(disponivel))
.onFailure(err -> {
// Fallback — pode retornar valor conservador ou cache
ctx.json(new JsonObject().put("disponivel", false).put("fonte", "fallback"));
});14. Vert.x — Health Checks
Health checks expõem endpoints /health/live e /health/ready para Kubernetes e load balancers saberem se o serviço está saudável.
import io.vertx.ext.healthchecks.HealthCheckHandler;
import io.vertx.ext.healthchecks.HealthChecks;
import io.vertx.ext.healthchecks.Status;
// Criar o sistema de health checks
HealthChecks healthChecks = HealthChecks.create(vertx);
// Health check de banco de dados
healthChecks.register("database", 3000 /* timeout em ms */, promise ->
pool.getConnection()
.compose(SqlConnection::close)
.onSuccess(v -> promise.complete(Status.OK()))
.onFailure(err -> promise.complete(Status.KO(
new JsonObject().put("motivo", err.getMessage()))))
);
// Health check do Event Bus
healthChecks.register("event-bus", promise ->
vertx.eventBus().<String>request("health.ping", "ping")
.onSuccess(msg -> promise.complete(Status.OK()))
.onFailure(err -> promise.complete(Status.KO()))
);
// Health check de serviço externo (gateway de pagamento)
healthChecks.register("pagamento-gateway", promise ->
webClient.get("/ping")
.send()
.onSuccess(resp -> {
if (resp.statusCode() == 200) {
promise.complete(Status.OK(new JsonObject()
.put("latencyMs", resp.getHeader("X-Response-Time"))));
} else {
promise.complete(Status.KO(new JsonObject()
.put("statusCode", resp.statusCode())));
}
})
.onFailure(err -> promise.complete(Status.KO(
new JsonObject().put("motivo", "timeout ou erro de conexão"))))
);
// Registrar os endpoints no router
HealthCheckHandler livenessHandler = HealthCheckHandler.createWithHealthChecks(healthChecks);
HealthCheckHandler readinessHandler = HealthCheckHandler.createWithHealthChecks(healthChecks);
router.get("/health/live").handler(livenessHandler);
router.get("/health/ready").handler(readinessHandler);
router.get("/health").handler(livenessHandler); // atalho15. Referência Rápida — Operadores e GDK
Operadores Groovy:
| Operador | Descrição | Exemplo |
|---|---|---|
?. | Safe navigation — para em null | user?.address?.city |
?: | Elvis — valor padrão | nome ?: "Anônimo" |
*. | Spread — aplica em todos | lista*.toUpperCase() |
** | Power | 2 ** 10 // 1024 |
<=> | Spaceship — compara (-1, 0, 1) | a <=> b |
in | Pertence | "x" in ["x", "y"] |
!in | Não pertence (Groovy 3+) | "z" !in ["x", "y"] |
instanceof | Teste de tipo | x instanceof String |
!instanceof | Não é tipo (Groovy 3+) | x !instanceof String |
=== / !== | Identidade de referência (Groovy 3+) | a === b |
~ | Regex pattern | ~/\d+/.matcher(s) |
=~ | Regex find | "abc123" =~ /\d+/ |
==~ | Regex match completo | "abc" ==~ /[a-z]+/ |
as | Conversão de tipo | "123" as Integer |
GDK — Métodos de Coleções:
| Método | Descrição |
|---|---|
each { } | Itera sem retorno |
collect { } | Transforma cada elemento |
findAll { } | Filtra elementos |
find { } | Retorna o primeiro que satisfaz |
inject(seed) { acc, v -> } | Fold/reduce |
sum { } | Soma com transformação |
min { } / max { } | Menor/maior por critério |
groupBy { } | Agrupa em Map |
countBy { } | Conta por critério |
any { } / every { } | Existencial / universal |
sort { } | Ordena por closure |
unique() | Remove duplicatas |
flatten() | Achata listas aninhadas |
collectMany { } | flatMap |
intersect() | Interseção de listas |
minus() | Diferença de listas |
16. Tabelas de Versões
Groovy
| Versão | Ano | Principais novidades |
|---|---|---|
| 2.x | 2012–2019 | Closures, GDK, Groovydoc, @CompileStatic, @TypeChecked, traits, @Delegate; Groovy 2.5 adicionou @Builder melhorado, @AutoFinal e suporte a Java 9 |
| 3.x | 2020 | Parrot parser — sintaxe Java 8+ completa (lambdas, method refs, default em interfaces); operadores !in, !instanceof, ===, !==; GINQ (query SQL sobre coleções); @POJO; suporte a Java 14+ |
| 4.x | 2022 | Suporte a JPMS (Java modules); paridade com Java 17 (sealed classes, records, switch expressions); novo esquema de versionamento (4.x em vez de 3.0.x); remoção do groovy-all monolith (use módulos); remoção de APIs depreciadas 2.x/3.x; suporte a Java 19+ |
| 5.x | 2025 (preview) | Suporte a Java 21+ features (virtual threads, pattern matching melhorado); alinhamento com GDK para tipos novos; melhorias em GINQ |
Vert.x
| Versão | Ano | Principais novidades |
|---|---|---|
| 3.x | 2015–2020 | Estabilização do modelo de Verticles; Web Router; Event Bus melhorado; suporte a Kotlin; gRPC; Service Proxy; métricas básicas |
| 4.x | 2021 | Future/Promise substituem completamente callbacks; Web Client reescrito; métricas com Micrometer integrado; gRPC client nativo; EventBus com codecs melhorados; Java 11+ mínimo; SQL Client redesenhado (reactive) |
| 5.x | 2024 | Suporte a Virtual Threads (Project Loom) — código bloqueante sem penalidade de performance; API unificada e simplificada; SQL Client melhorado; remoção das APIs depreciadas do 4.x; Java 11 mínimo mantido, Java 21 recomendado; integração Resilience4j oficial |