I'm working on a Spring MVC + Hibernate + JPA app with a user registration form and I decided to use a JSR-303 validator to check whether the username already existed in the DB:
public class UniqueUsernameValidator implements ConstraintValidator<VerifyUniqueUsername, String> {
@Autowired
UserService userService;
@Override
public void initialize(VerifyUniqueUsername constraintAnnotation) {
}
@Override
public boolean isValid(String username, ConstraintValidatorContext context) {
return username!=null && userService.findByUsername(username) == null;
}
}
It is very straightforward and validation worked great on my controller:
....
public String signup(@Valid @ModelAttribute("newUser") User user, BindingResult newUserBeanResult)
.....
The current problem I'm facing is that after I get my User
object validated and I call:
userService.save(user);
Which implements CrudRepository
, I get a NullPointerException
. For some reason UserService
is injected during validation on the controller but not when I call CrudRepository.save()
.
I saw similar posts such as this: @Autowired bean null in ConstraintValidator when invoked by Sessionfactory.getCurrentSession.merge and this: hibernate validator without using autowire but I was wondering if someone has run into this before. I would think that injecting beans to access the database on a validator is fairly common.
As a workaround I added a check for null on userService
but it doesn't feel right.
CrudRepository.save()
?pre-insert
I ended up solving this issue by instructing Spring's EntityManagerFactoryBean
to use my validator bean (more accurately, hibernate will now be using Spring's validator):
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
<property name="packagesToScan" value="some.packages"/>
<property name="jpaPropertyMap">
<map>
<entry key="javax.persistence.validation.factory" value-ref="validator" />
</map>
</property>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">10</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
However, this threw a StackOverflow error :)
Apparently the cause of this issue was that my validator was using a finder method (findByUsername
) and finder methods trigger a hibernate flush, which in turn triggers a validation. This loops indefinitely until you get the most famous exception there is.
So...I fixed this by changing the validators to use the EntityManager directly (instead of the CRUD repository) and temporarily change the FlushModeType to COMMIT. Here is the example:
public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {
@PersistenceContext
private EntityManager em;
@Autowired
UserService userService;
@Override
public void initialize(UniqueUsername constraintAnnotation) {
}
@Override
public boolean isValid(String username, ConstraintValidatorContext context) {
try {
em.setFlushMode(FlushModeType.COMMIT);
return userService.findByUsername(username) == null;
} finally {
em.setFlushMode(FlushModeType.AUTO);
}
}
}
This solves the issue where the validator was using the finder function which triggered a hibernate flush which in turn was triggering the validator causing a StackOverflowError.
When the validation logic is invoked in response to the save method it is done by hibernate. The validator object is created by hibernate so the spring @AutoWired will not work.
One option to fix this is to use the @Configurable annotation and enable load time weaving to make sure that even when hibernate instantiates the validator object spring gets to inject dependencies into it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With