Accessing Spring Beans inside AttributeConverter class

Viewed 10584

I'm developing a Spring Data JPA application, and I've created an AttributeConverter class in order to save an ArrayList of objects as JSON in a database column. Inside this class I need to make use of a class I have defined as a Spring Bean.

As the AttributeConverter class is managed by Hibernate, it seems to be instantiated before any Spring beans are created, and therefore DI does not seem to work (the Spring Bean in the AttributeConverter class is null, and I'm getting a NullPointer exception thrown). So at the moment I'm creating another instance of the said bean to be able to use it in the AttributeConverter class (which defeats the purpose of DI).

I've also tried creating a Util class (annotated with @Component) which implements ApplicationContextAware, which provides a method giving the SpringBean (cxt.getBean(BeanClass.class)). But this also is instantiated after the AttributeConverter.

Is there any idea of how this can be solved?

Thank you.

3 Answers

With JPA 2.2, Spring 5.1( SPR-16305) and Hibernate 5.3.0 (HHH-12135) you no longer need to use the mutable static property hack and can use dependency injection just like you would on a regular spring managed bean (note that annotations are no longer necessary):

public class MyAttributeConverter implements AttributeConverter<X,Y> {

    private final MySpringBean bean;

    public MyAttributeConverter(MySpringBean bean) {
        this.bean = bean;
    }

    public Y convertToDatabaseColumn(X attribute) {
      ...
    }

    public X convertToEntityAttribute(Y dbData) {
      ...
    }
}

You can inject a bean(@Component, @Service, @Repository) inside an AttributeConverter using static properties

Setps:

  1. Set in your AttributeConverter with the following annotations: @Component, @Converter and @Configurable
  2. Define the field that you want to autowired with the static modifier access
  3. Create an init method in order to autowired the repository
  4. Implement the methods defined in the interface AttributeConverter

Basically, the code should look like this...

//Step 1
@Component
@Converter
@Configurable
public class MyAttributeConverter implements AttributeConverter<X,Y> {
    //Where: X = the type of the entity attribute and Y = the type of the database column

    //Step 2
    private static MyRepository myRepository;

    //Step 3
    @Autowired
    public void initMyRepository(MyRepository myRepository){
        MyAttributeConverter.myRepository = myRepository;
    }

    //Step 4
    Y convertToDatabaseColumn(X attribute){//TODO implement method}
    X convertToEntityAttribute(Y dbData){//TODO implement method}
}

I hope it can help!!!

Generally Ipandzic's answer is the right hint I guess. However it did not work for me how he described it. I argument-constructor also looks a little strange in a Spring-environment. I played around a bit and was able to use an AttributeConverter of the following form (indeed you do not need the @Converter-annotation or any other on the AttributeConverter-class itself):

import javax.persistence.AttributeConverter;
import org.springframework.beans.factory.annotation.Autowired;

public class MyConverter implements AttributeConverter<String, String> {
    @Autowired
    private MyBean mybean;

    public String convertToDatabaseColumn(String value) {
        return myBean.changeValue(value);
    }

    public String convertToEntityAttribute(String dbValue) {
        return myBean.undoChange(dbValue);
    }
}

But creating this class and upgrading to Spring-Boot 2.1 (including Spring 5.1, Hibernate 5.3 and JPA 2.2) did not do the trick for me. The thing was, that I used LocalContainerEntityManagerFactoryBean to configure my persistent storage which will not enable Dependency injection for AttributeConverters. Reading the first link Ipandzic posted suggests you have to bring LocalSessionFactoryBuilder into play somehow. That's why I ended up with the following configuration:

//...

@Bean
public LocalSessionFactoryBean entityManagerFactory(DataSource dataSource, Environment env) {
    LocalSessionFactoryBean factory = new LocalSessionFactoryBean();
    factory.setDataSource(dataSource);

    // somehow tell the factory where you entitiy-definitions are, this is just
    // one possibility of doing so:
    String entityPackage = JpaMarkerModel.class.getPackage().getName();
    log.info("EntityManager will scan for entities in package [{}].", entityPackage);
    factory.setPackagesToScan(entityPackage);

    return factory;
}

@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(entityManagerFactory);
    return transactionManager;
}

//...

This "answer" is merely an addition to Ipandzi's, but maybe it helps some people to solve their problem quicker than I did.

Related