Add Hibernate 7 batches 2-6, batch 7, and batch 8: mapping styles, JPA annotations, natural IDs, @Immutable, stored procedures, in-memory test databases, JNDI mocking, proxies, associations, temporal mapping, named queries, HQL, Criteria API, EntityManager bootstrapping, Ehcache 3 L2 cache configuration, HikariCP connection pooling, Hibernate Validator CDI integration, aggregate functions, sorting, pagination, interceptors, and Hibernate Search 8 (Hibernate 7.4.5.Final + Spring Boot 4.1.1 + JDK 25)

This commit is contained in:
2026-09-20 06:06:42 +00:00
committed by Claude
commit 8568c0ce6c
330 changed files with 23668 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
# 20 — Hibernate Validator CDI integration
[← Previous: 19 — HikariCP connection pooling](19-hikaricp-connection-pooling.md) | [Back to README →](../README.md)
Backs the rewrite of ankurm.com post 4887 (Hibernate Validator CDI integration).
This is the odd chapter out in the batch: no Spring, no database, no Hibernate ORM at all. The
question the original article raised is purely about Jakarta Bean Validation and CDI --
`@Inject` inside a `ConstraintValidator`, with and without a CDI container actually running -- so
this chapter measures that in isolation, the same way chapter 17 isolated raw JPA bootstrap from
Spring.
## The claim, and the two things it depends on
A `ConstraintValidator` that needs a collaborator -- a policy object, a lookup service, anything
that isn't a static constant -- naturally reaches for `@Inject`. Whether that actually works
depends entirely on how the `ValidatorFactory` was built, not on the annotation itself:
```java
public class PositiveInventoryValidator implements ConstraintValidator<PositiveInventory, Integer> {
@Inject
private InventoryPolicy policy; // no null-guard, on purpose
@Override
public boolean isValid(Integer quantity, ConstraintValidatorContext context) {
return quantity == null || quantity >= policy.minimumThreshold();
}
}
```
[`PositiveInventoryValidator.java`](../src/main/java/com/ankurm/hibernatedemo/validation/PositiveInventoryValidator.java)
`InventoryPolicy` is a plain `@ApplicationScoped` CDI bean with one method,
`minimumThreshold()`, returning `5` -- deliberately not a hardcoded constant in the validator
itself, so injection either genuinely happens or the validator has nothing to call.
[`InventoryPolicy.java`](../src/main/java/com/ankurm/hibernatedemo/validation/InventoryPolicy.java)
## Without CDI: `@Inject` is not processed at all, and the failure is a plain NPE
```java
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
validator.validate(new StockLevel(3)); // throws
```
[`PlainValidationNoCdiTest.java`](../src/test/java/com/ankurm/hibernatedemo/validation/PlainValidationNoCdiTest.java)
```
RESULT[cdi-plain-validation-no-injection]: validating StockLevel(3) with Validation.buildDefaultValidatorFactory() (no CDI container running) throws jakarta.validation.ValidationException -> caused by java.lang.NullPointerException
```
[(full transcript)](output/20-plain-validation-no-cdi.txt)
`Validation.buildDefaultValidatorFactory()`'s default `ConstraintValidatorFactory` builds a
validator with plain reflection -- roughly `Class.getDeclaredConstructor().newInstance()`. It has
no idea what CDI or `@Inject` even are; the annotation is simply never looked at, so `policy`
stays `null`. The `NullPointerException` from calling `policy.minimumThreshold()` is not a
validation failure -- it's a validator bug, and Hibernate Validator itself is honest about that:
it wraps the unexpected exception in a `jakarta.validation.ValidationException` rather than
letting the raw NPE escape unannounced, which is exactly what the transcript shows.
<blockquote style="border-left:4px solid #d97757;background:#fdf3ee;padding:0.75rem 1.25rem;margin:1.5rem 0;">
<strong>Trap:</strong> this failure only shows up when the constraint actually runs against a
non-null value. A validator that only reaches the injected field on certain code paths can pass
every test that happens not to exercise those paths, then NPE the first time production data
takes the other branch.
</blockquote>
## With CDI: the same code, actually injected
```java
WeldContainer container = new Weld().initialize();
Validator validator = container.select(Validator.class).get();
validator.validate(new StockLevel(3)); // 1 violation
validator.validate(new StockLevel(5)); // 0 violations
validator.validate(new StockLevel(10)); // 0 violations
```
[`CdiValidationTest.java`](../src/test/java/com/ankurm/hibernatedemo/validation/CdiValidationTest.java)
```
RESULT[cdi-validation-injection-works]: validator obtained from a running Weld SE container | StockLevel(3) violations=1 | StockLevel(5) violations=0 | StockLevel(10) violations=0 -- InventoryPolicy.minimumThreshold()=5 was actually injected and actually used, no NullPointerException anywhere.
```
[(full transcript)](output/20-cdi-validation-injection.txt)
The mechanism, not just the result: `hibernate-validator-cdi-9.1.3.Final.jar` registers
`org.hibernate.validator.cdi.ValidationExtension` as a
`jakarta.enterprise.inject.spi.Extension` (confirmed by its own
`META-INF/services/jakarta.enterprise.inject.spi.Extension` file). Once Weld SE discovers that
extension on the classpath, it contributes CDI beans for `Validator` and `ValidatorFactory` whose
`ConstraintValidatorFactory` is `org.hibernate.validator.cdi.spi.InjectingConstraintValidatorFactory`
-- a factory that builds each `ConstraintValidator` instance through the CDI `BeanManager`
instead of plain reflection, resolving `@Inject` fields the same way any other managed bean's
are resolved. Get the `Validator` from `Validation.buildDefaultValidatorFactory()` even while a
CDI container happens to be running elsewhere in the same JVM, and you're back to the first,
broken case -- what matters is *which* `ValidatorFactory` built the validator, not merely whether
a container exists somewhere.
<blockquote style="border-left:4px solid #d97757;background:#fdf3ee;padding:0.75rem 1.25rem;margin:1.5rem 0;">
<strong>Trap:</strong> a project can have Weld or another CDI implementation on its classpath and
still get the plain, non-injecting behavior everywhere it calls
<code>Validation.buildDefaultValidatorFactory()</code> directly instead of obtaining the
<code>Validator</code>/<code>ValidatorFactory</code> as a CDI-managed bean.
</blockquote>
- Going deeper: this repo pins `hibernate-validator-cdi`, `weld-se-core`, and `org.glassfish.expressly` to specific, mutually-verified versions in [`pom.xml`](../pom.xml) -- `hibernate-validator-cdi:9.1.3.Final` requires `jakarta.enterprise.cdi-api:4.1.0` (checked against its own `pom.xml`), and `weld-se-core:6.0.4.Final` is the version that actually resolves that exact 4.1.0, confirmed with a throwaway `mvn dependency:tree` before writing any of this chapter's code.
- Going deeper: `hibernate-validator-cdi` also ships a method-validation interceptor (`ValidationInterceptor`, visible in the jar's contents) for validating `@Valid` parameters on CDI-managed bean methods -- out of scope for this chapter, which is about constructor/field injection into the validator itself, not method interception.
## Production checklist
- If a `ConstraintValidator` needs a collaborator, know which `ValidatorFactory` will actually
build it in production. In a full Jakarta EE server or a Spring Boot app with
`spring-boot-starter-validation` (which wires its own Spring-aware `ConstraintValidatorFactory`,
a separate mechanism from the CDI one measured here), injection works by a different, already
container-managed path -- this chapter's contrast is specifically about the CDI portable
extension versus the plain, no-container default.
- Never let a validator dereference an `@Inject`ed field without at least considering what happens
if it's ever built by a plain `ConstraintValidatorFactory` -- a defensive null-check turns an
opaque `ValidationException` into a clear "this validator requires CDI" message.
- `hibernate-validator-cdi` is a thin CDI portable extension over `hibernate-validator` itself,
not a different validation engine -- adding it changes *how* validators are instantiated, not
what Bean Validation itself does.
[← Previous: 19 — HikariCP connection pooling](19-hikaricp-connection-pooling.md) | [Back to README →](../README.md)