Sunday, August 26, 2012

Dependency Injection on Stateless Beans

I recently came across a situation where I needed to inject a dependency into a class that, when used, was expected to be stateless. I'm using Spring as the dependency injection container, and this class is instantiated directly from the class, and not through Spring context; thus, I could not use Spring context with basic dependency injection. Did my Google search, but did not find an answer, so I'm posting this because the solution was fairly simple and needed to be shared.

The scenario (assuming you're familiarized with JPA, Spring, and overall dependency injection) 
I have a utility class that is responsible for indexing entities using Lucene. I wanted to add this utility to a JPA listener so that it can create or update the index on the entity in a @PrePersist method. Why this way, and not through a service? Good question. The entity could be persisted directly through a service, directly using Entity Manager, or as a cascade effect when persisting an entity that contains this entity as a property. So, in order to ensure that the index is created/updated, I wanted to provide a listener that would do that prior to saving the entity.

The service class that creates/updates indexes needs to be instantiated by Spring context so that it can have all the indexing dependencies, such as the location of the indexes, so if I wanted a Listener to do this task, I needed to inject this instantiated bean by Spring context into the listener. But, JPA listeners are supposed to be stateless, Entity Manager does not call Spring context to get a reference of an instantiated listener, rather it uses the class specified in the @EntityListeners annotation and instantiates it directly. So, what to do?

Solution
If the search service, the service that does the indexing as well, can be declared as static in the Listener, surely it will be accessible when JPA executes a persist of the entity by any method.


@Component
public class EntityIndexListener
{
    static private SearchService searchService;
    ...




Declaring static is part of the solution, we still need to inject the instantiated class by Spring context to it. For this we create a set method, the same as we would as for any other dependency.


@Autowired(required = true)
@Qualifier("searchService")
public void setSearchService(SearchService searchService)
{
    this.searchService = searchService;
}

This will setup the dependency, assuming autowire is configured. If autowire is not configured, you can define the dependency explicitly. 

One last thing; to ensure that Spring does the injection at startup, declare an init method solely for the purpose of "telling" Spring that this method needs to be instantiated at startup, so that this method can be executed. The init method does not need to do anything.

@PostConstruct
public void init()
{
    logger.info("Initializing Search Service for Listener ["+ searchService +"]");
}

Hope this helps.

8 comments:

  1. Thank you! Thank you! Thank you! Thank you! Thank you!

    Have been struggling with this for the last hours and finally found your solutions that works flawless.

    Not the most intuitive approach, but hey, it works :)

    Best regard,
    Martin

    ReplyDelete
  2. This doesn't work for me. How could this work anyway since your setter has the "this.searchService" pointing to a static private member? This doesn't compile...

    ReplyDelete
    Replies
    1. oh, well replace this by the class name... works for me well.

      Delete
  3. Hi, your solution does not work for me either. The method annotated with @Autowired is never called, so as the @PostConstruct method. Is there any other Spring or JPA configuration that has to be set up? Could you please provide a complete working example (just the minimalistic one) ?
    Your solution looks promising, I could remove a lot of workaround ugly code if it worked. Thank you.

    ReplyDelete
    Replies
    1. include package of EntityIndexListener in the component-scan. of course @Autowired doesn't work without it.

      Delete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Thank you so much. Just an update:

    @Autowired(required = true)
    @Autowired(required = true)
    @Qualifier("searchService")
    public void setSearchService(SearchService searchService)
    {
    this.searchService = searchService;
    }

    Here, in @Autowire, "required = true" is optional. As the default for "@Autowire" is true :)

    ReplyDelete