quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.82k stars 2.69k forks source link

Injecting @ConfigMapping into ConstraintValidator results in an exception #43450

Open marko-bekhta opened 1 month ago

marko-bekhta commented 1 month ago

Describe the bug

Assume having some app properties as:

@ConfigMapping(prefix = "some.app.config.validation")
public interface ValidationConfigProperties {
    String pattern();
}

and then trying to use these properties in some custom constraint validator implementation as:

@ApplicationScoped
public class MyConstraintConstraintValidator implements ConstraintValidator<MyConstraint, String> {

    @Inject
    ValidationConfigProperties config;
    private Pattern pattern;

    @Override
    public void initialize(MyConstraint constraintAnnotation) {
        pattern = Pattern.compile(config.pattern());
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        return pattern.matcher(value).matches();
    }
}

Expected behavior

ConstraintValidatorFactory correctly instantiates the MyConstraintConstraintValidator instance.

Actual behavior

An exception is thrown:

Caused by: java.lang.RuntimeException: Failed to start quarkus
    at io.quarkus.runner.ApplicationImpl.<clinit>(Unknown Source)
    ... 7 more
Caused by: jakarta.validation.ValidationException: HV000032: Unable to initialize io.quarkus.it.hibernate.validator.injection.InjectedPropertyConstraintValidator_ClientProxy.
    at org.hibernate.validator.internal.engine.constraintvalidation.AbstractConstraintValidatorManagerImpl.initializeValidator(AbstractConstraintValidatorManagerImpl.java:148)
    at org.hibernate.validator.internal.engine.constraintvalidation.AbstractConstraintValidatorManagerImpl.createAndInitializeValidator(AbstractConstraintValidatorManagerImpl.java:90)
    at org.hibernate.validator.internal.engine.constraintvalidation.PredefinedScopeConstraintValidatorManagerImpl.getInitializedValidator(PredefinedScopeConstraintValidatorManagerImpl.java:60)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.<init>(ConstraintTree.java:56)
    at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.<init>(SimpleConstraintTree.java:37)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.of(ConstraintTree.java:66)
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.<init>(MetaConstraint.java:75)
    at org.hibernate.validator.internal.metadata.core.MetaConstraints.create(MetaConstraints.java:74)
    at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.convertToMetaConstraints(AnnotationMetaDataProvider.java:262)
    at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.findPropertyMetaData(AnnotationMetaDataProvider.java:235)
    at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.getFieldMetaData(AnnotationMetaDataProvider.java:229)
    at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.retrieveBeanConfiguration(AnnotationMetaDataProvider.java:130)
    at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.getBeanConfiguration(AnnotationMetaDataProvider.java:121)
    at org.hibernate.validator.internal.metadata.PredefinedScopeBeanMetaDataManager.getBeanConfigurationForHierarchy(PredefinedScopeBeanMetaDataManager.java:183)
    at org.hibernate.validator.internal.metadata.PredefinedScopeBeanMetaDataManager.createBeanMetaData(PredefinedScopeBeanMetaDataManager.java:150)
    at org.hibernate.validator.internal.metadata.PredefinedScopeBeanMetaDataManager.<init>(PredefinedScopeBeanMetaDataManager.java:100)
    at org.hibernate.validator.internal.engine.PredefinedScopeValidatorFactoryImpl.<init>(PredefinedScopeValidatorFactoryImpl.java:206)
    at org.hibernate.validator.PredefinedScopeHibernateValidator.buildValidatorFactory(PredefinedScopeHibernateValidator.java:42)
    at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.buildValidatorFactory(AbstractConfigurationImpl.java:435)
    at io.quarkus.hibernate.validator.runtime.HibernateValidatorRecorder$2.created(HibernateValidatorRecorder.java:190)
    at io.quarkus.arc.runtime.ArcRecorder.initBeanContainer(ArcRecorder.java:80)
    at io.quarkus.deployment.steps.ArcProcessor$notifyBeanContainerListeners1304312071.deploy_0(Unknown Source)
    at io.quarkus.deployment.steps.ArcProcessor$notifyBeanContainerListeners1304312071.deploy(Unknown Source)
    ... 8 more
Caused by: java.lang.RuntimeException: Error injecting io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties io.quarkus.it.hibernate.validator.injection.InjectedPropertyConstraintValidator.config
    at io.quarkus.it.hibernate.validator.injection.InjectedPropertyConstraintValidator_Bean.doCreate(Unknown Source)
    at io.quarkus.it.hibernate.validator.injection.InjectedPropertyConstraintValidator_Bean.create(Unknown Source)
    at io.quarkus.it.hibernate.validator.injection.InjectedPropertyConstraintValidator_Bean.create(Unknown Source)
    at io.quarkus.arc.impl.AbstractSharedContext.createInstanceHandle(AbstractSharedContext.java:119)
    at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:38)
    at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:35)
    at io.quarkus.arc.generator.Default_jakarta_enterprise_context_ApplicationScoped_ContextInstances.c0(Unknown Source)
    at io.quarkus.arc.generator.Default_jakarta_enterprise_context_ApplicationScoped_ContextInstances.computeIfAbsent(Unknown Source)
    at io.quarkus.arc.impl.AbstractSharedContext.get(AbstractSharedContext.java:35)
    at io.quarkus.arc.impl.ClientProxies.getApplicationScopedDelegate(ClientProxies.java:21)
    at io.quarkus.it.hibernate.validator.injection.InjectedPropertyConstraintValidator_ClientProxy.arc$delegate(Unknown Source)
    at io.quarkus.it.hibernate.validator.injection.InjectedPropertyConstraintValidator_ClientProxy.initialize(Unknown Source)
    at org.hibernate.validator.internal.engine.constraintvalidation.AbstractConstraintValidatorManagerImpl.initializeValidator(AbstractConstraintValidatorManagerImpl.java:142)
    ... 30 more
