Getting Spring 4 dependency injection working with

2019-06-09 16:11发布

问题:

I have been running into a lot of trouble trying to get custom Bean Validation constraints to successfully leverage Spring's dependency injection.

For example I might define a constraint:

@Constraint(validatedBy = { CustomConstraintValidator.class })
@Documented
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomConstraint {
    String message() default "custom.constraint.error";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

Validator Implementation

@Component
public class CustomConstraint implements ConstraintValidator<CustomConstraint, String> {

    @Autowired
    private SomeSpringBean someSpringBean;

    @Override
    public void initialize(CustomConstraint constraintAnnotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {

        if (value != null) {
            SomeObject storedValue = someSpringBean.find(value);

                    return storedValue != null;
        }

        return true;
    }

}

The standard way to setup Bean Validation with RESTeasy is to pull in: org.jboss.resteasy:resteasy-validator-provider-11:3.0.6.Final

However when the validation is executed at runtime, any @Autowired field is always null.

Versions
Spring: 4.0.4.Release
RESTeasy: 3.0.6.Final

回答1:

I have found a solution, it is not ideal yet but it does work and could evolve into a proper solution.

After reading through many StackOverflow questions and various web articles I found hardly any references to getting Spring and Resteasy working with validation. There are plenty of references for Spring and Bean Validation, but once Resteasy is introduced the discussion disappears. What is clear from other discussions is that Spring provides a ValidatorFactory implementation called LocalValidatorFactoryBean that will enabled Spring dependency injection within validator implementations.

Looking into org.jboss.resteasy:resteasy-validator-provider-11:3.0.6.Final I found out it mostly is setting up a @Provider instance that provides the GeneralValidator used by Resteasy to resolve Bean Validation. The problem I noticed was that the provider class, ValidatorContextResolver, does not really provide any way to set up a different version of the ValidatorFactory. It tries to load via CDI / JNDI and if that fails then defaults to a hardcoded ValidatorFactory implementation. In my instance I can't rely on the JNDI lookup so there was no way to provide Spring's LocalValidatorFactoryBean as the factory.

On the other hand the resteasy-validator-provider-11 was a very small library and the code that would need to change to instead use Spring's LocalValidatorFactoryBean was minimal. You can drop the resteasy-validator-provider-11 dependency and reimplement the library to use Spring instead. I will show related code below that will get this working, leaving out classes from the resteasy-validator-provider-11 library that I did not change.

ValidatorContextResolver
You need to get a copy of the LocalValidatorFactoryBean bootstrapped by Spring, you cannot just instantiate it with new here:

@Component
@Provider
public class ValidatorContextResolver implements ContextResolver<GeneralValidator> {
    private final static Logger log = Logger.getLogger(ValidatorContextResolver.class);
    private volatile ValidatorFactory validatorFactory;
    final static Object RD_LOCK = new Object();

    @Autowired
    private ValidatorFactory springValidatorFactoryBean;

    // this used to be initialized in a static block, but I was having trouble class loading the context resolver in some
    // environments. So instead of failing and logging a warning when the resolver is instantiated at deploy time
    // we log any validation warning when trying to obtain the ValidatorFactory.
    ValidatorFactory getValidatorFactory() {
        ValidatorFactory tmpValidatorFactory = validatorFactory;
        if (tmpValidatorFactory == null) {
            synchronized (RD_LOCK) {
                tmpValidatorFactory = validatorFactory;
                if (tmpValidatorFactory == null) {
                    log.info("Obtaining Spring-bean enabled validator factory");
                    validatorFactory = tmpValidatorFactory = springValidatorFactoryBean;
                }
            }
        }
        return validatorFactory;
    }

    @Override
    public GeneralValidator getContext(Class<?> type) {
        try {
            Configuration<?> config = Validation.byDefaultProvider().configure();
            BootstrapConfiguration bootstrapConfiguration = config.getBootstrapConfiguration();
            boolean isExecutableValidationEnabled = bootstrapConfiguration.isExecutableValidationEnabled();
            Set<ExecutableType> defaultValidatedExecutableTypes = bootstrapConfiguration.getDefaultValidatedExecutableTypes();
            return new GeneralValidatorImpl(getValidatorFactory(), isExecutableValidationEnabled, defaultValidatedExecutableTypes);
        } catch (Exception e) {
            throw new ValidationException("Unable to load Validation support", e);
        }
    }
}

SpringValidationConfig
Setup the LocalValidatorFactoryBean bean in Spring. You can do this however you want, I prefer programmatically

@Configuration
public class DatabaseConfig
{
    @Bean
    public ValidatorFactory validator() {
        LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
       // localValidatorFactoryBean.setValidationMessageSource(getValidationMessageSource());
        return localValidatorFactoryBean;
    }
}

And finally configure Spring to load the both of those classes. I do it by component-scan in my Spring config like:

<context:component-scan base-package="org.example.resteasy.spring.validation.config" />

Following these steps I was able to get the @Autowired dependency injection working in my custom validation constraint implementations like in the question.

The ideal path forward would be to have an alternate resteasy-validator-provider-11 that works with Spring.