AutoWiring Spring Beans Into Classes Not Managed By Spring Like JPA Entity Listeners

In my previous article JPA Auditing: Persisting Audit Logs Automatically using EntityListeners, I have discussed how we can use Spring Data JPA automate Auditing and automatically create audit logs or history records and update CreatedBy, CreatedDate, LastModifiedBy, LastModifiedDate properties.

So in order to save history records for our File entity, we were trying to auto-wire EntityManager inside our FileEntityListener class and we have come to know that we can not do this.

We can not inject any Spring-managed bean in the EntityListener because EntityListeners are instantiated by JPA before Spring inject anything into it. EntityListeners are not managed Spring so Spring cannot inject any Spring-managed bean e.g. EntityManager in the EntityListeners.

And this case is not just with EntityListeners, you can not auto wire any Spring-managed bean into another class (i.e. utility classes) which is not managed by Spring.

Because it is a very common problem and can also arise with other classes so I tried to come out with a common solution which will not just solve this problem but will also help us getting Spring managed beans in other places.

AutoWiring-Spring-Beans-Into-Classes-Not-Managed-By-Spring-Like-JPA-Entity-Listeners

So I have created one utility class to fetch any bean according to our requirement.

@Service
public class BeanUtil implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }

}

Now to get any a bean in class we will just need call the BeanUtil.getBean(YourClass.class) and pass the class type to it and we will get the bean.

For Example in our case, we were trying to get the EntityManager bean inside FileEntityListener, we can simply do it by writing BeanUtil.getBean(EntityManager.class).

public class FileEntityListener {

    private void perform(File target, Action action) {
        EntityManager entityManager = BeanUtil.getBean(EntityManager.class);
        entityManager.persist(new FileHistory(target, action));
    }

}

You can find complete code on this Github Repository and please feel free to provide your valuable feedback.
Next Post Newer Post Previous Post Older Post Home

