7.6 KiB
20 — Hibernate Validator CDI integration
← Previous: 19 — HikariCP connection pooling | Back to README →
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:
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
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
Without CDI: @Inject is not processed at all, and the failure is a plain NPE
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
validator.validate(new StockLevel(3)); // throws
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
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.
Trap: 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.
With CDI: the same code, actually injected
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
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.
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.
Trap: a project can have Weld or another CDI implementation on its classpath and still get the plain, non-injecting behavior everywhere it callsValidation.buildDefaultValidatorFactory()directly instead of obtaining theValidator/ValidatorFactoryas a CDI-managed bean.
- Going deeper: this repo pins
hibernate-validator-cdi,weld-se-core, andorg.glassfish.expresslyto specific, mutually-verified versions inpom.xml--hibernate-validator-cdi:9.1.3.Finalrequiresjakarta.enterprise.cdi-api:4.1.0(checked against its ownpom.xml), andweld-se-core:6.0.4.Finalis the version that actually resolves that exact 4.1.0, confirmed with a throwawaymvn dependency:treebefore writing any of this chapter's code. - Going deeper:
hibernate-validator-cdialso ships a method-validation interceptor (ValidationInterceptor, visible in the jar's contents) for validating@Validparameters 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
ConstraintValidatorneeds a collaborator, know whichValidatorFactorywill actually build it in production. In a full Jakarta EE server or a Spring Boot app withspring-boot-starter-validation(which wires its own Spring-awareConstraintValidatorFactory, 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
@Injected field without at least considering what happens if it's ever built by a plainConstraintValidatorFactory-- a defensive null-check turns an opaqueValidationExceptioninto a clear "this validator requires CDI" message. hibernate-validator-cdiis a thin CDI portable extension overhibernate-validatoritself, not a different validation engine -- adding it changes how validators are instantiated, not what Bean Validation itself does.
← Previous: 19 — HikariCP connection pooling | Back to README →