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.
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);
}
}
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.
From debugging I figured out:
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
.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.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);
}
}