Demoiselle Framework v4 — Modernização Jakarta EE 10
O Demoiselle Framework v4 foi modernizado para aproveitar plenamente os recursos do Jakarta EE 10, CDI 4.0 e Java 21. Esta documentação cobre as funcionalidades atuais e as orientações de migração.
← Voltar ao portal do projeto · Migração 4.1 · Extensões de produção
Início rápido
Requisitos: Java 21+, Maven 3.9+ e um runtime Jakarta EE 10. Em aplicações, prefira importar o BOM em vez de repetir versões por módulo:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-parent-bom</artifactId>
<version>4.1.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-core</artifactId>
</dependency>
<dependency>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-rest</artifactId>
</dependency>
<dependency>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-crud</artifactId>
</dependency>
</dependencies>
Para snapshots, use o repositório Sonatype OSS documentado no README no GitHub. O runtime fornece as APIs Jakarta EE; não empacote uma implementação completa da plataforma dentro da aplicação. O status das entregas e as extensões que dependem do ambiente do produto estão registrados no roadmap técnico.
🌐 Conformidade com Padrões IETF
O framework v4 implementa conformidade com três RFCs, garantindo interoperabilidade com qualquer cliente HTTP que siga os padrões:
| RFC | Padrão | Módulo | Seção |
|---|---|---|---|
| RFC 9457 | Problem Details for HTTP APIs | rest | P18 |
| RFC 8288 | Web Linking (paginação) | crud | P19 |
| RFC 6585 / RFC 7231 | HTTP 429 + Retry-After | security | P20 |
Respostas de erro padronizadas (application/problem+json), headers Link para navegação de páginas, e respostas 429 Too Many Requests com Retry-After — tudo configurável e retrocompatível com o formato legado.
Visão Geral das Mudanças
P1 — Java 21 Records para DTOs
SortModel
O SortModel agora é um record Java 21 imutável com validação no construtor compacto.
// Antes (classe mutável com boilerplate)
SortModel sort = new SortModel(CrudSort.ASC, "name");
sort.getType(); // getter tradicional
sort.getField();
// Depois (record imutável)
SortModel sort = new SortModel(CrudSort.ASC, "name");
sort.type(); // accessor do record
sort.field();
Validações automáticas:
// NullPointerException — tipo nulo
new SortModel(null, "name");
// NullPointerException — campo nulo
new SortModel(CrudSort.ASC, null);
// IllegalArgumentException — campo vazio ou em branco
new SortModel(CrudSort.ASC, " ");
O equals() e hashCode() são gerados automaticamente pelo compilador com base nos componentes do record.
DemoiselleRestExceptionMessage
Mensagens de erro REST agora são records imutáveis com nomenclatura Java padrão:
// Criação de mensagem de erro
var msg = new DemoiselleRestExceptionMessage(
"AUTH_FAILED",
"Token expirado",
"https://docs.example.com/errors/auth-failed"
);
// Acessores do record
msg.error(); // "AUTH_FAILED"
msg.errorDescription(); // "Token expirado"
msg.errorLink(); // pode ser null
Serialização JSON funciona nativamente com Jackson e JSON-B:
{
"error": "AUTH_FAILED",
"errorDescription": "Token expirado",
"errorLink": "https://docs.example.com/errors/auth-failed"
}
Migração: Os campos foram renomeados de
error_description→errorDescriptioneerror_link→errorLinkpara seguir convenções Java.
ResultSet com List.copyOf()
O ResultSet agora usa cópias defensivas via List.copyOf():
ResultSet rs = new ResultSet();
// setContent(null) resulta em lista vazia, não NullPointerException
rs.setContent(null);
rs.getContent(); // → List.of() (lista vazia imutável)
// Cópia defensiva — modificações na lista original não afetam o ResultSet
List<String> original = new ArrayList<>(List.of("a", "b", "c"));
rs.setContent(original);
original.add("d"); // modifica a lista original
rs.getContent().size(); // → 3 (inalterado)
Records de Metadados no ConfigurationLoader
Dois records internos encapsulam metadados de configuração:
// Metadados de campo de configuração
record ConfigFieldMeta(
String key, // chave no arquivo de configuração
Field field, // campo refletido
boolean ignored, // @ConfigurationIgnore presente?
boolean suppressLog // @ConfigurationSuppressLogger presente?
) {}
// Metadados da fonte de configuração
record ConfigSourceMeta(
ConfigurationType type, // PROPERTIES, XML ou SYSTEM
String resource, // nome do arquivo
String prefix // prefixo das chaves
) {}
P2 — Sealed Classes para Filtros CRUD
FilterOp — Hierarquia Selada
A nova sealed interface FilterOp substitui a cascata de if-else no AbstractDAO por uma hierarquia type-safe com 7 variantes:
public sealed interface FilterOp {
String key();
record Equals(String key, String value) implements FilterOp {}
record Like(String key, String pattern) implements FilterOp {}
record IsNull(String key) implements FilterOp {}
record IsTrue(String key) implements FilterOp {}
record IsFalse(String key) implements FilterOp {}
record EnumFilter(String key, String value, int ordinal) implements FilterOp {}
record UUIDFilter(String key, UUID value) implements FilterOp {}
}
Cada variante valida seus dados no construtor compacto — key nunca é null, Like.pattern nunca é null, EnumFilter.ordinal é ≥ 0, etc.
Resolução Automática de Filtros
O AbstractDAO resolve automaticamente o tipo de filtro com base no valor recebido:
| Valor | FilterOp Resolvido |
|---|---|
null ou "null" |
IsNull |
*texto ou texto* |
Like |
"true" / "isTrue" |
IsTrue |
"false" / "isFalse" |
IsFalse |
| Campo enum | EnumFilter |
| Campo UUID | UUIDFilter |
| Qualquer outro | Equals |
Pattern Matching Exaustivo
O buildPredicate() usa switch com pattern matching garantido pelo compilador:
protected Predicate buildPredicate(FilterOp op, From<?, ?> from,
CriteriaBuilder cb, CriteriaQuery<?> cq) {
return switch (op) {
case FilterOp.IsNull(var key) -> cb.isNull(from.get(key));
case FilterOp.Like(var key, var pattern) -> buildLikePredicate(cb, cq, from, key, pattern);
case FilterOp.IsTrue(var key) -> cb.isTrue(from.get(key));
case FilterOp.IsFalse(var key) -> cb.isFalse(from.get(key));
case FilterOp.EnumFilter(var key, _, var o) -> cb.equal(from.get(key), o);
case FilterOp.UUIDFilter(var key, var uuid) -> cb.equal(from.get(key), uuid);
case FilterOp.Equals(var key, var val) -> cb.equal(from.get(key), val);
};
// Sem cláusula default — o compilador garante exaustividade!
}
Benefício: Se uma nova variante for adicionada à sealed interface, o código não compila até que o switch seja atualizado.
P3 — CDI 4.0 Lite Build-Compatible Extensions
MessageBundleBuildCompatibleExtension
A extensão CDI que descobre interfaces @MessageBundle foi migrada para a API Build-Compatible do CDI 4.0 Lite:
public class MessageBundleBuildCompatibleExtension
implements BuildCompatibleExtension {
@Discovery
public void discovery(ScannedClasses scan) {
// CDI 4.0 Lite descobre automaticamente
}
@Enhancement(types = Object.class,
withAnnotations = MessageBundle.class)
public void collectMessageBundles(ClassInfo classInfo) {
// Coleta interfaces @MessageBundle
}
@Synthesis
public void registerBeans(SyntheticComponents syn) {
// Registra beans sintéticos com proxy dinâmico
}
}
Como usar @MessageBundle (sem mudanças para o desenvolvedor):
@MessageBundle
public interface AppMessages {
@MessageTemplate("{welcome}")
String welcome();
@MessageTemplate("{greeting}")
String greeting(String name);
}
# AppMessages.properties
welcome=Bem-vindo ao sistema
greeting=Olá, %s!
@Inject
@MessageBundle
AppMessages messages;
messages.welcome(); // "Bem-vindo ao sistema"
messages.greeting("João"); // "Olá, João!"
Compatibilidade: A extensão portável original (
MessageBundleExtension) é mantida como fallback para containers que não suportam CDI Lite.
ConfigurationBuildCompatibleExtension
A descoberta de ConfigurationValueExtractor também foi migrada para Build-Compatible Extension:
public class ConfigurationBuildCompatibleExtension
implements BuildCompatibleExtension {
@Discovery
public void discovery(ScannedClasses scan) { }
@Enhancement(types = ConfigurationValueExtractor.class)
public void collectExtractors(ClassInfo classInfo) {
// Coleta implementações de ConfigurationValueExtractor
}
@Synthesis
public void registerExtractorRegistry(SyntheticComponents syn) {
// Registra bean sintético ApplicationScoped com o cache de extractors
}
}
Benefícios das Build-Compatible Extensions:
- Compatível com GraalVM native image (processamento em build-time)
- Startup mais rápido em ambientes CDI Lite
- API mais declarativa e menos propensa a erros
P4 — Coleções Imutáveis no Módulo de Segurança
DemoiselleUserImpl — Cópias Defensivas
Todos os getters de coleções no DemoiselleUserImpl agora retornam cópias defensivas verdadeiramente imutáveis:
@Inject
DemoiselleUser user;
// getRoles() retorna cópia independente via List.copyOf()
List<String> roles = user.getRoles();
// Modificações internas posteriores NÃO afetam esta cópia
// getPermissions() retorna deep copy
Map<String, List<String>> perms = user.getPermissions();
// Cada lista de valores também é copiada via List.copyOf()
// getParams() retorna cópia independente via Map.copyOf()
Map<String, String> params = user.getParams();
Diferença em relação à versão anterior:
// ANTES: Collections.unmodifiableList() — view mutável
// Se a lista interna mudasse, a view refletia a mudança
List<String> roles = user.getRoles(); // view
user.addRole("admin");
roles.contains("admin"); // true (!) — a view refletia a mudança
// DEPOIS: List.copyOf() — cópia defensiva
List<String> roles = user.getRoles(); // cópia
user.addRole("admin");
roles.contains("admin"); // false — a cópia é independente
Validação de null em addRole():
user.addRole(null); // → NullPointerException
P5 — Preparação para Virtual Threads
ConfigurationLoader — ReentrantReadWriteLock
O ConfigurationLoader substituiu synchronized por ReentrantReadWriteLock para compatibilidade com Virtual Threads (Project Loom):
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void load(final Object object, Class<?> baseClass) {
// Leitura rápida — múltiplas threads leem simultaneamente
rwLock.readLock().lock();
try {
if (isAlreadyLoaded(object)) return;
} finally {
rwLock.readLock().unlock();
}
// Escrita exclusiva — apenas uma thread por vez
rwLock.writeLock().lock();
try {
// Double-checked locking
if (!isAlreadyLoaded(object)) {
processConfiguration(object, baseClass);
}
} finally {
rwLock.writeLock().unlock();
}
}
Benefícios:
- Virtual threads não ficam pinned ao carrier thread durante espera
- Múltiplas leituras simultâneas para configurações já carregadas
- Escrita exclusiva apenas no primeiro carregamento
- Recuperação automática após exceções (retry habilitado)
DynamicManagerCache — Campos de Instância
O DynamicManagerCache eliminou campos static mutáveis em favor de campos de instância gerenciados pelo CDI:
@ApplicationScoped
public class DynamicManagerCache implements Serializable {
// ANTES: static Map (compartilhado globalmente, não GC-friendly)
// DEPOIS: campos de instância (ciclo de vida gerenciado pelo CDI)
private final Map<String, ConcurrentHashMap<String, Object>> scriptCache =
new ConcurrentHashMap<>();
private final Map<String, Object> engineList =
new ConcurrentHashMap<>();
public Map<String, ConcurrentHashMap<String, Object>> getScriptCache() {
return scriptCache;
}
public Map<String, Object> getEngineList() {
return engineList;
}
}
// No DynamicManager — injeção via CDI
@Inject
private DynamicManagerCache cache;
// Uso via getters em vez de acesso estático
cache.getEngineList().put(engineName, engine);
cache.getScriptCache().put(engineName, new ConcurrentHashMap<>());
P6 — Melhorias na Criteria API do JPA 3.1
mergeHalf() com CriteriaUpdate
O mergeHalf() foi refatorado de JPQL via StringBuilder para CriteriaUpdate type-safe:
@Override
public T mergeHalf(I id, T entity) {
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaUpdate<T> update = cb.createCriteriaUpdate(entityClass);
Root<T> root = update.from(entityClass);
boolean hasUpdates = false;
for (final Field field : getAllFields(entityClass)) {
// @ManyToOne sempre incluído
if (!field.isAnnotationPresent(ManyToOne.class)) {
final Column column = field.getAnnotation(Column.class);
// Sem @Column ou @Column(updatable=false) → pular
if (column == null || !column.updatable()) {
continue;
}
}
field.setAccessible(true);
final Object value = field.get(entity);
if (value != null) {
update.set(root.get(field.getName()), value);
hasUpdates = true;
}
}
if (hasUpdates) {
String idName = CrudUtilHelper.getMethodAnnotatedWithID(entityClass);
update.where(cb.equal(root.get(idName), id));
getEntityManager().createQuery(update).executeUpdate();
}
return entity;
}
Regras de inclusão de campos:
| Anotação | Valor | Incluído no UPDATE? |
|---|---|---|
@Column(updatable=true) |
não-null | ✅ |
@Column(updatable=true) |
null | ❌ |
@Column(updatable=false) |
qualquer | ❌ |
@ManyToOne |
não-null | ✅ |
@ManyToOne |
null | ❌ |
| Sem anotação | qualquer | ❌ |
Benefício: Sem risco de SQL injection via concatenação de strings. Erros de nome de campo detectados em tempo de compilação com metamodel.
EntityGraph no AbstractDAO.find()
Subclasses do AbstractDAO agora podem controlar a estratégia de fetch via EntityGraph:
public class PedidoDAO extends AbstractDAO<Pedido, Long> {
@Override
protected EntityGraph<Pedido> getEntityGraph() {
EntityGraph<Pedido> graph = getEntityManager()
.createEntityGraph(Pedido.class);
graph.addAttributeNodes("itens", "cliente");
return graph;
}
}
Quando getEntityGraph() retorna não-null, o hint jakarta.persistence.fetchgraph é aplicado automaticamente na TypedQuery:
TypedQuery<T> query = getEntityManager().createQuery(criteriaQuery);
EntityGraph<T> graph = getEntityGraph();
if (graph != null) {
query.setHint("jakarta.persistence.fetchgraph", graph);
}
Comportamento padrão:
getEntityGraph()retornanull— nenhum hint é aplicado e o comportamento existente de paginação, ordenação e filtragem é mantido.
P7 — Soft Delete com @SoftDeletable
Exclusão lógica declarativa via anotação. Em vez de DELETE físico, o framework executa um UPDATE marcando o registro como excluído.
Configuração na Entidade
@Entity
@SoftDeletable(field = "deletedAt")
public class Produto {
@Id @GeneratedValue
private Long id;
private String nome;
private LocalDateTime deletedAt; // campo de soft delete
// getters e setters
}
Tipos Suportados
// LocalDateTime (padrão)
@SoftDeletable(field = "deletedAt")
// Boolean
@SoftDeletable(field = "deleted", type = Boolean.class)
// Instant
@SoftDeletable(field = "deletedInstant", type = Instant.class)
Comportamento Automático no AbstractDAO
// remove() executa UPDATE em vez de DELETE
dao.remove(42L);
// SQL gerado: UPDATE produto SET deleted_at = '2026-04-01T10:30:00' WHERE id = 42
// find() exclui registros soft-deleted automaticamente
Result result = dao.find();
// SQL gerado: SELECT ... FROM produto WHERE deleted_at IS NULL
// find(id) retorna null para registros soft-deleted
Produto p = dao.find(42L); // → null (registro marcado como excluído)
// count() exclui registros soft-deleted
Long total = dao.count();
// SQL gerado: SELECT COUNT(*) FROM produto WHERE deleted_at IS NULL
// findIncludingDeleted() retorna TODOS os registros
Result todos = dao.findIncludingDeleted();
// SQL gerado: SELECT ... FROM produto (sem filtro de soft delete)
Filtro para Boolean
Quando o tipo é Boolean, o filtro é WHERE deleted = false OR deleted IS NULL:
@SoftDeletable(field = "deleted", type = Boolean.class)
public class Tarefa {
private Boolean deleted;
}
// remove() → UPDATE tarefa SET deleted = true WHERE id = ?
// find() → SELECT ... WHERE (deleted = false OR deleted IS NULL)
Validação na Inicialização
// Campo inexistente → DemoiselleCrudException na inicialização do DAO
@SoftDeletable(field = "campoInexistente")
public class Invalida { }
// Tipo não suportado → DemoiselleCrudException
@SoftDeletable(field = "nome", type = String.class)
public class TipoInvalido { }
P8 — Auditoria Automática
Preenchimento automático de campos de auditoria via JPA EntityListener, sem código manual em cada entidade.
Anotações Disponíveis
| Anotação | Evento | Valor |
|---|---|---|
@CreatedAt |
@PrePersist |
LocalDateTime.now() |
@CreatedBy |
@PrePersist |
DemoiselleUser.getIdentity() |
@UpdatedAt |
@PreUpdate |
LocalDateTime.now() |
@UpdatedBy |
@PreUpdate |
DemoiselleUser.getIdentity() |
Configuração na Entidade
@Entity
@EntityListeners(AuditEntityListener.class)
public class Pedido {
@Id @GeneratedValue
private Long id;
private String descricao;
@CreatedAt
private LocalDateTime criadoEm;
@CreatedBy
private String criadoPor;
@UpdatedAt
private LocalDateTime atualizadoEm;
@UpdatedBy
private String atualizadoPor;
// getters e setters
}
Comportamento
// Ao persistir: preenche apenas @CreatedAt e @CreatedBy
entityManager.persist(pedido);
// pedido.criadoEm → 2026-04-01T10:30:00
// pedido.criadoPor → "admin"
// pedido.atualizadoEm → null
// pedido.atualizadoPor → null
// Ao atualizar: preenche apenas @UpdatedAt e @UpdatedBy
entityManager.merge(pedido);
// pedido.criadoEm → (inalterado)
// pedido.criadoPor → (inalterado)
// pedido.atualizadoEm → 2026-04-01T11:00:00
// pedido.atualizadoPor → "editor"
Fallback sem Contexto de Usuário
Quando DemoiselleUser não está disponível (ex.: operações em batch sem requisição HTTP), o valor de @CreatedBy/@UpdatedBy é "system".
P9 — Specification Pattern
Composição declarativa de consultas JPA complexas sem escrever CriteriaQuery manualmente.
Interface Funcional
@FunctionalInterface
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
// Métodos default para composição
default Specification<T> and(Specification<T> other) { ... }
default Specification<T> or(Specification<T> other) { ... }
default Specification<T> not() { ... }
}
Criando Specifications
// Specification simples
Specification<Produto> precoMaior100 = (root, query, cb) ->
cb.greaterThan(root.get("preco"), 100.0);
Specification<Produto> categoriaEletronicos = (root, query, cb) ->
cb.equal(root.get("categoria"), "ELETRONICOS");
Specification<Produto> emEstoque = (root, query, cb) ->
cb.greaterThan(root.get("estoque"), 0);
Composição
// AND: produtos eletrônicos com preço > 100
Specification<Produto> filtro = categoriaEletronicos.and(precoMaior100);
// OR: eletrônicos OU preço > 100
Specification<Produto> filtroOu = categoriaEletronicos.or(precoMaior100);
// NOT: produtos que NÃO são eletrônicos
Specification<Produto> naoEletronicos = categoriaEletronicos.not();
// Composição complexa: eletrônicos em estoque OU preço > 100
Specification<Produto> complexo = categoriaEletronicos.and(emEstoque)
.or(precoMaior100);
Uso no AbstractDAO
// find(spec) combina Specification com filtros do DemoiselleRequestContext
Result result = dao.find(precoMaior100.and(emEstoque));
// Paginação é aplicada automaticamente quando habilitada
// Retorna PageResult com metadados quando paginação está ativa
// find(null) equivale a find() padrão
Result result = dao.find(null); // mesmo que dao.find()
P10 — Operações em Batch
Processamento eficiente de grandes volumes com flush/clear automático para evitar estouro de memória.
Configuração
# demoiselle.properties
demoiselle.crud.batch.size=100
Valor padrão: 50 registros por batch. O valor deve ser maior que zero;
configurações 0 ou negativas falham imediatamente com uma mensagem clara,
antes de iniciar a operação.
persistAll
List<Produto> produtos = List.of(
new Produto("Notebook", 3500.0),
new Produto("Mouse", 89.90),
new Produto("Teclado", 199.90)
);
// Persiste todos com flush/clear a cada N registros
List<Produto> persistidos = dao.persistAll(produtos);
// persistidos.size() == 3
Em caso de erro, a exceção inclui o índice da entidade que falhou:
try {
dao.persistAll(entidades);
} catch (DemoiselleCrudException e) {
// "Erro ao persistir entidade no índice 42"
e.getMessage();
e.getCause(); // PersistenceException original
}
removeAll
List<Long> ids = List.of(1L, 2L, 3L, 4L, 5L);
// Remove todos (respeita @SoftDeletable quando presente)
int removidos = dao.removeAll(ids);
// removidos == 5
updateAll
// Atualiza todos os produtos da categoria "ELETRONICOS" com desconto
Specification<Produto> eletronicos = (root, query, cb) ->
cb.equal(root.get("categoria"), "ELETRONICOS");
Map<String, Object> updates = Map.of(
"desconto", 0.15,
"emPromocao", true
);
int atualizados = dao.updateAll(eletronicos, updates);
// SQL: UPDATE produto SET desconto = 0.15, em_promocao = true
// WHERE categoria = 'ELETRONICOS'
P11 — PageResult Tipado
Record imutável com metadados completos de paginação, eliminando cálculos manuais no frontend.
Estrutura
public record PageResult<T>(
List<T> content, // conteúdo da página
long totalElements, // total de elementos em todas as páginas
int totalPages, // total de páginas
int currentPage, // página atual (0-indexed)
int pageSize, // tamanho da página
boolean hasNext, // existe próxima página?
boolean hasPrevious // existe página anterior?
) implements Result { }
Uso Automático
O AbstractDAO.find() retorna PageResult automaticamente quando a paginação está habilitada:
// Com paginação habilitada → PageResult
Result result = dao.find();
if (result instanceof PageResult<?> page) {
page.totalElements(); // 150
page.totalPages(); // 15
page.currentPage(); // 0
page.pageSize(); // 10
page.hasNext(); // true
page.hasPrevious(); // false
page.content(); // List<Produto> (10 itens)
}
// Sem paginação → ResultSet (comportamento anterior mantido)
Factory Method
// Criação manual com cálculos automáticos de metadados
PageResult<Produto> page = PageResult.of(
produtos, // conteúdo
150L, // totalElements
20, // offset
10 // pageSize
);
// totalPages = 15, currentPage = 2, hasNext = true, hasPrevious = true
Headers HTTP
Quando a resposta contém PageResult, o CrudFilter inclui headers automaticamente:
HTTP/1.1 200 OK
X-Total-Count: 150
X-Total-Pages: 15
X-Current-Page: 0
X-Page-Size: 10
X-Has-Next: true
X-Has-Previous: false
Content-Range: ...
Access-Control-Expose-Headers: ..., X-Total-Count, X-Total-Pages, ...
P12 — Operadores de Comparação no FilterOp
6 novos operadores de comparação via query string, sem necessidade de endpoints customizados.
Novos Operadores
| Prefixo | Operador | Exemplo Query String | FilterOp |
|---|---|---|---|
gt: |
Maior que | ?preco=gt:100 |
GreaterThan |
lt: |
Menor que | ?preco=lt:50 |
LessThan |
gte: |
Maior ou igual | ?idade=gte:18 |
GreaterThanOrEqual |
lte: |
Menor ou igual | ?estoque=lte:10 |
LessThanOrEqual |
between: |
Entre dois valores | ?preco=between:10,100 |
Between |
in: |
Lista de valores | ?status=in:ATIVO,PENDENTE |
In |
Exemplos de Uso via API REST
GET /api/produtos?preco=gt:100
GET /api/produtos?preco=between:50,200
GET /api/produtos?categoria=in:ELETRONICOS,INFORMATICA,GAMES
GET /api/produtos?estoque=lte:5
GET /api/produtos?dataCriacao=gte:2026-01-01
Precedência
Prefixos de operador têm precedência sobre filtros existentes. Um valor como gt:*texto* é interpretado como GreaterThan (não Like):
?campo=gt:true → GreaterThan (não IsTrue)
?campo=lt:*abc* → LessThan (não Like)
?campo=in:null → In com valor "null" (não IsNull)
Validação
# between: requer exatamente 2 valores
?preco=between:10 → IllegalArgumentException
?preco=between:10,20,30 → IllegalArgumentException
?preco=between:10,20 → OK (Between com lower=10, upper=20)
Hierarquia Completa do FilterOp (13 variantes)
public sealed interface FilterOp {
// Originais (7)
record Equals(String key, String value) implements FilterOp {}
record Like(String key, String pattern) implements FilterOp {}
record IsNull(String key) implements FilterOp {}
record IsTrue(String key) implements FilterOp {}
record IsFalse(String key) implements FilterOp {}
record EnumFilter(String key, String value, int ordinal) implements FilterOp {}
record UUIDFilter(String key, UUID value) implements FilterOp {}
// Novos operadores de comparação (6)
record GreaterThan(String key, String value) implements FilterOp {}
record LessThan(String key, String value) implements FilterOp {}
record GreaterThanOrEqual(String key, String value) implements FilterOp {}
record LessThanOrEqual(String key, String value) implements FilterOp {}
record Between(String key, String lower, String upper) implements FilterOp {}
record In(String key, List<String> values) implements FilterOp {}
}
P13 — Cache de Consultas com Eventos CDI
Cache automático de resultados de consultas com invalidação reativa via eventos CDI.
Habilitando Cache em um Método
public class ProdutoREST extends AbstractREST<Produto, Long> {
@GET
@Cacheable(ttl = 60) // cache por 60 segundos
public Result find() {
return super.find();
}
@GET
@Path("/destaque")
@Cacheable // TTL padrão: 300 segundos (5 minutos)
public Result findDestaques() {
// consulta customizada
}
}
Como Funciona
- Requisição GET chega ao endpoint
@Cacheable CacheInterceptorverifica se existe resultado em cache para a chaveentityClass:method:paramsHash- Cache hit → retorna resultado imediatamente (header
X-Cache: HIT) - Cache miss → executa o método, armazena resultado no cache (header
X-Cache: MISS)
Invalidação Automática
Operações de escrita no AbstractDAO disparam EntityModifiedEvent automaticamente:
// persist() → EntityModifiedEvent(Produto.class, PERSIST, entity)
// mergeFull() / mergeHalf() → EntityModifiedEvent(Produto.class, MERGE, entity)
// remove() → EntityModifiedEvent(Produto.class, REMOVE, id)
O CacheInvalidationListener observa esses eventos e invalida todas as entradas de cache da classe afetada:
// Ao persistir um Produto, TODAS as consultas cacheadas de Produto são invalidadas
// Consultas cacheadas de outras entidades (Pedido, Cliente) permanecem intactas
Em recursos que estendem AbstractREST<Produto, ...>, o CrudFilter resolve
Produto automaticamente. Em outros beans CDI, declare a entidade para que a
chave criada pelo interceptor use o mesmo namespace do evento:
@Cacheable(entityClass = Produto.class, ttl = 60)
public List<Produto> consultarDestaques() {
// ...
}
Resultados null não são armazenados. A anotação também pode ser aplicada à
classe; a configuração do método tem precedência.
Headers de Cache na Resposta
# Cache miss (primeira requisição)
HTTP/1.1 200 OK
X-Cache: MISS
# Cache hit (requisições subsequentes dentro do TTL)
HTTP/1.1 200 OK
X-Cache: HIT
Arquitetura do Cache
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ CrudFilter │────▶│ CacheInterceptor │────▶│ QueryCacheStore │
│ (JAX-RS) │ │ (CDI Interceptor)│ │ (ConcurrentMap) │
└─────────────┘ └──────────────────┘ └─────────────────┘
▲
│ invalidate
┌────────┴────────┐
│ CacheInvalidation│
│ Listener │
└────────┬────────┘
│ @Observes
┌────────┴────────┐
│EntityModifiedEvent│
└────────┬────────┘
│ fire()
┌────────┴────────┐
│ AbstractDAO │
│ persist/merge/ │
│ remove │
└─────────────────┘
P14 — Módulo de Observabilidade (demoiselle-observability)
Módulo transversal que fornece métricas, health checks e tracing distribuído para os demais módulos do framework. Todas as dependências externas (MicroProfile Metrics, MicroProfile Health, OpenTelemetry) são opcionais — quando ausentes, o módulo degrada graciosamente para implementações noop sem erro.
Dependência Maven
<dependency>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-observability</artifactId>
</dependency>
@Counted — Métricas via CDI Interceptor
Incrementa automaticamente um contador MicroProfile Metrics a cada invocação do método anotado.
import org.demoiselle.jee.observability.annotation.Counted;
@ApplicationScoped
public class TokenService {
@Counted("demoiselle.jwt.tokens.issued")
public String issueToken(DemoiselleUser user) {
// lógica de emissão de token
return jwt;
}
@Counted // nome automático: "TokenService.validateToken"
public DemoiselleUser validateToken(String jwt) {
// lógica de validação
return user;
}
}
Contadores pré-definidos do framework:
| Módulo | Operação | Contador |
|---|---|---|
| security-jwt | Token emitido | demoiselle.jwt.tokens.issued |
| security-jwt | Token validado | demoiselle.jwt.tokens.validated |
| rest | Rate limit rejeitado | demoiselle.rest.ratelimit.rejected |
| configuration | Configuração carregada | demoiselle.configuration.loaded |
| script | Script executado | demoiselle.script.executed |
Quando value() está vazio, o nome é gerado automaticamente no formato Classe.metodo.
@Traced — Tracing OpenTelemetry via CDI Interceptor
Cria spans OpenTelemetry automaticamente com atributos demoiselle.module e demoiselle.operation.
import org.demoiselle.jee.observability.annotation.Traced;
@ApplicationScoped
public class PedidoService {
@Traced(module = "pedido", operation = "criar")
public Pedido criarPedido(PedidoDTO dto) {
// lógica de criação
return pedido;
}
@Traced // module = "PedidoService", operation = "buscarPorId"
public Pedido buscarPorId(Long id) {
return pedido;
}
}
Quando module() ou operation() estão vazios, os valores são derivados do nome da classe e do método.
MetricsAdapter — Abstração para Degradação Graceful
// Interface
public interface MetricsAdapter {
void increment(String counterName);
long getCount(String counterName);
}
// Quando MicroProfile Metrics está no classpath:
// → MicroProfileMetricsAdapter (delega para MetricRegistry)
// Quando MicroProfile Metrics NÃO está no classpath:
// → NoopMetricsAdapter (increment() não faz nada, getCount() retorna 0)
TracingAdapter — Abstração para Degradação Graceful
public interface TracingAdapter {
<T> T executeInSpan(String module, String operation, SpanCallable<T> callable) throws Exception;
@FunctionalInterface
interface SpanCallable<T> {
T call() throws Exception;
}
}
// Quando OpenTelemetry está no classpath:
// → OpenTelemetryTracingAdapter (cria spans com atributos)
// Quando OpenTelemetry NÃO está no classpath:
// → NoopTracingAdapter (executa callable diretamente)
Health Checks — MicroProfile Health
O módulo registra automaticamente health checks quando MicroProfile Health está no classpath:
GET /health/live
{
"status": "UP",
"checks": [
{ "name": "demoiselle-cdi", "status": "UP" }
]
}
GET /health/ready
{
"status": "UP",
"checks": [
{ "name": "demoiselle-configuration", "status": "UP", "data": { "module": "demoiselle-configuration" } },
{ "name": "demoiselle-security-jwt-keys", "status": "UP", "data": { "type": "master", "publicKeyConfigured": true } }
]
}
Health checks registrados:
| Nome | Tipo | Verifica |
|---|---|---|
demoiselle-cdi |
Liveness | CDI container ativo |
demoiselle-configuration |
Readiness | Configuração carregada |
demoiselle-security-jwt-keys |
Readiness | Chaves JWT disponíveis (quando demoiselle-security-jwt no classpath) |
ObservabilityExtension — Detecção Automática de APIs
A CDI Extension detecta automaticamente quais APIs estão no classpath e registra os beans apropriados:
[INFO] MicroProfile Metrics detected — registering MicroProfileMetricsAdapter
[INFO] MicroProfile Health detected — registering HealthCheckProducer
[INFO] OpenTelemetry não disponível — tracing desativado
Nenhuma configuração manual é necessária. Basta adicionar a dependência da API desejada ao pom.xml:
<!-- Para métricas -->
<dependency>
<groupId>org.eclipse.microprofile.metrics</groupId>
<artifactId>microprofile-metrics-api</artifactId>
</dependency>
<!-- Para health checks -->
<dependency>
<groupId>org.eclipse.microprofile.health</groupId>
<artifactId>microprofile-health-api</artifactId>
</dependency>
<!-- Para tracing -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
P15 — Módulo OpenAPI (demoiselle-openapi)
Geração automática de documentação OpenAPI para endpoints do framework via MicroProfile OpenAPI.
Dependência Maven
<dependency>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-openapi</artifactId>
</dependency>
OpenAPIContributor — Interface para Contribuições Modulares
Cada módulo do framework pode contribuir definições OpenAPI parciais implementando esta interface:
import org.demoiselle.jee.openapi.OpenAPIContributor;
import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.models.OpenAPI;
@ApplicationScoped
public class MeuModuloOpenAPIContributor implements OpenAPIContributor {
@Override
public OpenAPI contribute() {
OpenAPI partial = OASFactory.createOpenAPI();
partial.paths(OASFactory.createPaths()
.addPathItem("/api/meu-recurso", OASFactory.createPathItem()
.GET(OASFactory.createOperation()
.summary("Lista recursos")
.operationId("listarRecursos"))));
return partial;
}
}
DemoiselleOASModelReader — Agregação Automática
O DemoiselleOASModelReader descobre todos os OpenAPIContributor via CDI e agrega suas contribuições:
- Paths de múltiplos contributors são mesclados no documento final
- Em caso de sobreposição, o primeiro contributor processado prevalece (first-wins)
- Se um contributor lançar exceção, o erro é logado e os demais continuam normalmente
Configuração de Ativação
# demoiselle.properties
# Desativar documentação OpenAPI automática (default: true)
demoiselle.openapi.enabled=false
Quando desativado, DemoiselleOASModelReader.buildModel() retorna um documento OpenAPI vazio.
P16 — CI/CD com GitHub Actions
O pipeline de CI/CD usa Java 21, o mesmo baseline exigido pelo build Maven.
Build Java 21 e supply chain
# .github/workflows/ci.yml
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: temurin
cache: maven
- run: mvn clean verify -B
- O Maven Enforcer exige Java 21+ e Maven 3.9+ já na fase
validate. - Plugins Maven usados pelo reactor têm versões explícitas.
- O CycloneDX gera
target/bom.jsonetarget/bom.xmlagregados na fasepackage. - O checkout não mantém credenciais e o workflow recebe somente
contents: read. - Relatórios JaCoCo são publicados como artefatos do build Java 21.
Relatórios de Cobertura em PRs
Em pull requests, um job separado gera relatório agregado de cobertura e posta um resumo no PR:
📊 JaCoCo Coverage Summary
| Module | Instruction | Branch | Line | Method |
|-------------------------|------------|--------|-------|--------|
| demoiselle-core | 85.2% | 72.1% | 83.4% | 90.1% |
| demoiselle-configuration| 78.5% | 65.3% | 76.2% | 85.7% |
| demoiselle-security | 91.0% | 80.4% | 89.1% | 93.2% |
Limiar de Cobertura Configurável
<!-- pom.xml raiz -->
<properties>
<jacoco.minimum.instruction.coverage>0.05</jacoco.minimum.instruction.coverage>
<jacoco.minimum.branch.coverage>0.01</jacoco.minimum.branch.coverage>
</properties>
A regra é avaliada por bundle Maven e bloqueia regressões abaixo dos gates iniciais de 5% de instruções e 1% de branches. Os limites devem crescer de forma gradual conforme a cobertura dos módulos aumenta.
P17 — Módulo de Testes de Integração (demoiselle-integration-tests)
Módulo dedicado a testes de integração que validam fluxos completos entre múltiplos módulos do framework.
Estrutura
- Executado na fase
verifydo Maven viamaven-failsafe-plugin - Testes unitários (
surefire) são desabilitados neste módulo - Módulos opcionais são tratados com
@EnabledIfdo JUnit 5
Fluxo Configuração → Segurança → REST
@EnabledIf("isSecurityJwtAvailable")
class ConfigSecurityRestIT {
@Test
void fullFlow_configLoadAndTokenIssueAndValidate() {
// 1. Carregar configuração de segurança
// 2. Emitir token JWT com claims
// 3. Validar token em novo TokenManager
// 4. Verificar que claims são preservados
}
@Test
void requiredRole_acceptsValidTokenWithCorrectRole() {
// Token com role "admin" → interceptor permite execução
}
@Test
void requiredRole_rejectsTokenWithWrongRole() {
// Token com role "viewer" → interceptor rejeita com 403
}
@Test
void expiredToken_isRejectedWithUnauthorized() {
// Token expirado → interceptor rejeita com 401
}
@Test
void corsFilter_appliesConfiguredHeaders() {
// Filtro CORS aplica headers configurados
}
}
Fluxo Configuração → Script
@EnabledIf("isScriptAvailable")
class ConfigScriptIT {
@Test
void loadEngineAndExecuteScriptWithParameters() {
// 1. Carregar engine Groovy
// 2. Cachear script com parâmetros
// 3. Executar e verificar resultado
}
@Test
void fullFlow_loadCacheUpdateAndReExecute() {
// Carregar → cachear → atualizar → re-executar
}
}
Tratamento de Módulos Opcionais
@EnabledIf("isSecurityJwtAvailable")
class SecurityJwtIntegrationIT {
static boolean isSecurityJwtAvailable() {
try {
Class.forName("org.demoiselle.jee.security.jwt.impl.TokenManagerImpl");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
Quando o módulo não está no classpath, os testes são ignorados automaticamente (não falham).
P18 — Conformidade RFC 9457 — Problem Details for HTTP APIs
🌐 Padrão IETF — RFC 9457 — Respostas de erro padronizadas com
application/problem+json
Respostas de erro padronizadas conforme RFC 9457 com media type application/problem+json.
Ativação
# demoiselle.properties
demoiselle.rest.errorFormat=rfc9457
Valor padrão: "legacy" — formato anterior mantido para retrocompatibilidade. Qualquer valor diferente de "rfc9457" é normalizado para "legacy".
Formato da Resposta
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "Recurso /api/items/42 não encontrado",
"instance": "/api/items/42"
}
Campos null são omitidos do JSON. Campos de extensão aparecem no nível raiz:
{
"type": "urn:demoiselle:validation-error",
"title": "Validation Failed",
"status": 412,
"instance": "/api/users",
"violations": [
{"field": "User.nome", "message": "não pode ser vazio"}
]
}
Mapeamento de Exceções
| Exceção | Status | Title |
|---|---|---|
ConstraintViolationException |
412 | Validation Failed |
SQLException |
500 | Database Error |
DemoiselleRestException |
statusCode da exceção | Primeira mensagem |
InvalidFormatException |
400 | Malformed Input |
ClientErrorException |
status do cliente | HTTP Error |
| Exceção genérica | 500 | Internal Server Error |
About:blank Automático (RFC 9457 §4.2)
Quando type é "about:blank" e title é null, o framework preenche title automaticamente com a frase-razão HTTP:
ProblemDetail pd = new ProblemDetail();
pd.setStatus(404);
pd.applyAboutBlankDefaults();
pd.getTitle(); // → "Not Found"
Mapeamento DemoiselleRestExceptionMessage → ProblemDetail
DemoiselleRestExceptionMessage ProblemDetail
├── error ─────────────────────────→ title
├── errorDescription ──────────────→ detail
└── errorLink (não vazio) ─────────→ type
Quando múltiplas mensagens estão presentes, todas são incluídas como extensão "messages".
Controle de Detalhes
Detalhes internos são omitidos por padrão para não expor mensagens de banco, stack traces ou implementação. Só habilite a opção durante diagnóstico em ambiente controlado:
# Opt-in temporário; não recomendado em produção
demoiselle.rest.showErrorDetails=true
P32 — Headers REST seguros
O módulo REST adiciona por padrão os headers abaixo, sem sobrescrever valores já definidos pela aplicação:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Permissions-Policy: camera=(), microphone=(), geolocation=()
HSTS e CSP não são enviados automaticamente porque dependem do uso efetivo de HTTPS e da política de conteúdo da aplicação. Para substituir o mapa ou desligar o filtro:
demoiselle.rest.securityHeadersEnabled=true
demoiselle.rest.securityHeaders.X-Content-Type-Options=nosniff
demoiselle.rest.securityHeaders.X-Frame-Options=SAMEORIGIN
Por redução de fingerprinting, Demoiselle-Version fica oculto por padrão. A
compatibilidade pode ser reativada explicitamente:
demoiselle.rest.exposeFrameworkVersion=true
P19 — RFC 8288 — Header Link para Paginação
🌐 Padrão IETF — RFC 8288 — Web Linking para navegação de páginas via header
Link
Headers Link padronizados conforme RFC 8288 nas respostas paginadas, complementando os headers customizados existentes.
Resposta Paginada
HTTP/1.1 200 OK
X-Total-Count: 150
X-Total-Pages: 15
X-Current-Page: 1
X-Page-Size: 10
X-Has-Next: true
X-Has-Previous: true
Link: </api/resource?range=0-9>; rel="first",
</api/resource?range=0-9>; rel="prev",
</api/resource?range=20-29>; rel="next",
</api/resource?range=140-149>; rel="last"
Access-Control-Expose-Headers: ..., Link
Regras de Geração
| Relação | Condição |
|---|---|
rel="first" |
Sempre presente quando totalPages > 1 |
rel="last" |
Sempre presente quando totalPages > 1 |
rel="next" |
Presente quando hasNext == true |
rel="prev" |
Presente quando hasPrevious == true |
Quando totalPages <= 1, nenhum header Link é emitido.
LinkHeaderBuilder
Classe utilitária para gerar headers Link a partir de PageResult:
String link = LinkHeaderBuilder.build("/api/resource?filter=active", pageResult);
// Preserva query parameters existentes:
// </api/resource?filter=active&range=10-19>; rel="next", ...
Retrocompatibilidade
Todos os headers customizados (X-Total-Count, X-Total-Pages, X-Current-Page, X-Page-Size, X-Has-Next, X-Has-Previous) continuam presentes. O header Link é adicionado em complemento.
P33 — Limites de requisição CRUD
O CRUD aplica tetos globais antes de construir consultas, reduzindo risco de consumo excessivo de memória e banco. Os defaults são:
demoiselle.crud.pagination.maxPagination=100
demoiselle.crud.limits.maxFilters=20
demoiselle.crud.limits.maxFilterValues=100
demoiselle.crud.limits.maxFilterValueLength=1024
demoiselle.crud.limits.maxSortFields=10
O tamanho efetivo da página é o menor valor entre o default global ou
@Search.quantityPerPage e maxPagination. Requisições que excedem os demais
limites são rejeitadas como entrada inválida antes da validação dos campos.
Valores nulos ou não positivos nas propriedades de limits retornam aos
defaults seguros; configure valores positivos para ampliar ou reduzir os tetos.
P20 — RFC 6585/7231 — Rate Limiting com HTTP 429
🌐 Padrão IETF — RFC 6585 / RFC 7231 — Respostas 429 Too Many Requests com header
Retry-After
O RateLimitInterceptor agora retorna respostas HTTP 429 com header Retry-After conforme as RFCs 6585 e 7231, em vez de lançar exceções genéricas.
Resposta 429 (formato RFC 9457)
HTTP/1.1 429 Too Many Requests
Retry-After: 5
Content-Type: application/problem+json
{
"type": "urn:demoiselle:rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Retry after 5 seconds"
}
Resposta 429 (formato legado)
HTTP/1.1 429 Too Many Requests
Retry-After: 5
Content-Type: application/json
{
"error": "Rate limit exceeded. Retry after 5 seconds"
}
O formato é selecionado automaticamente com base na configuração errorFormat. Quando o módulo REST não está presente, o formato legado é usado como fallback.
Retry-After
O valor do header Retry-After é calculado pelo SlidingWindowCounter com base no registro mais antigo na janela deslizante. O valor é sempre >= 1 e <= windowSeconds.
P21 — JWT Refresh Tokens e Blacklist
Refresh Token
O RefreshTokenManager gera pares access token / refresh token:
@Inject
RefreshTokenManager refreshTokenManager;
// Gerar par de tokens
TokenPair pair = refreshTokenManager.issueTokenPair(user);
pair.accessToken(); // JWT de curta duração
pair.refreshToken(); // JWT de longa duração (apenas sub, jti, exp)
// Renovar access token
String newAccessToken = refreshTokenManager.refresh(pair.refreshToken());
# demoiselle.properties
demoiselle.security.jwt.refreshTokenTtlMilliseconds=86400000 # 24h (padrão)
Token Blacklist
Revogação de tokens antes da expiração via JTI:
// removeUser() automaticamente adiciona o JTI à blacklist
tokenManager.removeUser(user);
// Tokens revogados são rejeitados na validação
tokenManager.getUser(); // → DemoiselleSecurityException (401)
A blacklist é @ApplicationScoped e remove automaticamente entradas expiradas.
P22 — Identificação de Chave e Algoritmos JWT
Identificador da Chave Ativa
# demoiselle.properties
# A chave pública/privada continua sendo configurada pelas propriedades do módulo.
demoiselle.security.jwt.activeKeyId=key-2024
demoiselle.security.jwt.privateKey=...
demoiselle.security.jwt.publicKey=...
Novos tokens recebem activeKeyId no header kid. Na validação, um kid
explícito e desconhecido é rejeitado com HTTP 401; a chave fallback só pode ser
usada quando o token omite kid (compatibilidade) ou informa o identificador
ativo. Um provedor configurável de múltiplas chaves é uma evolução planejada,
não devendo ser simulado com propriedades aninhadas não suportadas.
Algoritmos Permitidos
# Opcional; RS256 é o default seguro
demoiselle.security.jwt.allowedAlgorithms=RS256,RS384,RS512
A lista nunca fica aberta: sem configuração explícita, somente RS256 é
permitido. Tokens sem algoritmo ou com algoritmo fora da allowlist são
rejeitados.
Clock Skew
demoiselle.security.jwt.clockSkewSeconds=60 # tolerância em segundos (padrão)
P34 — Perfis avançados JWT
A validação possui três perfis. O default compat preserva tokens existentes;
recommended e strict são opt-in e falham se issuer ou audience não
estiverem configurados ou forem omitidos da chamada explícita de validação.
| Perfil | Requisitos adicionais |
|---|---|
compat |
Mantém exp, assinatura, allowlist de algoritmo e seleção segura de kid |
recommended |
iss, aud, header typ (JWT), iat, jti e idade máxima |
strict |
Tudo de recommended mais sub |
demoiselle.security.jwt.validationProfile=recommended
demoiselle.security.jwt.issuer=https://auth.example.gov.br
demoiselle.security.jwt.audience=api-demoiselle
demoiselle.security.jwt.expectedType=JWT
demoiselle.security.jwt.maxTokenAgeSeconds=900
Quando maxTokenAgeSeconds não é informado, os perfis avançados usam
timetoLiveMilliseconds convertido para segundos. Overrides booleanos
requireIssuer, requireAudience, requireIssuedAt, requireJwtId e
requireSubject podem reforçar compat, mas false nunca enfraquece os pisos
de recommended ou strict. Um nome de perfil desconhecido é rejeitado para
evitar downgrade por erro de digitação.
Tokens emitidos pelo framework incluem typ quando o perfil o exige e usam a
identidade como sub. A tolerância de clockSkewSeconds continua sendo aplicada
à expiração e à idade baseada em iat.
P23 — Claims Customizados via ClaimsEnricher
Interface CDI para injetar claims customizados durante criação/validação de tokens:
@ApplicationScoped
public class TenantClaimsEnricher implements ClaimsEnricher {
@Override
public void enrich(JwtClaims claims, DemoiselleUser user) {
claims.setClaim("tenant_id", resolveTenantId(user));
}
@Override
public void extract(JwtClaims claims, DemoiselleUser user) {
String tenantId = claims.getStringClaimValue("tenant_id");
user.getParams().put("tenant_id", tenantId);
}
}
Múltiplas implementações são suportadas simultaneamente. Quando nenhuma está registrada, o comportamento é idêntico ao anterior.
P24 — Eventos de Segurança via CDI
Records Java para eventos de autenticação e autorização:
// Observar login/logout/falha
public void onAuth(@Observes AuthenticationEvent event) {
log.info("{} {} at {}", event.action(), event.user().getIdentity(), event.timestamp());
}
// Observar negação de autorização
public void onDenied(@Observes AuthorizationEvent event) {
log.warn("Acesso negado: {} tentou acessar {}/{}",
event.user().getIdentity(), event.resource(), event.operation());
}
| Evento | Ação | Disparado por |
|---|---|---|
AuthenticationEvent |
LOGIN |
SecurityContext.setUser() |
AuthenticationEvent |
LOGOUT |
SecurityContext.removeUser() |
AuthenticationEvent |
FAILURE |
AuthenticatedInterceptor |
AuthorizationEvent |
— | RequiredRoleInterceptor, RequiredPermissionInterceptor |
P25 — @RequiredAnyRole e @RequiredAllPermissions
Anotações com lógica AND/OR explícita para autorização:
// OR: usuário precisa de pelo menos UMA das roles
@RequiredAnyRole({"admin", "manager", "supervisor"})
public void aprovarPedido(Long id) { ... }
// AND: usuário precisa de TODAS as permissões
@RequiredAllPermissions({
@Permission(resource = "pedido", operation = "aprovar"),
@Permission(resource = "financeiro", operation = "consultar")
})
public void aprovarPedidoFinanceiro(Long id) { ... }
Usuário não autenticado → 401. Usuário sem roles/permissões → 403.
P26 — Configuração CORS via Properties
# demoiselle.properties
demoiselle.security.cors.allowedOrigins=https://app.example.com,https://admin.example.com
demoiselle.security.cors.allowedMethods=GET,POST,PUT,DELETE,OPTIONS
demoiselle.security.cors.allowedHeaders=Authorization,Content-Type,X-Requested-With
demoiselle.security.cors.maxAge=3600
Quando allowedOrigins contém *, qualquer origem é permitida. Sem configuração, o comportamento anterior é mantido.
P27 — @CacheControl com Atributos Tipados
// Antes (string bruta, propenso a erros de digitação)
@CacheControl("max-age=3600, no-store")
// Depois (atributos tipados com validação em compilação)
@CacheControl(maxAge = 3600, noStore = true)
// Combinação de atributos
@CacheControl(maxAge = 86400, mustRevalidate = true, isPrivate = true)
// Modo legado mantido para retrocompatibilidade
@CacheControl("no-cache, no-store, must-revalidate")
Quando value() está vazio, os atributos tipados são usados. Quando value() está preenchido, ele prevalece (modo legado).
P28 — Perfis de Configuração e @DefaultValue
Perfis
# Via propriedade de sistema
java -Ddemoiselle.profile=dev -jar app.jar
# Via variável de ambiente
export DEMOISELLE_PROFILE=prod
O framework resolve automaticamente demoiselle-dev.properties (ou demoiselle-prod.properties) com fallback para demoiselle.properties:
demoiselle-dev.properties → chaves específicas do perfil
demoiselle.properties → fallback para chaves ausentes
@DefaultValue
@Configuration(prefix = "app")
public class AppConfig {
@DefaultValue("8080")
private int port;
@DefaultValue("localhost")
private String host;
@DefaultValue("INFO")
private LogLevel logLevel; // enums suportados
}
Quando a chave não existe no arquivo de configuração, o valor da anotação é usado. Quando a chave existe, o valor do arquivo prevalece.
P29 — Core API: Result Tipado e Métodos de Conveniência
Result<T>
// Antes: cast necessário
List<?> content = result.getContent();
List<Produto> produtos = (List<Produto>) content;
// Depois: type-safe
Result<Produto> result = dao.find();
List<Produto> produtos = result.getContent(); // sem cast
Métodos Default no Crud
// Novos métodos com implementação default
boolean exists = dao.exists(42L); // delega para find(id) != null
long total = dao.count(); // delega para find().getContent().size()
List<Produto> all = dao.findAll(); // delega para find().getContent()
DemoiselleException com Error Code
throw new DemoiselleRestException("Token expirado", "DEMOISELLE-SEC-001");
// toString() inclui o código
// "DemoiselleRestException[DEMOISELLE-SEC-001]: Token expirado"
Formato: DEMOISELLE-<MÓDULO>-<NÚMERO> (ex: DEMOISELLE-SEC-001, DEMOISELLE-CFG-002).
P30 — Security Token: Correções e Modernização
Correção do validate(issuer, audience)
O método agora verifica efetivamente os parâmetros issuer e audience do usuário armazenado (antes eram ignorados).
TTL e Eviction
// Entradas expiradas são removidas automaticamente
// TTL padrão: 3600 segundos (1 hora)
// Limpeza executada a cada chamada de setUser()
TokenEntry Record
// Antes: DemoiselleUser armazenado diretamente
// Depois: record com timestamp de expiração
record TokenEntry(DemoiselleUser user, Instant expiresAt) {}
Resolução de Ambiguidade CDI no Token
O TokenImpl foi marcado com @Vetoed para evitar ambiguidade CDI (WELD-001409) no WildFly/Weld. O Token request-scoped é produzido exclusivamente pelo producer em SecurityFilter:
// SecurityFilter.java — fonte única do Token no container CDI
@Produces
@RequestScoped
public Token produceToken() {
if (currentToken != null) {
return currentToken; // TokenImpl mutável com key/type do header
}
return new TokenImpl(); // TokenImpl vazio (mutável)
}
O TokenManagerImpl do JWT continua usando token.setKey() e token.setType() normalmente, pois o producer entrega TokenImpl (mutável).
O TokenRecord (record imutável) permanece disponível para uso externo onde imutabilidade é desejada, mas não é usado como bean CDI.
Correções de Ambiguidade CDI no Observability
Os adapters do módulo demoiselle-observability foram marcados com @Vetoed para evitar duplicidade com os beans sintéticos registrados pela ObservabilityExtension:
| Classe | Correção |
|---|---|
NoopTracingAdapter |
@Vetoed — registrado via extensão |
OpenTelemetryTracingAdapter |
@Vetoed — registrado via extensão |
NoopMetricsAdapter |
@Vetoed — registrado via extensão |
MicroProfileMetricsAdapter |
@Vetoed — registrado via extensão |
HealthCheckProducer |
@Vetoed — registrado via extensão |
P31 — Módulo MCP (demoiselle-mcp)
Módulo para construção de servidores MCP (Model Context Protocol) com o Demoiselle Framework. Permite expor beans CDI como ferramentas, recursos e prompts MCP via anotações declarativas, sem escrever código de infraestrutura de protocolo.
O módulo utiliza JSON-RPC 2.0 como formato de mensagens e suporta dois transportes: SSE (Server-Sent Events) via JAX-RS e stdio para comunicação local entre processos.
Dependências
Apenas demoiselle-core e demoiselle-configuration são obrigatórias. Integrações com demoiselle-rest (ProblemDetail), demoiselle-security (JWT, @RateLimit), demoiselle-crud (PageResult, Specification) e demoiselle-observability (@Counted) são opcionais — o módulo degrada graciosamente quando ausentes.
<dependency>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-mcp</artifactId>
<version>4.1.0-SNAPSHOT</version>
</dependency>
Anotações
@McpTool — Expor métodos como ferramentas MCP
@ApplicationScoped
public class CalculatorService {
@McpTool(description = "Soma dois números inteiros")
public int add(@McpParam(name = "a", description = "Primeiro operando") int a,
@McpParam(name = "b", description = "Segundo operando") int b) {
return a + b;
}
}
O inputSchema (JSON Schema) é gerado automaticamente a partir dos parâmetros do método. Use @McpParam para personalizar nome, descrição e obrigatoriedade:
@McpTool(name = "buscar-produtos", description = "Busca produtos por filtro")
public List<Produto> buscar(
@McpParam(name = "categoria", description = "Categoria do produto") String categoria,
@McpParam(name = "precoMax", description = "Preço máximo", required = false) Double precoMax) {
// ...
}
@McpResource — Expor dados como recursos MCP
@McpResource(uri = "config://app", name = "App Config",
description = "Configuração da aplicação", mimeType = "application/json")
public String readConfig() {
return "{ \"version\": \"1.0\" }";
}
@McpPrompt — Expor templates de prompt MCP
@McpPrompt(name = "code-review", description = "Revisa código fonte")
public List<Map<String, Object>> codeReview(
@McpParam(name = "code", description = "Código a revisar") String code) {
return List.of(Map.of(
"role", "user",
"content", Map.of("type", "text", "text", "Revise este código: " + code)
));
}
Mapeamento de Tipos Java → JSON Schema
| Tipo Java | JSON Schema |
|---|---|
String |
{"type": "string"} |
int/Integer/long/Long |
{"type": "integer"} |
double/Double/float/Float |
{"type": "number"} |
boolean/Boolean |
{"type": "boolean"} |
List<T>/T[] |
{"type": "array", "items": {...}} |
| POJO | {"type": "object", "properties": {...}} |
Configuração via demoiselle.properties
# Nome e versão do servidor MCP
demoiselle.mcp.server.name=minha-aplicacao
demoiselle.mcp.server.version=2.0.0
# Transporte: "sse" (padrão) ou "stdio"
demoiselle.mcp.transport=sse
# Autenticação JWT no transporte SSE
demoiselle.mcp.security.enabled=false
# Desabilitar ferramentas específicas sem remover código
demoiselle.mcp.tools.disabled=ferramenta-debug, ferramenta-teste
Transporte SSE
O transporte SSE expõe dois endpoints JAX-RS:
GET /mcp/sse— estabelece conexão SSE, retorna eventoendpointcom a URI do POSTPOST /mcp/messages?sessionId=...— recebe mensagens JSON-RPC, retorna respostas via SSE
Cliente MCP Servidor
│ │
│── GET /mcp/sse ──────────────────>│
│<── event: endpoint ───────────────│ (URI do POST)
│ │
│── POST /mcp/messages ────────────>│ (initialize)
│<── event: message ────────────────│ (capabilities)
│ │
│── POST /mcp/messages ────────────>│ (tools/call)
│<── event: message ────────────────│ (resultado)
Transporte stdio
Para comunicação local entre processos (ex.: integração com IDEs):
java -cp app.jar org.demoiselle.jee.mcp.transport.McpStdioTransport
Lê JSON-RPC de stdin (uma mensagem por linha), escreve respostas em stdout. Logs vão para stderr.
Segurança JWT (opcional)
Quando demoiselle.mcp.security.enabled=true e demoiselle-security está no classpath:
- Conexões SSE exigem token JWT válido no header
Authorization: Bearer <token> - Token ausente/inválido → HTTP 401
- Token expirado → HTTP 401 com detail “Token expired”
- O transporte stdio ignora configurações de segurança (contexto local confiável)
Integrações Opcionais
| Módulo | Integração | Comportamento sem o módulo |
|---|---|---|
demoiselle-rest |
Erros formatados como ProblemDetail (RFC 9457) | Erros como texto simples |
demoiselle-security |
Autenticação JWT, @RateLimit por ferramenta |
Sem autenticação, sem rate limit |
demoiselle-crud |
PageResult com metadados de paginação, SpecificationBuilder |
PageResult tratado como POJO |
demoiselle-observability |
Contadores @Counted por ferramenta |
Sem métricas |
Protocolo JSON-RPC 2.0
O handler central valida e roteia mensagens conforme a especificação MCP:
| Método | Descrição |
|---|---|
initialize |
Negociação de capacidades (tools, resources, prompts) |
notifications/initialized |
Marca sessão como ativa |
tools/list |
Lista ferramentas registradas com inputSchema |
tools/call |
Invoca ferramenta por nome com argumentos JSON |
resources/list |
Lista recursos registrados |
resources/read |
Lê conteúdo de um recurso por URI |
prompts/list |
Lista prompts registrados com argumentos |
prompts/get |
Executa prompt por nome com argumentos |
Códigos de erro JSON-RPC:
| Código | Significado |
|---|---|
-32600 |
Requisição inválida (jsonrpc ≠ “2.0” ou sessão não inicializada) |
-32601 |
Método desconhecido |
-32602 |
Ferramenta/recurso/prompt inexistente |
-32603 |
Erro interno do servidor |
-32700 |
JSON malformado |
P35 — Segurança, SPIs e contratos operacionais
Este ciclo fecha os itens de hardening e operação identificados na auditoria. O guia de migração 4.1 contém exemplos completos e impactos de compatibilidade.
Segurança atômica e proxies
SecurityStore abstrai contadores atômicos com TTL. O backend local é limitado
e usado por rate limit, brute force e proteção de replay. A identidade
autenticada é a chave preferencial; X-Forwarded-For só é lido quando o peer
imediato está em demoiselle.security.trustedProxies.
MCP
Quando a segurança está ativa, GET e POST exigem Bearer JWT e a sessão permanece vinculada ao mesmo principal. Ausência do módulo JWT falha fechado. TTL absoluto, idle timeout, teto de sessões e limite de tools são configuráveis:
demoiselle.mcp.security.enabled=true
demoiselle.mcp.sessionTtlMillis=1800000
demoiselle.mcp.sessionIdleMillis=300000
demoiselle.mcp.maxSessions=10000
demoiselle.mcp.toolRateLimitRequests=60
demoiselle.mcp.toolRateLimitWindowSeconds=60
HashCash e rotação JWT
O módulo demoiselle-security-hashcash está no BOM/reactor e exige segredo de
pelo menos 32 bytes. Seus desafios são assinados, vinculados a recurso e
protegidos contra replay. JWT oferece JwtKeyProvider; o provider local suporta
múltiplos kid, refresh e janela de rotação, rejeitando identificadores
desconhecidos sem fallback.
Cache e segredos
CacheBackend permite substituir o cache CRUD; o backend local é LRU limitado,
com TTL e métricas. SecretProvider é descoberto via ServiceLoader e inclui os
schemes env, sys e file:
app.password=${secret:env:APP_PASSWORD}
app.token=${secret:sys:app.token}
app.key=${secret:file:/run/secrets/app-key}
Falhas não são cacheadas e nunca retornam segredo default.
Idempotência, outbox, cursor e lifecycle
@Idempotentoferece aquisição atômica, conflito durante processamento e replay byte-for-byte/JSON de respostas 2xx.OutboxServicepublica emAFTER_SUCCESS; stores e publishers externos implementamOutboxStore/OutboxPublisher.CursorCodecassina cursores HMAC-SHA256 eKeysetPaginationconstrói a comparação lexicográfica ASC/DESC, BEFORE/AFTER. Inclua chave única como último sort.@ApiLifecycleadicionaDeprecation,Sunsete links sem sobrescrever headers da aplicação.
Result e Script
Result<T> é leitura; mutação pertence a MutableResult<T>. ResultSet
implementa o contrato mutável e PageResult permanece imutável. No módulo
Script, um ReadWriteLock por engine protege todo o ciclo de vida e o cache
compartilhado.
CI e inventário
O reactor contém 16 módulos. Inventário/testes e matriz de runtimes são gerados
por scripts padrão-library-only; module_inventory.py --check bloqueia drift.
SBOM CycloneDX 1.6, checksums/proveniência, actions pinadas e smoke tests fazem
parte da CI. O gate OpenAPI entra em ação quando o projeto possui uma spec
versionada ou gerada.
Extensões de produção para produtos
As implementações locais mantêm desenvolvimento e testes autocontidos. Produtos com requisitos de cluster, integração e release podem usar seis extensões:
- estado distribuído para segurança, cache e idempotência consistentes;
- chaves e segredos gerenciados com rotação e auditoria centralizadas;
- outbox persistente e broker para publicação confiável após commit;
- matriz executada para certificar o runtime realmente usado pelo produto;
- baseline OpenAPI para impedir quebras de contrato antes do deploy; e
- builds herméticos para tornar reprodutibilidade um gate de release.
Consulte Extensões de produção para produtos Demoiselle para critérios dos adapters, benefícios, riscos de fallback, testes de falha e uma sequência recomendada de adoção.
Testes Baseados em Propriedades (jqwik)
O framework usa jqwik em uma suíte ampla de property-based tests que validam propriedades universais de corretude. A quantidade evolui com a suíte; os relatórios Surefire/Failsafe do build são a fonte de verdade:
| # | Propriedade | Módulo |
|---|---|---|
| 1 | Rejeição de campos blank no SortModel | crud |
| 2 | Igualdade estrutural de Records | crud |
| 3 | Round-trip JSON do DemoiselleRestExceptionMessage | rest |
| 4 | Independência de cópia defensiva no ResultSet | crud |
| 5 | Cópias defensivas no DemoiselleUserImpl | security |
| 6 | Null-safety do FilterOp.key() (13 variantes) | crud |
| 7 | resolveFilterOp() sempre retorna FilterOp válido | crud |
| 8 | Wildcard resolve para Like | crud |
| 9 | Exclusão correta de campos no CriteriaUpdate | crud |
| 10 | Soft delete marca registro em vez de remover | crud |
| 11 | Consultas excluem registros soft-deleted | crud |
| 12 | findIncludingDeleted retorna todos os registros | crud |
| 13 | Persist preenche apenas campos de criação | crud |
| 14 | Merge preenche apenas campos de atualização | crud |
| 15 | Specification.and() retorna interseção | crud |
| 16 | Specification.or() retorna união | crud |
| 17 | Specification.not() retorna complemento | crud |
| 18 | find(Specification) combina spec com filtros DRC | crud |
| 19 | find(Specification) aplica paginação | crud |
| 20 | persistAll retorna lista de mesmo tamanho | crud |
| 21 | removeAll retorna contagem correta | crud |
| 22 | updateAll aplica updates e Specification | crud |
| 23 | PageResult tipo correto baseado em paginação | crud |
| 24 | PageResult calcula metadados corretamente | crud |
| 25 | PageResult cópia defensiva | crud |
| 26 | resolveFilterOp resolve prefixos de operador | crud |
| 27 | Prefixos de operador têm precedência | crud |
| 28 | Cache round-trip (hit/miss com TTL) | crud |
| 29 | Operações de escrita disparam EntityModifiedEvent | crud |
| 30 | Invalidação de cache por entityClass | crud |
| 31 | Contagem monotônica do @Counted | observability |
| 32 | Segurança dos adapters noop | observability |
| 33 | Span do @Traced contém atributos corretos | observability |
| 34 | Agregação de OpenAPIContributors preserva paths | openapi |
| 35 | Tolerância a falhas na agregação OpenAPI | openapi |
| 36 | Interceptor de segurança aceita tokens válidos e rejeita inválidos | integration-tests |
| 37 | Round-trip de configuração | integration-tests |
| 38 | Round-trip de claims JWT | integration-tests |
| 39 | Invariante do rate limiter | integration-tests |
| 40 | Round-trip serialização ProblemDetail (RFC 9457) | rest |
| 41 | Validação de chaves de extensão do ProblemDetail | rest |
| 42 | About:blank preenche title com frase-razão HTTP | rest |
| 43 | Round-trip mapeamento ExceptionMessage → ProblemDetail | rest |
| 44 | Múltiplas mensagens incluídas como extensão | rest |
| 45 | Invariante status-consistente nas respostas RFC 9457 | rest |
| 46 | Media type application/problem+json no formato RFC 9457 | rest |
| 47 | Omissão de detail quando showErrorDetails é false | rest |
| 48 | Instance preenchido com URI da requisição | rest |
| 49 | Normalização de errorFormat desconhecido para legacy | rest |
| 50 | Relações do header Link metamórficas com PageResult | crud |
| 51 | Invariante offset-limit consistente nas URIs do Link | crud |
| 52 | Preservação de query parameters no LinkHeaderBuilder | crud |
| 53 | Headers customizados consistentes com PageResult e Link | crud |
| 54 | Rate limiter rejeita (N+1)-ésima requisição com Retry-After | security |
| 55 | Resposta 429 contém Retry-After consistente | security |
| 56 | Invariante de schema válido para @McpTool | mcp |
| 57 | Mapeamento correto de tipos Java → JSON Schema | mcp |
| 58 | Metadados de parâmetros (required e description) | mcp |
| 59 | Rejeição de nomes/URIs duplicados nos registros | mcp |
| 60 | Consistência entre registros e respostas de listagem | mcp |
| 61 | Lookup por nome/URI inexistente retorna -32602 | mcp |
| 62 | isError reflete exceção do método CDI | mcp |
| 63 | Round-trip de serialização de argumentos e resultados | mcp |
| 64 | Round-trip do transporte stdio | mcp |
| 65 | Capabilities refletem estado dos registros | mcp |
| 66 | Requisições pré-handshake rejeitadas com -32600 | mcp |
| 67 | Validação JSON-RPC e consistência de id | mcp |
| 68 | Método desconhecido retorna -32601 | mcp |
| 69 | Notificações não produzem resposta | mcp |
| 70 | Formatação de erros condicional ao classpath | mcp |
| 71 | Mapeamento de tipo de exceção para status ProblemDetail | mcp |
| 72 | Serialização de PageResult com metadados de paginação | mcp |
| 73 | Resposta de rate limit | mcp |
| 74 | Contadores de métricas auto-registrados | mcp |
| 75 | Filtragem de ferramentas desabilitadas | mcp |
| 76 | Round-trip de serialização JsonRpcMessage | mcp |
| 77 | Campos null omitidos na serialização | mcp |
| 78 | Exclusividade mútua entre error e result | mcp |
| 79 | SpecificationBuilder suporta todos os operadores | mcp |
| 80 | PageResult como objeto simples sem demoiselle-crud | mcp |
| 81 | Geração de argumentos de prompt a partir de parâmetros | mcp |
Exemplo — Property test de cópia defensiva:
@Property(tries = 200)
void modifyingOriginalListDoesNotAffectGetContent(
@ForAll List<String> elements) {
List<String> mutableList = new ArrayList<>(elements);
ResultSet rs = new ResultSet();
rs.setContent(mutableList);
List<?> snapshot = rs.getContent();
mutableList.add("EXTRA");
mutableList.clear();
assertEquals(elements.size(), snapshot.size(),
"Modificações na lista original não devem afetar getContent()");
}
Guia de Migração — Demoiselle v3 → v4
Pré-requisitos
| Componente | Versão Mínima |
|---|---|
| Java | 17+ (LTS) |
| Maven | 3.9+ |
| Jakarta EE | 10 |
| CDI | 4.0 |
| JPA | 3.1 |
| JAX-RS | 3.1 |
| Weld (referência CDI) | 5.x |
| Servidor de Aplicação | WildFly 27+, Quarkus ou Open Liberty |
Parte I — Migração do Build
1. Atualizar o POM
<parent>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
</parent>
Configurar Java 21 no compiler plugin:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
2. Namespace Jakarta
Todas as importações javax.* devem ser substituídas por jakarta.*:
// Antes
import javax.inject.Inject;
import javax.enterprise.context.ApplicationScoped;
import javax.persistence.Entity;
import javax.ws.rs.GET;
// Depois
import jakarta.inject.Inject;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.Entity;
import jakarta.ws.rs.GET;
| Antigo | Novo |
|---|---|
javax.enterprise.* |
jakarta.enterprise.* |
javax.inject.* |
jakarta.inject.* |
javax.ws.rs.* |
jakarta.ws.rs.* |
javax.persistence.* |
jakarta.persistence.* |
javax.validation.* |
jakarta.validation.* |
javax.servlet.* |
jakarta.servlet.* |
javax.annotation.* |
jakarta.annotation.* |
javax.json.* |
jakarta.json.* |
javax.script.* (API do JDK) não deve ser alterado.
3. Atualizar beans.xml
<!-- Antes -->
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
<!-- Depois -->
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
version="4.0" bean-discovery-mode="all">
4. Renomear Arquivos SPI
# Antes
META-INF/services/javax.enterprise.inject.spi.Extension
# Depois
META-INF/services/jakarta.enterprise.inject.spi.Extension
5. Remover DeltaSpike
O Demoiselle 4 não depende mais do Apache DeltaSpike:
@MessageBundle/@MessageTemplate→ use as anotações do Demoiselle emorg.demoiselle.jee.core.annotationCdiTestRunner→ substitua por Weld JUnit 5 (@EnableAutoWeld)
6. Migrar Testes para JUnit 5
| JUnit 4 | JUnit 5 |
|---|---|
@org.junit.Test |
@org.junit.jupiter.api.Test |
@Before / @After |
@BeforeEach / @AfterEach |
@BeforeClass / @AfterClass |
@BeforeAll / @AfterAll |
@Ignore |
@Disabled |
@RunWith(CdiTestRunner.class) |
@EnableAutoWeld |
Assert.assertEquals(...) |
Assertions.assertEquals(...) |
7. Remover WildFly Swarm
O profile wildfly-swarm foi removido. Para runtimes embarcados, use WildFly 27+ (Galleon), Quarkus ou Open Liberty.
8. Migrar Swagger → OpenAPI
| Swagger 1.x | OpenAPI 3.0 (MicroProfile) |
|---|---|
@Api |
@Tag |
@ApiOperation |
@Operation |
@ApiParam |
@Parameter |
io.swagger:swagger-jaxrs |
org.eclipse.microprofile.openapi:microprofile-openapi-api |
9. Atualizar Groovy (se aplicável)
<!-- Antes -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</dependency>
<!-- Depois -->
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-all</artifactId>
<type>pom</type>
</dependency>
Parte II — Mudanças de API e Comportamento
10. Records — DTOs e Mensagens
| Classe | Antes | Depois |
|---|---|---|
SortModel |
Classe mutável | Record imutável |
DemoiselleRestExceptionMessage |
Classe com getters | Record (error(), errorDescription(), errorLink()) |
ResultSet |
setContent(null) → NPE |
setContent(null) → lista vazia |
PageResult |
Não existia | Record com metadados de paginação |
TokenEntry |
DemoiselleUser direto no mapa |
Record TokenEntry(user, expiresAt) |
// Antes: getError(), getErrorDescription(), getErrorLink()
msg.getError();
// Depois: error(), errorDescription(), errorLink()
msg.error();
11. Sealed Classes — FilterOp
// Antes: strings e if-else no DAO
if (value == null) { /* isNull */ }
else if (value.contains("*")) { /* like */ }
// Depois: pattern matching exaustivo
return switch (op) {
case FilterOp.IsNull(var key) -> cb.isNull(from.get(key));
case FilterOp.Like(var key, var p) -> buildLikePredicate(cb, cq, from, key, p);
// ... compilador garante exaustividade
};
Novos operadores de comparação via query string (gt:, lt:, gte:, lte:, between:, in:) — sem alteração necessária em código existente.
12. Coleções Imutáveis — Segurança
// Antes: Collections.unmodifiableList() — view mutável
List<String> roles = user.getRoles(); // view
user.addRole("admin");
roles.contains("admin"); // true (!)
// Depois: List.copyOf() — cópia defensiva
List<String> roles = user.getRoles(); // cópia independente
user.addRole("admin");
roles.contains("admin"); // false
Se seu código dependia de views mutáveis, ajuste para re-obter a lista após modificações.
13. Result Tipado
// Antes: raw type, cast necessário
Result result = dao.find();
List<?> content = result.getContent();
// Depois: genérico, type-safe
Result<Produto> result = dao.find();
List<Produto> content = result.getContent();
Código existente com raw type continua compilando (apenas warnings).
14. Formato de Erro REST — RFC 9457
Nenhuma alteração necessária por padrão. O formato legado é mantido:
# demoiselle.properties — padrão (sem alteração)
# demoiselle.rest.errorFormat=legacy
# Para ativar RFC 9457 (opt-in)
demoiselle.rest.errorFormat=rfc9457
Se seu frontend parseia os campos error, error_description, error_link, ele continua funcionando. Para migrar para RFC 9457, ajuste o parser para os campos type, title, status, detail, instance.
15. Headers de Paginação — RFC 8288
O header Link é adicionado automaticamente. Headers customizados (X-Total-Count, etc.) continuam presentes. Nenhuma alteração necessária.
Se seu frontend já usa os headers X-*, nada muda. Para adotar o padrão RFC 8288, passe a usar o header Link com as relações next, prev, first, last.
16. Rate Limiting — HTTP 429
// Antes: DemoiselleSecurityException genérica
catch (DemoiselleSecurityException e) { /* status 429 */ }
// Depois: Response JAX-RS direta com Retry-After
// O interceptor retorna Response 429 diretamente
// Se seu código capturava a exceção, ajuste para tratar a resposta HTTP
O header Retry-After agora é incluído automaticamente. Clientes podem implementar backoff baseado nesse valor.
17. JWT — Novas Funcionalidades e Defaults Seguros
# Refresh token (opt-in)
demoiselle.security.jwt.refreshTokenTtlMilliseconds=86400000
# Identificador da chave configurada
demoiselle.security.jwt.activeKeyId=key-2024
demoiselle.security.jwt.privateKey=...
demoiselle.security.jwt.publicKey=...
# Allowlist opcional; sem a propriedade, somente RS256 é aceito
demoiselle.security.jwt.allowedAlgorithms=RS256,RS384,RS512
# Clock skew (padrão 60s)
demoiselle.security.jwt.clockSkewSeconds=60
Atenção na migração: a validação agora é fail-closed para algoritmo e
kid. Tokens comkidexplícito diferente deactiveKeyId(ou de uma chave conhecida por um provedor futuro) recebem 401. Valide emissores legados antes de atualizar. Propriedades aninhadaskeys.<kid>.*não são suportadas nesta versão.
18. Token — Resolução de Ambiguidade CDI
// Antes: TokenImpl era @RequestScoped (bean CDI normal)
// Problema: ambiguidade com o producer em SecurityFilter (WELD-001409)
// Depois: TokenImpl é @Vetoed (não é mais bean CDI)
// O Token request-scoped é produzido exclusivamente pelo SecurityFilter
// setKey() e setType() continuam funcionando normalmente
// TokenRecord existe como alternativa imutável para uso externo,
// mas o producer do SecurityFilter entrega TokenImpl (mutável)
// para compatibilidade com TokenManagerImpl.setUser()
19. Security Token — validate(issuer, audience)
O método validate(issuer, audience) agora verifica efetivamente os parâmetros. Se seu código dependia do comportamento anterior (parâmetros ignorados), ajuste os params do DemoiselleUser para incluir issuer e audience.
20. Configuração — Perfis e @DefaultValue
Funcionalidades opt-in. Sem configuração de perfil, o comportamento é idêntico ao anterior:
# Ativar perfil (opt-in)
java -Ddemoiselle.profile=dev -jar app.jar
# ou
export DEMOISELLE_PROFILE=prod
21. @CacheControl Tipado
// Antes (continua funcionando)
@CacheControl("max-age=3600, no-store")
// Depois (nova opção)
@CacheControl(maxAge = 3600, noStore = true)
O atributo value() tem precedência quando preenchido — código existente não quebra.
22. CORS via Properties
# Opt-in — sem configuração, comportamento anterior mantido
demoiselle.security.cors.allowedOrigins=https://app.example.com
demoiselle.security.cors.allowedMethods=GET,POST,PUT,DELETE
demoiselle.security.cors.allowedHeaders=Authorization,Content-Type
demoiselle.security.cors.maxAge=3600
23. Novas Anotações de Segurança
Aditivas — não afetam código existente:
@RequiredAnyRole({"admin", "manager"}) // OR — nova
@RequiredAllPermissions({...}) // AND — nova
@RequiredRole("admin") // existente — inalterada
@RequiredPermission(resource="x", op="y") // existente — inalterada
24. Observabilidade e OpenAPI
Módulos opcionais. Basta adicionar a dependência ao pom.xml:
<dependency>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-observability</artifactId>
</dependency>
<dependency>
<groupId>org.demoiselle.jee</groupId>
<artifactId>demoiselle-openapi</artifactId>
</dependency>
Sem a dependência, nenhum comportamento muda.
Parte III — Mudanças de Comportamento
| Componente | Antes (v3) | Depois (v4) |
|---|---|---|
SecurityContextImpl |
hasPermission()/hasRole() → NPE sem usuário |
Retorna false |
TokenManagerImpl |
removeUser() → UnsupportedOperationException |
Limpa token do request scope |
KeyPairHolder |
Campos static |
@ApplicationScoped CDI bean |
AbstractDAO |
Mensagens hardcoded, Exception genérica |
Message bundles, exceções específicas |
CrudFilter |
Projeção limitada a 2 níveis | Profundidade arbitrária + ReflectionCache |
Checklist de Migração
- Atualizar versão do parent POM para 4.1.0-SNAPSHOT
- Configurar Java 21 no maven-compiler-plugin
- Substituir imports
javax.*porjakarta.* - Atualizar
beans.xmlpara namespace Jakarta EE - Renomear arquivos
META-INF/services/javax.*parajakarta.* - Remover dependências DeltaSpike
- Migrar testes de JUnit 4 para JUnit 5
- Remover configurações WildFly Swarm
- Migrar anotações Swagger para OpenAPI (se aplicável)
- Atualizar Groovy para
org.apache.groovy(se aplicável) - Atualizar servidor de aplicação para Jakarta EE 10
- Ajustar accessors de records (
getError()→error()) - Verificar código que depende de views mutáveis de coleções
- Testar a aplicação completa
Resumo de Breaking Changes
| Mudança | Impacto | Ação |
|---|---|---|
| Baseline Java 21 | Alto | Atualizar JDK local, CI e imagens de runtime |
javax.* → jakarta.* |
Alto | Substituir imports |
Demoiselle-Version oculto |
Baixo | Ativar demoiselle.rest.exposeFrameworkVersion=true somente se necessário |
| Limites CRUD globais | Médio | Ajustar propriedades se a API aceita páginas/filtros maiores |
Record accessors (error() vs getError()) |
Médio | Atualizar chamadas |
List.copyOf() em coleções de segurança |
Baixo | Re-obter lista após mutação |
TokenImpl agora @Vetoed (não é bean CDI) |
Baixo | Token vem do producer do SecurityFilter |
validate(issuer, audience) corrigido |
Baixo | Verificar params do user |
| Rate limit retorna Response (não exceção) | Baixo | Ajustar catch se aplicável |
Resumo de Funcionalidades Opt-in (sem breaking change)
| Funcionalidade | Configuração |
|---|---|
| RFC 9457 Problem Details | demoiselle.rest.errorFormat=rfc9457 |
| RFC 8288 Link headers | Automático (aditivo) |
| JWT Refresh Token | demoiselle.security.jwt.refreshTokenTtlMilliseconds |
| JWT Key Rotation | demoiselle.security.jwt.activeKeyId |
| JWT Múltiplos Algoritmos | demoiselle.security.jwt.allowedAlgorithms |
| JWT Validation Profiles | demoiselle.security.jwt.validationProfile=recommended ou strict |
| Perfis de Configuração | -Ddemoiselle.profile=dev |
| @DefaultValue | Anotação em campos @Configuration |
| CORS via Properties | demoiselle.security.cors.* |
| @CacheControl tipado | Atributos na anotação |
| @RequiredAnyRole | Nova anotação |
| @RequiredAllPermissions | Nova anotação |
| Observabilidade | Dependência Maven |
| OpenAPI | Dependência Maven |
| MCP (Model Context Protocol) | Dependência Maven + demoiselle.mcp.* |