21 comments :

  1. hello, I've tried this and got an NullPointerException for the ApplicationContext object (the static one).

    ReplyDelete
    Replies
    1. Hi Agustinus, you should not get any problems in it.
      However you find the complete code on https://github.com/njnareshjoshi/articles/tree/master/spring-data-jpa-auditing

      Delete
    2. probably because ApplicationContext is not YET set when you call for EntityManager

      Delete
  2. In class BeanUtil the context has to be set first otherwise the npe would be thrown when getBean method is invoked. I assume this is the problem that Agustinus has. In your repo I have not found the place where you provide this context to the utility class.

    ReplyDelete
    Replies
    1. `BeanUtil` class is implementing `ApplicationContextAware` and overriding `setApplicationContext` method which automatically get called and set the applicationContext in the `BeanUtil`. I have executed the repo's code and it working fine, please tell me if you are getting the problem while executing it.

      Delete
  3. For the Multi module spring boot projects where beanUtil class should be place?

    ReplyDelete
    Replies
    1. It depends on the project architecture and from where you want access this code. If you want to access it from multiple modules then BeanUtil class should be in a module which is visible from other modules. Some kind of core module which other module can access.

      Delete
    2. I am getting RuntimeException
      No EntityManager with actual transaction available for current thread
      do you have any idea why?

      Delete
    3. I am not sure but it seems like some configuration issue, because your entity manager is not intialized yet.

      Delete
  4. Hi, after trying other solutions which did not work for me;
    this has worked for me for an entity listener's @PostUpdate method.

    Thank you very much.

    ReplyDelete
  5. Hi Naresh, I'm having similar issue, where I have Model as separate module. Model has two packages, each package is an entityscan for 2 other modules. and when ran those other 2 modules, I can see the applicationcontext loading for only 1 module on startup and not loading for another module. Not sure how I need to set the architecture for model class such that context is loaded for both the modules on startup.

    Below is the package structure of modules, Not sure, if the below is easily understood.


    Module-service1
    com.test.service1 (Package)
    com.test.service1.audit (Package)
    AuditRelatedClasses
    MainApplication (EntityScan = com.test.model)

    Module-service2
    com.test.anotherservice2 (Package)
    com.test.anotherservice2.audit (Package)
    AuditRelatedClasses
    MainApplication (EntityScan = com.test.anotherservice.model)


    Model
    com.test.anotherservice.model
    com.test.anotherservice.model.action
    Action
    BeanUtil
    com.test.anotherservice.model.consumer
    Consumer
    ConsumerEntityListener
    ConsumerHistory
    com.test.model
    com.test.model.action
    Action
    BeanUtil
    com.test.model.action.user
    User
    UserEntityListener
    UserHistory

    ReplyDelete
  6. Please let me know how to set the architecture for model module such that when module-service1 is started it can set the application context in BeanUtil class under (com.test.model.action) package.

    The context is loading fine for (Module-service2) in (com.test.anotherservice.model.action.BeanUtil) and was able to see and data inserted in Consumer table as I can see audit data in ConsumerHistory.
    However, Its not working for User and when tried to insert User, I am getting NullPointerException as the context is in (com.test.model.action.BeanUtil) for User is not set.

    Hope you understand the issue I'm facing. Also I'm curious about @PostUpdate, wondering if that could help!
    Please help me!!

    ReplyDelete
  7. Hi Naresh, I apologise for long boring messages above. I have finally figured out what the issue was. The package structure need to be same, which is very important. Package structure that I had in the model module for one of the services does not match with the structure of it corresponding module.

    However, I came across another issue, I have 2 custom entitymanagers and audting the same entity. On startup, I could see 2 history tables being created. Is there a way where I can write data into single history table from two custom entitymanagers?

    ReplyDelete
    Replies
    1. I am npot sure about your exact application but I am guessing, you are running two entity manager at the same time which means you are running two application at the same time which are connecting to same database. In that case you should use `spring.jpa.hibernate.ddl-auto=update` in your application.properties file in both application.

      Delete
  8. Thanks for the response Naresh.
    I have this(spring.jpa.hibernate.ddl-auto=update) property already set in both applications.

    However,
    from one application I'm using spring default entitymanager and
    from other application I have created custom entityManagerFacoryBean) by creating the below configuration in my class
    @EnableJpaRepositories(entityManagerFactoryRef = "customServiceEntityManagerFactory", transactionManagerRef = "customServiceTransactionManager")
    @Bean(name = ["customServiceEntityManagerFactory"])
    fun customServiceEntityManagerFactory(
    builder: EntityManagerFactoryBuilder,
    @Qualifier("customServiceDataSource") dataSource: DataSource
    ): LocalContainerEntityManagerFactoryBean {
    return builder
    .dataSource(dataSource)
    .packages("com.myservice.model")
    .persistenceUnit("customservice")
    .build()
    }
    (This code is in Kotlin by the way)

    so basically, the above custom entitymanager has the package scan set to `com.myservice.model`, which is also the same for the other module.

    So, whats happening is, I'm audting an entity under that package structure `com.myservice.model.user` and upon starting both applications, I could see two tables getting created in database for history.

    for user entity, it is creating `user_history` and `userhistory` tables.

    Not sure, why its creating two history tables and how overcome this issue

    ReplyDelete
    Replies
    1. @PP, looks like one entity manager is constructing history table name as `user_history` and other as `userhistory`, the later one might be happening with your custom entitymanager.

      So you will need to modify the behaviour of our custom entity manager to put underscore in your table names, you can do that by same values to `spring.jpa.hibernate.naming.implicit-strategy` and `spring.jpa.hibernate.naming.physical-strategy` properties in both application. In case of cutom entitymanager you will need to set both properties to your entity manager manually.

      Or you can put `@Table(name = "user_history")` on the `UserHistory` entity class in both application.

      Please let me know if this solves your problem?

      Delete
  9. @Naresh, I'm guessing, your understanding is that both applications are connecting to different tables with same name(having same package structure). Actually, what I'm trying to do is to connect these two modules having
    (1 - Spring EntityManager,
    2 - Custom EntityManager)
    to the same Model, which has Just
    - User Entity Class - @Table(name = "user")
    - UserHistory Class (After making changes based on your advice, I have set this class - @Table(name . = "user_history")

    What happened after the change is,

    It created only 1 history table after starting those two modules, which is great! However, I could see additional unwanted columns getting created.

    Column Structure is like

    Id, modified_date, modified_by, modifieddate, modififedby

    So. after starting application with spring entitymanager it created modified_date, modified_by.
    After starting second application with custom entitymnanager it created additional columns (modifieddate, modififedby)
    When, I tried to edit changes with the application that has custom entitymanager - I got the below error -

    Note - I haven't tried setting (spring.jpa.hibernate.naming.implicit-strategy`,`spring.jpa.hibernate.naming.physical-strategy`) properties yet

    Caused by: javax.persistence.RollbackException: Error while committing the transaction
    at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:77) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:71) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:536) ~[spring-orm-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    ... 96 common frames omitted
    Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'javax.persistence.EntityManager' available: expected single matching bean but found 2: org.springframework.orm.jpa.SharedEntityManagerCreator#0,org.springframework.orm.jpa.SharedEntityManagerCreator#1
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1036) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:338) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:333) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1107) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]

    ReplyDelete
  10. I have added @Column(name = "modified_by") and @Column(name = "modified_date") to the variables in user_history table. Then when I restarted both the modules(applications). It worked fine, just by creating needed columns (Id, modified_date, modified_by)

    However, I stumbled across another issue, Now the below method in BeanUtil that implements ApplicationContextAware is throwing error

    public static T getBean(Class beanClass) {
    return context.getBean(beanClass);
    }

    This line `return context.getBean(beanClass);` is throwing error when I try to get the EntityManager from when tried to update from the application that has customentitymanager.

    I tried to change getBean method by sending stringname of my customEntityManager. However, wasn't able to extrat ct EntityManager instance from it.

    Below is my code in Kotlin. I tired to convert, which is not working

    beanClass - 'customEntityManager' name

    fun getBean(beanClass: String): EntityManager {
    val response = context!!.getBean(beanClass)
    val proxyFactory = ProxyFactory(response)
    proxyFactory.addInterface(FactoryBean::class.java)
    val proxy = proxyFactory.getProxy()
    val lem = proxy as LocalContainerEntityManagerFactoryBean
    (throwing error at above line as it couldn't convert to LocalContainerEntityManagerFactoryBean)
    println(response)
    return context!!.getBean(beanClass) as EntityManager
    }

    ReplyDelete
    Replies
    1. It will nice to set `spring.jpa.hibernate.naming.implicit-strategy`,`spring.jpa.hibernate.naming.physical-strategy` properties because that will solve your problem without any code change even for future entities.

      You have not mentioned which error you are getting while calling `return context.getBean(beanClass)`, but if you have marked `customEntityManager ` as `@Bean` you should not face any problem.

      Delete