Search code examples
javavaadinquarkusbean-validationhibernate-validator

Vaadin Bean Validation not working after Quarkus Live Reload


I'm playing around with Quarkus 3.6.3 and Vaadin 24.3.0. I have the problem that the required indicator for mandatory fields are not shown any more when the quarkus application is live reloaded in dev mode due to changes in any of the class files.

Java classes

I have a view showing some text fields and I use BeanValidationBinder to bind the fields to an instance of a JPA entity class User. This class is simple and looks like this:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class User {

    @NotNull
    @Column
    private String firstName;

    @NotNull
    @Column
    private String lastName;

    // Getters and Setters
}

For the sake of simplicity, the view class looks like this:

@Route(value = "user", layout = MainLayout.class)
public class UserFormView extends VerticalLayout {

    private final TextField firstName = new TextField("First name");

    private final TextField lastName = new TextField("Last name");

    private final Binder<User> binder = new BeanValidationBinder<>(User.class);

    public UserFormView() {

        binder.forField(firstName).withNullRepresentation("").bind("firstName");
        binder.forField(lastName).withNullRepresentation("").bind("lastName");
        binder.readBean(new User());

        add(firstName, lastName);
    }
}

The problem

After a live reload the variable propertyDescriptor in the method BeanValidationBinder#configureRequired is null and thus the bean validation annotations of the domain class are not processed for setting the required indicator and for the binder's validation.

Now I wonder if the required indicators should also be shown correctly after live reload.

Debugging

From debugging I figured out:

  • when initially starting the application, an instance of PredefinedScopeBeanMetaDataManager is created and holds a beanMetaDataMap of classes with bean validation annotations as instances of BeanMetaDataImpl. This instance is kept in an instance of CloseAsNoopValidatorFactoryWrapper within the constant LazyFactoryInitializer#FACTORY.
  • after a live reload the beanMetaDataMap of this instance is cleared and a new instance of PredefinedScopeBeanMetaDataManager is created which again holds a map of classes with bean validation annotations. But this new instance is not used for further lookups but the old instance with the empty map is used since it is kept in the constant descibed above.

Solution

  • Thanks for the detective work. That is a bug for sure.

    From what I can see, there's not much we can do in Quarkus. Vaadin Flow should avoid to store the ValidatorFactory in a static field in LazyFactoryInitializer#FACTORY. Or at least clean things up when the app is stopped and create a new one then. The current code would cause class loader leaks with any server reloading the app anyway.

    Following this comment by Ivan Kaliuzhnyi in the Flow tracker https://github.com/vaadin/flow/issues/4481#issuecomment-1712720926, you should be able to work around it even if it's not pretty at all.

    Pasting from the comment to keep history:

    @Configuration
    public class ValidatorConfig implements InitializingBean {
    
        @Autowired
        private ValidatorFactory validatorFactory;
    
        @Bean
        public ValidatorFactory validatorFactory(MessageSource messageSource) {
            var bean = new LocalValidatorFactoryBean();
            bean.setValidationMessageSource(messageSource);
            return bean;
        }
    
        @Bean
        public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(ValidatorFactory validatorFactory) {
            // AvailableSettings.JPA_VALIDATION_FACTORY -> javax.persistence.validation.factory
            // AvailableSettings.JAKARTA_VALIDATION_FACTORY -> jakarta.persistence.validation.factory
            return hibernateProperties ->
                hibernateProperties.put(AvailableSettings.JAKARTA_VALIDATION_FACTORY, validatorFactory);
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
    
            var className = "com.vaadin.flow.data.validator.BeanValidator.LazyFactoryInitializer";
            var fieldName = "FACTORY";
            var field = FieldUtils.getDeclaredField(ClassUtils.getClass(className), fieldName, true);
    
            var unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            var unsafe = (Unsafe) unsafeField.get(null);
    
            unsafe.putObject(unsafe.staticFieldBase(field),
                    unsafe.staticFieldOffset(field),
                    validatorFactory);
    
        }
    }