121 lines
7.6 KiB
Markdown
121 lines
7.6 KiB
Markdown
# 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)
|