Caused by: jakarta.enterprise.inject.CreationException: Error creating synthetic bean [oli60TV5cswBFUOsKjucMk-Wsw8]: java.util.NoSuchElementException: SRCFG00027: Could not find a mapping for io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties
    at io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties_oli60TV5cswBFUOsKjucMk-Wsw8_Synthetic_Bean.doCreate(Unknown Source)
    at io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties_oli60TV5cswBFUOsKjucMk-Wsw8_Synthetic_Bean.create(Unknown Source)
    at io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties_oli60TV5cswBFUOsKjucMk-Wsw8_Synthetic_Bean.get(Unknown Source)
    at io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties_oli60TV5cswBFUOsKjucMk-Wsw8_Synthetic_Bean.get(Unknown Source)
    at io.quarkus.arc.impl.CurrentInjectionPointProvider.get(CurrentInjectionPointProvider.java:48)
    ... 43 more
Caused by: java.util.NoSuchElementException: SRCFG00027: Could not find a mapping for io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties
    at io.smallrye.config.SmallRyeConfig.getConfigMapping(SmallRyeConfig.java:592)
    at io.quarkus.arc.runtime.ConfigMappingCreator.create(ConfigMappingCreator.java:30)
    at io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties_oli60TV5cswBFUOsKjucMk-Wsw8_Synthetic_Bean.createSynthetic(Unknown Source)
    ... 48 more

How to Reproduce?

I've created a simple test in this commit https://github.com/marko-bekhta/quarkus/commit/f0fffb29172bf0d46d13765e047df1c430de6cb7

Output of uname -a or ver

Linux fedora 6.10.9-100.fc39.x86_64 #1 SMP PREEMPT_DYNAMIC Mon Sep 9 02:28:01 UTC 2024 x86_64 GNU/Linux

Output of java -version

Java version: 21.0.4, vendor: Eclipse Adoptium

Quarkus version or git rev

3.14.3

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)

Additional information

Injecting the same config properties elsewhere works fine.

quarkus-bot[bot] commented 1 month ago

/cc @radcortez (config)

radcortez commented 1 month ago

You need to annotate the mapping with @StaticInitSafe because ConstraintValidators are initialized during STATIC_INIT: https://quarkus.io/guides/config-mappings#static-init

marko-bekhta commented 1 day ago

Ohh, sorry, @radcortez, I missed your comment and just noticed that you've closed the ticket.

Yeah, indeed, constraint validators are currently initialized during the static init. I opened this ticket since we are currently working towards a Hibernate Validator 9 release, and we have a chance to make some changes there (if we need to) to address some of the Quarkus use cases.

@StaticInitSafe doesn't fit the case when the user has to pass the application configuration at runtime, let's say, depending on where it is deployed.

Not sure what would be the best solution, but here are a few suggestions we can consider:

radcortez commented 1 day ago

There is already a way to use runtime configuration, by injecting a Instance, which is initialized at runtime:

@ApplicationScoped
public class MyConstraintConstraintValidator implements ConstraintValidator<MyConstraint, String> {
    @Inject
    Instance<ValidationConfigProperties> config;

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        return config.get().pattern().matcher(value).matches();
    }
}

I think we shouldn't add specific APIs for this case, considering that other components have the same semantics (REST Providers for instance). This is not only for configuration but for any injectable bean.

yrodiere commented 1 day ago

Thanks @radcortez! I think that's something we discussed with Marko last time we talked about this. I wonder if you tried @marko-bekhta?

If it works in the case of Validator, it looks like this would be the solution @marko-bekhta ?

come up with a workaround that would allow the user to use the runtime property and add an example of that to the docs ...

yrodiere commented 1 day ago

By the way @radcortez , that's out of scope for this issue, but maybe we should create another one about clarifying the error message...

Caused by: java.util.NoSuchElementException: SRCFG00027: Could not find a mapping for io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties
    at io.smallrye.config.SmallRyeConfig.getConfigMapping(SmallRyeConfig.java:592)
    at io.quarkus.arc.runtime.ConfigMappingCreator.create(ConfigMappingCreator.java:30)
    at io.quarkus.it.hibernate.validator.injection.ValidationConfigProperties_oli60TV5cswBFUOsKjucMk-Wsw8_Synthetic_Bean.createSynthetic(Unknown Source)
    ... 48 more

Shouldn't that mention something like "you're trying to use runtime config at static init" or even simply "no values found for property x or y"... ? So that the user understand what they are doing wrong.

Unless that information is in a cause you removed from the stack trace @marko-bekhta?

radcortez commented 23 hours ago

It is just a generic error message. The mapping is not available in the config, so it cannot be found.

We can certainly provide a more helpful message. We did something similar for values here: https://github.com/quarkusio/quarkus/pull/36281

It should be possible to validate if the mapping injection can be done safely, and if not, warn the user.