A principal divergência entre o Micronaut e outros frameworks é o uso minimo de reflection e a geração de código em compile-time visando melhorar o startup time, consumo de memória e se adequar melhor a ambientes serverless.

Projetos podem ser iniciados usando o site micronaut.io/launch

Dependency Injection

Um Bean é um objeto que tem seu ciclo de vida gerenciado pelo Micronaut. O framework usa as annotations do pacote jakarta.inject para definir o ciclo de vida dos Beans.

Sempre que possível é recomendado usar a injeção de dependências pelo construtor:

class MyService {
    private final MyRepository repository
 
    MyService(MyRepository repository) {
        this.repository = repository
    }
}

Escopo dos Beans

AnnotationDescrição
@SingletonApenas uma instância do Bean vai existir, ela é criada na primeira vez que for injetada.
@ContextApenas uma instância vai existir, ela é criada na inicialização da aplicação (quando ApplicationContext é criado).
@PrototypeUma nova instância é criada toda vez que o Bean é injetado.
@RequestScopeUma nova instância é criada para cada requisição HTTP.

@Controller s e @Client s são utilizam @Singleton por padrão. Outros Beans que sejam Stateless podem ser anotados com @Singleton ou @Context .

Quando for nessário guardar o contexto de uma requisição um Bean @RequestScope pode ser util.

Bean Intospection

A annotation @Introspected é usada para gerar o codigo necessário para permitir que o comportamento de um objeto seja alterado em runtime sem o uso de reflection.

Na prática, classes que utilizam @Validated ou DTOs que sofrerem Serialização/Deserialização usam @Introspected .

Bean Mappers

Mappers são utilizados para converter objetos Request/Response (DTOs) em entidades de domínio ou banco de dados e vice-versa, sem precisar escrever algo como:

Usuario usuario = new Usuario()
usuario.nome = dto.nome
usuario.email = dto.email
usuario.idade = dto.idade

Quando os campos da origem e destino possuem os mesmos nomes e tipos, a cópia pode ser feita automaticamente usando a annotation @Mapper :

@Singleton
interface UsuarioMapper {
    @Mapper
    Usuario toEntity(UsuarioRequestDTO requestDTO)
    
    @Mapper
    UsuarioResponseDTO toDto(Usuario usuario)
}
@Controller("/usuarios")
class UsuarioController {
 
    private final UsuarioService service
    private final UsuarioMapper mapper
 
    UsuarioController(UsuarioService service, UsuarioMapper mapper) {
        this.service = service
        this.mapper = mapper
    }
 
    @Post
    UsuarioResponseDTO criar(UsuarioRequestDTO request) {
        Usuario usuario = mapper.toEntity(request) // Converte o DTO do JSON para a entidade de negócio
        Usuario usuarioSalvo = service.salvar(usuario)        
        return mapper.toDto(usuarioSalvo) // Converte de volta para mandar em JSON
    }
}

Tanto a classe de origem quanto a de destino precisam ser anotadas com @Introspected .

Bean Validation

Utilizando a annotation @Validated é possível validar os métodos ou campos de um Bean, depois basta usar @Valid para em outros métodos que recebem esse Bean como parâmetro.

@Introspected
class NovoUsuarioRequestDTO {
 
    // @NotBlank: Não é null, não é vazio ("") e não tem apenas espaços em branco ("   ").
    @NotBlank(message = "O nome é obrigatório")
    @Size(min = 3, max = 100, message = "O nome deve ter entre 3 e 100 caracteres")
    String nome
 
    @NotBlank(message = "O e-mail é obrigatório")
    @Email(message = "Formato de e-mail inválido")
    String email
 
    // @NotNull: Usado para objetos, números e listas não serem nulos.
    @NotNull(message = "A idade não pode ser nula")
    @Min(value = 18, message = "O usuário deve ter pelo menos 18 anos")
    Integer idade
 
    @NotBlank(message = "O CPF é obrigatório")
    @Pattern(regexp = "^\\d{11}\$", message = "O CPF deve conter exatamente 11 dígitos numéricos")
    String cpf
 
    // Valida o tamanho de collections.
    // `List<@NotBlank String>` ainda não é suportado para validar elementos internos.
    @NotNull(message = "A lista de telefones não pode ser nula")
    @Size(min = 1, max = 3, message = "Forneça entre 1 e 3 números de telefone")
    List<String> telefones
}
@Controller("/usuarios")
class UsuarioController {
 
    // `@Valid` aplica as validações antes de entrar no método. 
    @Post
    HttpResponse<String> registrar(@Body @Valid NovoUsuarioRequestDTO request) {
        

    }
}

