Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

@Autowired bean works with @Valid on controller but fails with CRUD repository

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.

  1. Is this expected behaviour? Are these validations supossed to fire before calling CrudRepository.save()?
  2. Am I suppossed to handle "manually" hibernate events? In this case pre-insert
like image 778
Ulises Avatar asked Jul 15 '12 06:07

Ulises


2 Answers

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.

like image 190
Ulises Avatar answered Sep 22 '22 17:09

Ulises


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.

like image 36
gkamal Avatar answered Sep 20 '22 17:09

gkamal