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:
@@ -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)
|
||||
Reference in New Issue
Block a user