Também é possível criar validações customizadas, isso exige criar uma annotation e um ConstraintValidator para a lógica de validação:

@Retention(RUNTIME)
// Aponta para a classe que conterá a lógica de validação
@Constraint(validatedBy = MaiorDeIdadeValidator)
@interface MaiorDeIdade {
    String message() default "O usuário deve ser maior de 18 anos"
}
@Singleton
class MaiorDeIdadeValidator implements ConstraintValidator<MaiorDeIdade, LocalDate> {
 
    @Override
    boolean isValid(LocalDate dataNascimento,
                    AnnotationValue<MaiorDeIdade> annotationMetadata,
                    ConstraintValidatorContext context) {
        
        // Validadores customizados geralmente consideram null como válido.
        // Isso deve ser tratado utilizando o `@NotNull`.
        if (dataNascimento == null) {
            return true
        }
 
        LocalDate dataLimite = LocalDate.now().minusYears(18)
        return !dataNascimento.isAfter(dataLimite)
    }
}
@Introspected
class NovoUsuarioRequestDTO {
 
    @NotNull(message = "A data de nascimento é obrigatória")
    @MaiorDeIdade(message = "Você deve ter pelo menos 18 anos para se cadastrar")
    LocalDate dataNascimento
}

As seguintes annotations são usadas para definir se algo pode ou não ser nulo:

  • @NonNull - O elemento nunca deve ser null.
  • @Nullable - O elemento pode ser null.

Por padrão, o Micronaut assume @NonNull na maioria dos casos.

O seguinte exemplo faz com que o parâmetro nome ( /dar-oi?nome= ) seja opcional e não cause erro.

@Controller("/dar-oi")
class DarOiController {
 
    @Get
    String oi(@QueryValue @Nullable String nome) {
        return nome ? "Oi, $nome!" : "Oi!"
    }
}

Configurações da aplicação

As configurações são definidas em application.properties e variações como application-prod.properties . As variações podem ser ativadas usando a environment variable MICRONAUT_ENVIRONMENTS=prod ou a JVM -Dmicronaut.environments=prod .

Para injetar valores isolados no código, usa-se @Property ou @Value :

@Singleton
class NotificationService {
 
    // Busca a chave exata, falha ao iniciar se a chave não existir.
    @Property(name = "app.notificacao.email-admin")
    String emailAdmin
 
    // Permite usar templates e definir um valor fallback.
    @Value('${app.notificacao.url-server: `http://localhost:8080` }')
    String serverUrl
}

Configuration Properties

Para agrupar configurações relacionadas e tipadas usa-se a annotation @ConfigurationProperties em uma classe de configurações:

security.hashing=argon2
security.argon2.memory=65536
security.argon2.iterations=3
security.argon2.parallelism=2
security.argon2.salt-length=16
security.argon2.hash-length=32
@Validated
@ConfigurationProperties("security")
class SecurityConfig {
 
    @NotBlank
    String hashing
 
    Argon2Config argon2 = new Argon2Config()
 
    // security.argon2
    @ConfigurationProperties("argon2")
    static class Argon2Config {
        
        @NotNull
        @Min(16384) // 16 MB
        Integer memory
        @NotNull
        @Min(2)
        Integer iterations
        @NotNull
        @Min(1)
        Integer parallelism
        @NotNull
        @Min(8)
        Integer saltLength 
        @NotNull
        @Min(16)
        Integer hashLength
    }
}

Immutable Configuration

Também é possível criar configurações imutáveis usando @ConfigurationProperties em uma interface.

@ConfigurationProperties("my.engine")
interface EngineConfig {
 
    @Bindable(defaultValue = "Ford") 
    @NotBlank
    String getManufacturer()
 
    @Min(1)
    int getCylinders()
    
    // Valores opcionais podem retornar Optional<T> ou usar @Nullable
    Optional<Double> getRodLength()
}

Environment Variables

A convenção padrão mapeia automaticamente as environment variables para as chaves de configuração, usando os nomes em SNAKE_CASE para chaves separadas por ponto. As configurações do application.properties sempre são sobrepostas pelas environment variables.

Environment VariableConfiguração
SERVER_PORT=8080server.port
DATABASE_URL=...database.url
SECURITY_ARGON__2_MEMORY=100security.argon-2.memory

Aspected Oriented Programming

O uso mais comum de AOP vai ser por meio dos “Around Advice”s, criando interceptadores de métodos para adicionar comportamentos antes e depois.

