Aula 5: CDI Avançado e Bean Validation
Objetivos
- Dominar Producers (
@Produces) para criação de beans customizados - Utilizar Qualifiers para diferenciar implementações do mesmo tipo
- Implementar Interceptors para cross-cutting concerns (logging, auditoria, métricas)
- Aplicar o sistema de Events do CDI para comunicação desacoplada entre componentes
- Integrar Bean Validation (
@Valid,@NotBlank,@Size) para validação declarativa de dados
Contexto e Motivação
Na aula anterior, dominamos os fundamentos do CDI: @Inject, escopos e a arquitetura em camadas. Porém, aplicações reais apresentam cenários mais complexos que exigem recursos avançados do CDI. Considere as seguintes situações:
- Múltiplas implementações: Você tem um
NotificacaoServiceque pode enviar emails ou SMS. Como o CDI sabe qual injetar? - Beans que não são classes suas: Você precisa injetar um
ObjectMapperdo Jackson ou uma conexão configurada. Como criar beans a partir de métodos? - Cross-cutting concerns: Toda chamada de serviço precisa de logging e medição de tempo. Como evitar código duplicado em cada método?
- Eventos assíncronos: Quando um usuário se cadastra, é preciso enviar email de boas-vindas, criar perfil padrão e registrar auditoria. Como desacoplar essas ações?
- Validação de dados: Campos obrigatórios, tamanhos mínimos, formatos de email. Como validar de forma declarativa sem poluir a lógica de negócio?
O CDI avançado e o Bean Validation resolvem cada um desses problemas de forma elegante e padronizada.
Todos esses recursos fazem parte das especificações Jakarta EE. O que você aprende aqui funciona em qualquer runtime compatível (Quarkus, WildFly, Open Liberty), não apenas no Quarkus.
Conteúdo
Producers: Criando Beans Customizados
Um Producer é um método ou campo anotado com @Produces que cria beans que o CDI não consegue instanciar automaticamente. Isso é útil quando:
- O bean é de uma biblioteca externa (você não controla a classe)
- A criação requer lógica customizada (configuração, factory)
- O tipo depende de condições em runtime
@ApplicationScoped
public class ConfigProducer {
// Produz um ObjectMapper customizado para toda a aplicação
@Produces
@ApplicationScoped
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
// Produz configurações a partir de arquivo .properties
@Produces
@Named("appVersion")
public String appVersion(
@ConfigProperty(name = "quarkus.application.version") String version
) {
return version;
}
}
Agora, qualquer bean pode injetar esses recursos:
@ApplicationScoped
public class ExportService {
@Inject
ObjectMapper mapper; // Injetado via Producer
@Inject
@Named("appVersion")
String versao;
public String exportarParaJson(Object dados) throws JsonProcessingException {
return mapper.writeValueAsString(dados);
}
}
Use Producers quando precisar controlar a criação de um bean. Para classes que você controla diretamente, prefira as anotações de escopo (@ApplicationScoped, etc.) — é mais simples e idiomático.
Qualifiers: Diferenciando Implementações
Quando existe mais de uma implementação de uma interface, o CDI precisa de uma forma de distingui-las. Qualifiers são anotações customizadas que resolvem essa ambiguidade.
Cenário: O CritiqueHub precisa enviar notificações por email e por SMS.
// Interface comum
public interface NotificacaoService {
void enviar(String destinatario, String mensagem);
}
// Qualifier customizado
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
public @interface Via {
Canal value();
enum Canal { EMAIL, SMS }
}
// Implementação para Email
@ApplicationScoped
@Via(Via.Canal.EMAIL)
public class EmailNotificacaoService implements NotificacaoService {
@Override
public void enviar(String destinatario, String mensagem) {
// Lógica de envio por email
System.out.println("EMAIL para " + destinatario + ": " + mensagem);
}
}
// Implementação para SMS
@ApplicationScoped
@Via(Via.Canal.SMS)
public class SmsNotificacaoService implements NotificacaoService {
@Override
public void enviar(String destinatario, String mensagem) {
// Lógica de envio por SMS
System.out.println("SMS para " + destinatario + ": " + mensagem);
}
}
Uso com injeção qualificada:
@ApplicationScoped
public class UsuarioService {
@Inject
@Via(Via.Canal.EMAIL)
NotificacaoService emailService;
@Inject
@Via(Via.Canal.SMS)
NotificacaoService smsService;
public void boasVindas(Usuario usuario) {
emailService.enviar(usuario.getEmail(), "Bem-vindo ao CritiqueHub!");
if (usuario.getTelefone() != null) {
smsService.enviar(usuario.getTelefone(), "Cadastro confirmado!");
}
}
}
Interceptors: Cross-Cutting Concerns
Interceptors permitem executar lógica antes e depois de métodos sem modificar o código original. Isso é o mecanismo de AOP (Aspect-Oriented Programming) do CDI.
Cenário: Registrar log e medir tempo de execução de todos os métodos de serviço.
// 1. Definir o Interceptor Binding (anotação)
@InterceptorBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Auditavel {}
// 2. Implementar o Interceptor
@Auditavel
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class AuditoriaInterceptor {
private static final Logger LOG = Logger.getLogger(AuditoriaInterceptor.class);
@AroundInvoke
public Object auditar(InvocationContext context) throws Exception {
String metodo = context.getMethod().getName();
String classe = context.getTarget().getClass().getSimpleName();
LOG.infof(">>> Iniciando %s.%s()", classe, metodo);
long inicio = System.currentTimeMillis();
try {
Object resultado = context.proceed(); // Executa o método original
long duracao = System.currentTimeMillis() - inicio;
LOG.infof("<<< %s.%s() concluído em %dms", classe, metodo, duracao);
return resultado;
} catch (Exception e) {
long duracao = System.currentTimeMillis() - inicio;
LOG.errorf("!!! %s.%s() falhou após %dms: %s", classe, metodo, duracao, e.getMessage());
throw e;
}
}
}
Uso — basta anotar a classe ou método:
@ApplicationScoped
@Auditavel // Todos os métodos desta classe serão auditados
public class AvaliacaoService {
@Inject
AvaliacaoRepository repository;
public Avaliacao criar(Avaliacao avaliacao) {
// Lógica de criação — o interceptor cuida do log automaticamente
return repository.salvar(avaliacao);
}
}
| Tipo de Interceptor | Anotação | Quando Usar |
|---|---|---|
| @AroundInvoke | Métodos de negócio | Logging, métricas, segurança |
| @AroundConstruct | Construtor | Inicialização customizada |
| @PostConstruct | Após criação do bean | Setup de recursos |
| @PreDestroy | Antes da destruição | Cleanup de recursos |
Events: Comunicação Desacoplada
O sistema de Events do CDI permite que beans se comuniquem sem conhecer uns aos outros. O emissor dispara um evento, e observadores interessados reagem.
// Evento (um record simples)
public record UsuarioCadastrado(Long id, String email, String nome) {}
// Emissor — dispara o evento
@ApplicationScoped
public class UsuarioService {
@Inject
Event<UsuarioCadastrado> eventoCadastro;
@Transactional
public Usuario cadastrar(String email, String nome) {
Usuario usuario = new Usuario(email, nome);
repository.salvar(usuario);
// Dispara o evento — não sabe quem vai observar
eventoCadastro.fire(new UsuarioCadastrado(usuario.getId(), email, nome));
return usuario;
}
}
// Observador 1 — envia email de boas-vindas
@ApplicationScoped
public class EmailBoasVindas {
public void onCadastro(@Observes UsuarioCadastrado evento) {
System.out.println("Enviando email para " + evento.email());
}
}
// Observador 2 — registra auditoria
@ApplicationScoped
public class AuditoriaObserver {
public void onCadastro(@Observes UsuarioCadastrado evento) {
System.out.println("Auditoria: novo usuário " + evento.nome() + " (ID: " + evento.id() + ")");
}
}
O UsuarioService não sabe que EmailBoasVindas ou AuditoriaObserver existem. Novos observadores podem ser adicionados sem alterar o serviço — isso é o Open/Closed Principle na prática.
Bean Validation: Validação Declarativa
Bean Validation (Jakarta Validation) permite validar dados usando anotações, sem poluir a lógica de negócio com if/else:
public class CriarUsuarioRequest {
@NotBlank(message = "Nome é obrigatório")
@Size(min = 2, max = 150, message = "Nome deve ter entre 2 e 150 caracteres")
private String nome;
@NotBlank(message = "Email é obrigatório")
@Email(message = "Email deve ter formato válido")
private String email;
@NotBlank(message = "Nome de usuário é obrigatório")
@Size(min = 3, max = 30, message = "Nome de usuário deve ter entre 3 e 30 caracteres")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Nome de usuário aceita apenas letras, números e _")
private String nomeUsuario;
@Min(value = 13, message = "Idade mínima é 13 anos")
private Integer idade;
// Getters e Setters
}
Constraints mais comuns:
| Anotação | Função | Exemplo |
|---|---|---|
@NotNull | Não pode ser nulo | Campos obrigatórios |
@NotBlank | Não pode ser nulo, vazio ou só espaços | Strings obrigatórias |
@Size(min, max) | Tamanho entre min e max | Nomes, descrições |
@Email | Formato de email válido | Campos de email |
@Min / @Max | Valor numérico mínimo/máximo | Notas (1-10) |
@Pattern(regexp) | Corresponde ao padrão regex | Usernames, códigos |
@Past / @Future | Data no passado/futuro | Datas de nascimento |
Integração com JAX-RS — usando @Valid:
@Path("/usuarios")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UsuarioResource {
@POST
public Response criar(@Valid CriarUsuarioRequest request) {
// Se a validação falhar, o Quarkus retorna automaticamente 400 Bad Request
// com detalhes dos erros de validação
Usuario usuario = service.criar(request);
return Response.status(Response.Status.CREATED).entity(usuario).build();
}
}
Validação customizada:
// Constraint customizada
@Constraint(validatedBy = NotaPossivelValidator.class)
@Retention(RUNTIME)
@Target({FIELD, PARAMETER})
public @interface NotaPossivel {
String message() default "Nota deve estar entre 1 e 10";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Implementação do validador
public class NotaPossivelValidator implements ConstraintValidator<NotaPossivel, Integer> {
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
if (value == null) return true; // @NotNull cuida disso
return value >= 1 && value <= 10;
}
}
// Uso
public class CriarAvaliacaoRequest {
@NotNull
@NotaPossivel
private Integer nota;
}
Exercícios (Checkpoints)
- Crie um Producer que produza uma instância de
java.util.Randomcom seed configurável viaapplication.properties. - Implemente duas implementações de
FormatadorService(JSON e XML) com Qualifiers, e crie um endpoint que use ambas. - Crie um Interceptor
@Cronometradoque meça e registre o tempo de execução de métodos, e aplique-o aoProdutoService. - Implemente um sistema de eventos onde a criação de um produto dispara notificações para
LogObservereCacheObserver. - Adicione validações Bean Validation ao
ProdutoDTO(nome obrigatório, preço positivo) e verifique que o endpoint POST retorna 400 para dados inválidos.