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
| Annotation | Descrição |
|---|---|
@Singleton | Apenas uma instância do Bean vai existir, ela é criada na primeira vez que for injetada. |
@Context | Apenas uma instância vai existir, ela é criada na inicialização da aplicação (quando ApplicationContext é criado). |
@Prototype | Uma nova instância é criada toda vez que o Bean é injetado. |
@RequestScope | Uma 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 Variable | Configuração |
|---|---|
SERVER_PORT=8080 | server.port |
DATABASE_URL=... | database.url |
SECURITY_ARGON__2_MEMORY=100 | security.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=8082HTTP Routing
Rotas podem ser definidas usando URIs com templates:
| Template | Description | Matching 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:
| Annotation | Description | Example |
|---|---|---|
@Body | Binds from the body of the request | @Body String body |
@CookieValue | Binds a parameter from a cookie | @CookieValue String myCookie |
@Header | Binds a parameter from an HTTP header | @Header String requestId |
@QueryValue | Binds from a request query parameter | @QueryValue String myParam |
@Part | Binds from a part of a multipart request | @Part CompletedFileUpload file |
@RequestAttribute | Binds from an attribute of the request. Attributes are typically created in filters | @RequestAttribute String myAttribute |
@PathVariable | Binds from the path of the request | @PathVariable String id |
@RequestBean | Binds 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:
| Pattern | Example Matched Paths |
|---|---|
/** | any path |
customer/j?y | customer/joy, customer/jay |
customer/*/id | customer/adam/id, customer/amy/id |
customer/** | customer/adam, customer/adam/id, customer/adam/name |
customer/**/*.html | customer/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.