Para isso, o primeiro passo é definir uma annotation utilizando o @Around :

@Retention(RUNTIME)
@Target([TYPE, METHOD]) // Pode ser aplicada em classes e métodos
@Around
@interface NomeAnnotation {
}

O proximo passo é implementar um interceptor com a lógica para a annotation:

@Singleton
@InterceptorBean(NomeAnnotation) // Conecta o interceptor com a annotation 
class NomeAnnotationInterceptor implements MethodInterceptor<Object, Object> {
 
    @Override
    Object intercept(MethodInvocationContext<Object, Object> context) {
        long inicio = System.currentTimeMillis()
 
        try {
            return context.proceed() // Chama o método original
        } finally {
            long tempo = System.currentTimeMillis() - inicio
            println "  [${context.methodName}] levou ${tempo}ms para executar."
        }
    }
}

HTTP Server

A porta padrão do server é 8080 e pode ser alterada usando server.port .

micronaut.server.port=8082

HTTP Routing

Rotas podem ser definidas usando URIs com templates:

TemplateDescriptionMatching URI
/books/{id}Simple match/books/1
/books/{id:2}A variable of two characters max/books/10
/books{/id}An optional URI variable/books/10 or /books
/book{/id:[a-zA-Z]+}An optional URI variable with regex/books/foo
/books{?max,offset}Optional query parameters/books?max=10&offset=10
/books{/path:.*}{.ext}Regex path match with extension/books/foo/bar.xml

Os métodos são definidos com as annotations:

  • @Delete
  • @Get
  • @Head
  • @Options
  • @Patch
  • @Put
  • @Post

Exemplo:

@Controller("/pedidos")
class PedidoController {
 
    // /pedidos/123
    @Get("/{id}")
    HttpResponse<PedidoDTO> buscar(Long id) {
        // ...
    }
 
    // /pedidos/codigo/ABC123
    @Get("/codigo/{numero}") // @PathVariable permite usar um nome diferente para o parâmetro do método
    HttpResponse<PedidoDTO> buscarPorCodigo(@PathVariable("numero") String codigo) {
        // ...
    }
}

Request Binding

Para extrair outros dados da requisição são usadas as seguintes annotations:

AnnotationDescriptionExample
@BodyBinds from the body of the request@Body String body
@CookieValueBinds a parameter from a cookie@CookieValue String myCookie
@HeaderBinds a parameter from an HTTP header@Header String requestId
@QueryValueBinds from a request query parameter@QueryValue String myParam
@PartBinds from a part of a multipart request@Part CompletedFileUpload file
@RequestAttributeBinds from an attribute of the request. Attributes are typically created in filters@RequestAttribute String myAttribute
@PathVariableBinds from the path of the request@PathVariable String id
@RequestBeanBinds any Bindable value to single Bean object@RequestBean MyBean bean

Exemplo de uso (tudo é obrigatório a menos que seja anotado com @Nullable ou tenha um valor default):

@Controller("/")
class BindingController {
 
    @Post("/usuarios")
    HttpResponse<String> criarUsuario(@Body NovoUsuarioDTO usuarioDto) {
        return HttpResponse.created("Usuário criado: ${usuarioDto.nome}")
    }
 
    @Get("/carrinho")
    HttpResponse<String> carregarCarrinho(@CookieValue("SESSION_ID") String sessionId) {
        return HttpResponse.ok("Carrinho da sessão $sessionId")
    }
 
    @Get("/private")
    HttpResponse<String> acessarDados(@Header("Authorization") String token) {
        return HttpResponse.ok("Token: $token")
    }
 
    // Opcional se anotado com @Nullable.
    @Get("/produtos")
    HttpResponse<String> listarProdutos(@QueryValue @Nullable String categoria, @QueryValue(defaultValue = "1") int pagina) {
        return HttpResponse.ok("Categoria: $categoria, Página: $pagina")
    }
 
    @Post(value = "/usuarios/{id}/upload", consumes = MediaType.MULTIPART_FORM_DATA)
    HttpResponse<String> upload(@PathVariable Long id, @Part CompletedFileUpload foto) {
        return HttpResponse.ok("Arquivo ${foto.filename} recebida para o usuário $id")
    }
 
    @Get("/perfil")
    HttpResponse<String> meuPerfil(@RequestAttribute("usuarioLogado") String usuario) {
        return HttpResponse.ok("Perfil do usuário: $usuario")
    }
 
    @Get("/pedidos/{numeroPedido}")
    HttpResponse<String> buscarPedido(@PathVariable String numeroPedido) {
        return HttpResponse.ok("Buscando pedido: $numeroPedido")
    }
 
