Pular para o conteúdo principal

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 NotificacaoService que pode enviar emails ou SMS. Como o CDI sabe qual injetar?
  • Beans que não são classes suas: Você precisa injetar um ObjectMapper do 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.

observação

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);
}
}
dica

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 InterceptorAnotaçãoQuando Usar
@AroundInvokeMétodos de negócioLogging, métricas, segurança
@AroundConstructConstrutorInicialização customizada
@PostConstructApós criação do beanSetup de recursos
@PreDestroyAntes da destruiçãoCleanup 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() + ")");
}
}
important

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çãoFunçãoExemplo
@NotNullNão pode ser nuloCampos obrigatórios
@NotBlankNão pode ser nulo, vazio ou só espaçosStrings obrigatórias
@Size(min, max)Tamanho entre min e maxNomes, descrições
@EmailFormato de email válidoCampos de email
@Min / @MaxValor numérico mínimo/máximoNotas (1-10)
@Pattern(regexp)Corresponde ao padrão regexUsernames, códigos
@Past / @FutureData no passado/futuroDatas 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)

  1. Crie um Producer que produza uma instância de java.util.Random com seed configurável via application.properties.
  2. Implemente duas implementações de FormatadorService (JSON e XML) com Qualifiers, e crie um endpoint que use ambas.
  3. Crie um Interceptor @Cronometrado que meça e registre o tempo de execução de métodos, e aplique-o ao ProdutoService.
  4. Implemente um sistema de eventos onde a criação de um produto dispara notificações para LogObserver e CacheObserver.
  5. 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.

Referências