3

I am trying to do the following inside a spring bean:

@PostConstruct
public void registerTorchEntityListeners()
{
    Session session = entityManager.unwrap(Session.class);
    for (EntityType<?> entity : entityManager.getMetamodel().getEntities())
    {
        if (entity.getJavaType().isAnnotationPresent(TorchEntityListeners.class))
        {
             TorchEntityListeners annotation = (TorchEntityListeners) entity.getJavaType().getAnnotation(TorchEntityListeners.class);
             for (Class listenerClass : annotation.value())
             {
                 Map<String, DescriptorEventListener> map = applicationContext.getBeansOfType(listenerClass);
                 for (DescriptorEventListener listenerBean : map.values())
                 {
                     session.getClassDescriptor(entity.getClass()).getEventManager().addListener(listenerBean);
                 }
             }
        }
    }

}

The problem is I get the following exception because (I think) I am not in a transaction and therefore do not have a session available to grab the ClassDescriptor so that I can add a listener to a particular entity:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'torchEntityListenerConfigurer': Invocation of init method failed; nested exception is java.lang.IllegalStateException: No transactional EntityManager available
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:133)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:396)

Basically I am trying to do the EclipseLink equivalent of this: http://invariantproperties.com/2013/09/29/spring-injected-beans-in-jpa-entitylisteners/. I would prefer to annotate the entity with the listener rather than doing something like this: Injecting a Spring dependency into a JPA EntityListener.

Thoughts?

Community
  • 1
  • 1
testing123
  • 11,367
  • 10
  • 47
  • 61

2 Answers2

3

Of course I figure it out 30 minutes after I add a bounty :)

I finally got this to work by getting the entityManager from a wired in EntityManagerFactory instead of using: @PersistenceContext to inject it into the TorchEntityListenerConfigurer

Here is the working solution...and it works great!

Here is the config:

<bean id="approvalEntityListener" class="com.prometheus.torchlms.core.activity.approval.ApprovalEntityListener">
    <property name="activityRepository" ref="activityRepository" />
    <property name="notificationFactory" ref="notificationFactory" />
    <property name="notificationService" ref="notificationService" />
</bean>
<bean id="springEntityListenerConfigurer" class="com.prometheus.torchlms.core.SpringEntityListenerConfigurer">
    <constructor-arg ref="entityManagerFactory" />
</bean>

Here is where the magic happens (in case this is useful to someone):

public class SpringEntityListenerConfigurer implements ApplicationContextAware
{
    private ApplicationContext applicationContext;
    private EntityManagerFactory entityManagerFactory;

    public SpringEntityListenerConfigurer(EntityManagerFactory entityManagerFactory)
    {
        this.entityManagerFactory = entityManagerFactory;
    }

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

    @PostConstruct
    public void registerTorchEntityListeners()
    {
        //entityManager.
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        Session session = entityManager.unwrap(Session.class);
        for (EntityType<?> entity : entityManagerFactory.getMetamodel().getEntities())
        {
            if (entity.getJavaType().isAnnotationPresent(SpringEntityListeners.class))
            {
                 SpringEntityListeners annotation = (SpringEntityListeners) entity.getJavaType().getAnnotation(SpringEntityListeners.class);
                 for (Class listenerClass : annotation.value())
                 {
                     Map<String, DescriptorEventListener> map = applicationContext.getBeansOfType(listenerClass);
                     for (DescriptorEventListener listenerBean : map.values())
                     {
                         ClassDescriptor classDescriptor = session.getClassDescriptor(entity.getJavaType());
                         if (null != classDescriptor)
                         {
                             classDescriptor.getEventManager().addListener(listenerBean);
                         }
                     }
                 }
            }
        }

    }
}

So now I can add my @SpringEntityListeners({ApprovalEntityListener.class}) to any entity I want with any listener that I want and those listeners can be spring beans!

In case it helps here's the annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SpringEntityListeners
{
    Class<?>[] value();
}
testing123
  • 11,367
  • 10
  • 47
  • 61
  • Just a complement. You do not need to get an `EntityManager` : you can simply write `for (EntityType> entity : entityManagerFactory.getMetamodel().getEntities())` – Serge Ballesta Aug 13 '14 at 14:37
  • @SergeBallesta Great feedback. I will update the answer. I still need the EntityManager in order to get the Session so I can get the ClassDescriptor. Are you aware of another way to get a hold of that? – testing123 Aug 14 '14 at 03:24
2

In fact you can do your registrations without getting an EntityManager, because you only use it to get a client session, and you only use the client session to register listeners with the server session without any direct interaction with the database nor changing any object.

The EntityManagerFactory is actually an EntityManagerFactoryImpl that can directly expose the ServerSession with unwrap. I had to dig through classes explicitly marked as INTERNAL, to find that, and the ServerSession (also marked as INTERNAL) explicitely states in his javadoc : All changes to objects and the database must be done through a unit of work acquired from the client session, this allows the changes to occur in a transactional object space and under a exclusive database connection.

But for your use case, I think it is correct to use it like that, using the server session only to get access to the Project that actually contains the class descriptors and is a public class in EclipseLink :

public void registerTorchEntityListeners()
{
    // no entityManager here and Session is only used to get access to Project
    Project proj = entityManagerFactory.unwrap(Session.class).getProject();
    for (EntityType<?> entity : entityManagerFactory.getMetamodel().getEntities())
    {
        if (entity.getJavaType().isAnnotationPresent(SpringEntityListeners.class))
        {
             SpringEntityListeners annotation = (SpringEntityListeners) entity.getJavaType().getAnnotation(SpringEntityListeners.class);
             for (Class listenerClass : annotation.value())
             {
                 Map<String, DescriptorEventListener> map = applicationContext.getBeansOfType(listenerClass);
                 for (DescriptorEventListener listenerBean : map.values())
                 {
                     ClassDescriptor classDescriptor = proj.getClassDescriptor(entity.getJavaType());
                     if (null != classDescriptor)
                     {
                         classDescriptor.getEventManager().addListener(listenerBean);
                     }
                 }
             }
        }
    }
}
KSev
  • 1,859
  • 1
  • 24
  • 23
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252