    // Agrupa múltiplos parâmetros da requisição (Query params, Headers, ...) em um Bean.
    @Get("/relatorios{?filtros*}")
    HttpResponse<String> gerarRelatorio(@RequestBean FiltroRelatorioBean filtros) {
        return HttpResponse.ok("Relatório para o id ${filtros.id} a partir de ${filtros.dataInicio}")
    }
}
 
@Introspected
class FiltroRelatorioBean {
    @QueryValue @Nullable LocalDate dataInicio
    @QueryValue @Nullable LocalDate dataFim
    @Header("X-Relatorio-Id") String id
}

HttpRequest e HttpResponse

Por padrão os métodos de um controller retornam status 200, isso pode ser alterado de algumas formas.

Usando a annotation @Status :

@Post
@Status(HttpStatus.CREATED)
Usuario salvar(@Body Usuario usuario) { // 201
    return service.save(usuario)
}

Retornando um HttpResponse que inclui um status customizadoe um body customizados:

@Get("/{id}")
HttpResponse buscar(Long id) {
    def resultado = service.find(id)
    if (resultado) {
        return HttpResponse.ok(resultado)
    }
 
    return HttpResponse.status(HttpStatus.NOT_FOUND).body("Não encontrado")
}

Retornando apenas o status:

@Delete("/{id}")
HttpStatus deletar(Long id) {
    service.delete(id)
    return HttpStatus.NO_CONTENT // 204
}

Para acesso total aos detalhes da requisição e resposta um método pode usar as seguintes interfaces:

  • HttpRequest
  • HttpHeaders
  • HttpParameters
  • Cookies
@Controller("/")
class HelloController {
 
    @Get("/hello")
    HttpResponse<String> hello(HttpRequest<?> request) {
        String usuario = request.parameters
                                .getFirst("user")
                                .orElse("Visitante")
 
        return HttpResponse.ok("Olá, $usuario")
                           .header("X-Header-Customizado", "Bem-vindo")
    }
}

Content-Type

Por padrão o Micronaut sempre utiliza application/json isso pode ser alterado usando a annotation @Produces ou settando produces diretamente na annotation do verbo HTTP:

    @Get
    HttpResponse index() {
        HttpResponse.ok().body('{"msg":"Apenas JSON"}')
    }
 
    @Produces(MediaType.TEXT_HTML)
    @Get("/html")
    String html() {
        "<html><title><h1>HTML</h1></title><body></body></html>"
    }
 
    @Get(value = "/xml", produces = MediaType.TEXT_XML)
    String xml() {
        "<message>XML</message>"
    }

Mesma coisa para o Content-Type :

    @Consumes([MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON])
    @Post("/multiple")
    HttpResponse multipleConsumes() {
        HttpResponse.ok()
    }
 
    @Post(value = "/member", consumes = MediaType.TEXT_PLAIN)
    HttpResponse consumesMember() {
        HttpResponse.ok()
    }

Reactive HTTP Request Processing

Dependendo da abordagem com o banco de dados, pode ser nescessário configurar métodos que acessam o banco de dados para não bloquearem (JDBC).

Em um cenário bloqueante (JPA/Hibernate com JDBC) deve-se utilizar a annotation @ExecuteOn para que o método seja executado em uma thread separada.

    @Get("/{id}")
    @ExecuteOn(TaskExecutors.IO)
    Usuario buscar(Long id) {
        return usuarioRepository.findById(id).orElse(null)
    }

Já em um cenário reativo, os métodos devem retornar um tipo reativo correspondente e será assíncrono, então não é necessário usar @ExecuteOn .

JSON Binding

DTOs que precisam ser serializados/deserializados para JSON devem ser anotados com @Serdeable , caso ja estiverem anotados com @Introspected deve ser mantido apenas o @Serdeable (já inclui @Introspected ).

@Serdeable 
class PessoaDTO {
    String nome
    Integer idade
}

Também é possível processar JSON de maneira reativa:

@Post("/reativo")
@SingleResult //
Publisher<HttpResponse<UsuarioDTO>> salvarReativo(@Body Publisher<UsuarioDTO> usuarioPublisher) {
    
    return Mono.from(usuarioPublisher).map { usuario ->
        HttpResponse.created(usuario)
    }
}

Error Handling

A classe de erros padrão JsonError gera uma mensagem e HATEOAS, erros de validação geram uma resposta automaticamente.

Para criar expections customizadas e capturar seus erros, cria-se uma classe ExceptionHandler :

@Singleton
class ContaBloqueadaExceptionHandler implements ExceptionHandler<ContaBloqueadaException, HttpResponse<JsonError>> {
 
    @Override
    HttpResponse<JsonError> handle(HttpRequest request, ContaBloqueadaException exception) {
        
        def erro = new JsonError("Conta bloqueada: ${exception.message}")
        
        return HttpResponse.status(HttpStatus.FORBIDDEN).body(erro)
    }
}

HTTP Filters

Middlewares são criados usando HTTP Filters, que utilizam filters methods.

Filter methods devem ser declarados em um Bean anotado com @ServerFilter (para interceptar requisições no servidor) ou @ClientFilter para interceptar requisições feitas pelo HTTP client. Cada filter method também deve ser anotado com @RequestFilter (antes da requisição ser processada) ou @ResponseFilter (após a requisição ser completada).

Esses métodos podem receber parâmetros como HttpRequest e HttpResponse ( HttpResponse apenas para response filters). O tipo de retorno pode ser void ou null (para continuar a execução normal), ou um HttpRequest modificado (em request filters) ou um HttpResponse . O retorno de tipos reativos também pode ser feito para construir filtros assíncronos.

Request filters podem ter acesso ao Body usando a anotação @Body .

Cada filtro é vinculado a um template de URI, por exemplo:

PatternExample Matched Paths
/**any path
customer/j?ycustomer/joy, customer/jay
customer/*/idcustomer/adam/id, customer/amy/id
customer/**customer/adam, customer/adam/id, customer/adam/name
customer/**/*.htmlcustomer/index.html, customer/adam/profile.html, customer/adam/job/description.html

Server Filter Bloqueante e Assíncrono

Quando uma requisição possui múltiplos filtros, a annotation @Order define qual roda primeiro.

Caso um filtro precise executar algo bloqueante, deve ser usado @ExecuteOn :

@ServerFilter("/hello/**")
class TraceFilter {
 
    private final TraceService traceService
 
    @RequestFilter
    @ExecuteOn(TaskExecutors.BLOCKING) // Não bloqueia
    void filterRequest(HttpRequest<?> request) {
        traceService.trace(request)
    }
 
    @ResponseFilter
    void filterResponse(MutableHttpResponse<?> res) {
        res.headers.add("X-Trace-Enabled", "true")
    }
}

Fluxo em Filter Methods

Para continuar ou impedir o fluxo a requisição, pode-se retornar um CompletableFuture :

@ServerFilter(ServerFilter.MATCH_ALL_PATTERN) // Roda sempre
class FooBarFilter {
    
    @RequestFilter
    CompletableFuture<@Nullable HttpResponse<?>> filter(@NonNull HttpRequest<?> request) {
        if (request.headers.contains("X-FOOBAR")) {
            return CompletableFuture.completedFuture(null) // Continua
        } else {
            return CompletableFuture.completedFuture(HttpResponse.unauthorized()) // Encerrar
        }
    }
}

File Uploads

Para receber arquivos o controller deve receber MediaType.MULTIPART_FORM_DATA , byte[] pode ser usado para arquivos pequenos mas para arquivos maiores é recomendado usar StreamingFileUpload .

@Controller("/arquivos")
class UploadController {
 
    @Post(value = "/upload", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN)
    Mono<HttpResponse<String>> upload(StreamingFileUpload file) {
        
        File tempFile = File.createTempFile(file.filename, "temp")
        // `transferTo` não bloqueia.
        Publisher<Boolean> uploadPublisher = file.transferTo(tempFile)
 
        Mono.from(uploadPublisher)
            .map({ success ->
                if (success) {
                    HttpResponse.ok("Uploaded")
                } else {
                    HttpResponse.<String>status(CONFLICT)
                            .body("Upload Failed")
                }
            })
 
    }
}

File Transfer

Para transferir arquivos que já existem no disco é usado o SystemFile , caso contrário, usa-se o StreamedFile .

    SystemFile baixarRelatorio() {
        File file =
 
        return new SystemFile(file).attach("relatorio.pdf")
        // ou: new SystemFile(file, MediaType.TEXT_HTML_TYPE)
    }

No caso de transferir arquivos para um frontend/download:

@Get("/relatorio/download")
StreamedFile baixarParaFrontend() {
    InputStream fluxoPdf = relatorioService.gerarPdf()
    
    return new StreamedFile(fluxoPdf, MediaType.APPLICATION_PDF_TYPE)
                 .attach("relatorio.pdf") 
}

Quando o arquivo é transferido para outro serviço, o .attach() não é necessário.

HTTP Client