Quarkus: @Inject Injection into EntityListener not working

Created on 3 Feb 2020  路  25Comments  路  Source: quarkusio/quarkus

Describe the bug
I do have JPA entities annotated with @EntityListeners.
I @Inject some CDI bean (@RequestScoped in my case but happens for other scopes, too) into the EntityListener.
The bean never gets injected, the reference is allways null.

Things I tried:

  • placing some CDI scope annotation onto the entity listener.
  • Tried different CDI scopes on the injected bean
  • Tried all variants of injection: constructor-, setter- and attribute-injection on the listener

Expected behavior
Instance of the CDI bean should get injected

Actual behavior
The CDI bean reference is allways null resulting in a NPE when accessing some attribute of the bean.

To Reproduce
See attached test case

Configuration

quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test
quarkus.datasource.driver=org.h2.Driver
quarkus.hibernate-orm.database.generation=drop-and-create

(Also happens with postgresql, drop-and-create is just for testing)

Environment (please complete the following information):

  • Output of java -version: from 8 to 11
  • Quarkus version or git rev: 1.2.0

Additional context
Interestingly, when the CDI bean I want injected into the EntityListener ist injected somewhere else (e.g. some @ApplicationScoped bean), injection works.
Possible causse: looks like Quarkus bean discovery does not regard EntityListeners as trigger for registering some bean during compile-time-initialization.

Example code:

src/main/java/...

package org.acme;

import java.util.UUID;

import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id;
import javax.persistence.Version;

@Entity
@EntityListeners(FooListener.class)
public class FooEntity {

    @Id
    private String id = UUID.randomUUID().toString();
    @Version
    private long version;
    private String data;

    // getters/setters omitted for brevity
}
package org.acme;

import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.persistence.PrePersist;

// also happens with @Dependent
public class FooListener {

    @Inject
    private FooBean fooBean;

    @PrePersist
    public void prePersist(FooEntity entity) {
        entity.setData(fooBean.pleaseDoNotCrash());
    }
}
package org.acme;

import javax.enterprise.context.RequestScoped;

@RequestScoped
public class FooBean {

    public String pleaseDoNotCrash() {
        return "Yeah!";
    }
}

src/test/java/...

package org.acme;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

@QuarkusTest
public class EntityListenerInjectionTest {

    @Inject
    private EntityManager em;

    @Test
    @Transactional
    public void shouldNotCrash() {
        FooEntity o = new FooEntity();
        em.persist(o);
    }

    @Test
    @Transactional
    public void shouldInvokeEntityListener() {
        FooEntity o = new FooEntity();
        em.persist(o);
        assertEquals("Yeah!", o.getData());
    }
}
package org.acme;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.h2.H2DatabaseTestResource;

@QuarkusTestResource(H2DatabaseTestResource.class)
public class TestResources {
}
arehibernate-orm kinbug

Most helpful comment

For all those arriving here via google:

A workaround is to create some other bean and @Inject your bean there... just so Quarkus knows it needs to register your bean for CDI injection.

@ApplicationScoped
public class JPAEntityListenerInjectionWorkaround {
  @Inject FooBean fooBean;
}

Then retrieve your bean instance in the EntityListener like this:
FooBean fooBean = CDI.current().select(FooBean.class).get();

All 25 comments

Support for JPA Listeners was not implemented yet; please see the limitations documented here:

@HonoluluHenk , may I re-classify this report as a feature request?

OMG, sorry for missing this :(

may I re-classify this report as a feature request?

sure!

For all those arriving here via google:

A workaround is to create some other bean and @Inject your bean there... just so Quarkus knows it needs to register your bean for CDI injection.

@ApplicationScoped
public class JPAEntityListenerInjectionWorkaround {
  @Inject FooBean fooBean;
}

Then retrieve your bean instance in the EntityListener like this:
FooBean fooBean = CDI.current().select(FooBean.class).get();

OMG, sorry for missing this :(

no worries at all :) I realize it's not very visible; also we could do a better job in logging a warning.

I have the same use case -> so +1 for the "user demand"

See pr #7296 for possible fix/workaround

@HonoluluHenk thanks for your contribution. However, I think that we should avoid adding the Fake* classes just to workaround this particular problem.

It should be fairly easy to identify entity listerners (i.e. scan the index for @PrePersist) and add a synthetic scope to them in a quarkus extension. The other step would be to provide a quarkus-specific listener "instantiator". Something similar to QuarkusConstructorInjector we use for RESTEasy.

Ideally, Hibernate should provide an SPI that can be implemented by quarkus. If I understand it correctly, Hibernate is currently using BeanManager to create listener instances. Is there a way to replace the current implementation with some pluggable mechanism?

@Sanne CC

I'll have a look into it...

@mkouba +1 no need for fake classes. Regarding the Hibernate SPI: to be honest I don't know this area very well, I should investigate but am a bit overwhelmed ATM.

Even if it's not possible today, it's certainly possible to change the code :) We do regular Hibernate releases to address on any Quarkus need.

@HonoluluHenk thanks! appreciate it. If you need to change any code in Hibernate ORM, feel free to take that in consideration. Ping me if you send a PR to Hibernate, I'll prioritize it. (hopefully it won't be needed?)

@HonoluluHenk Did you get any time to look into this? I am also working on a project which would require support for entity listeners / JPA callbacks.

@Sanne Any idea when the Quarkus team will get around to look into this? Possibly we need to consider finding a workaround or contributing a solution.

Any idea when the Quarkus team will get around to look into this? Possibly we need to consider finding a workaround or contributing a solution.

sorry, I don't know. It's for sure high on the wish list, but there's a couple more important things that need to be done first and it's a small team. A contribution would be great, or even if you find a reasonable workaround please describe it here for others.

@Sanne Thanks for the heads-up.

@knutwannheden For completness: the workaround from https://github.com/quarkusio/quarkus/issues/6948#issuecomment-581516291 works fine for me.

As a side-note: you need to inject your bean somewhere (it seems to neither matter where, nor if it is actually used there) for it to be available for programmatic lookup via CDI.current().

@j-be The workaround you mention appears to be working for JPA entity listeners, but I don't see it working for Envers revision listeners. Explicitly injecting a revision listener instance somewhere else doesn't help with the instantiation that is required for Envers.

To my example I have now however added an @Dependent annotation to MyRevisionListener and then also inject it into MyRevisionListenerTest. But as I mentioned, the problem still remains.

Please let me know if I missed something.

Oops. I mixed this issue up with the issue #8268 I recently reported. Please disregard the comment above.

@knutwannheden that is weird indeed. I am using the workaround in an envers RevisionListener to accees the userId from the OAuth Cookie and save it inside the revision. What I am doing is:

  • Define a @ApplicationScoped SecurityService
  • @Inject all the needed stuff there
  • @Inject SecurityService in one of my JaxRS classes
  • Use CDI in the RevisonListener

I'm not sure whether the SecurityService in between makes a difference - I implemented it for convenience only, i.e. to have everything in one place.

Btw, how does the error manifest (Build error, NullPointer on runtime, ...)?

@j-be Using CDI.current() in the RevisionListener should work as expected, but normally it should be possible to use @Inject in the RevisionListener implementation.

The problem is then that the @Inject annotated fields are null.

@knutwannheden sorry, my bad, only saw your "Oops" comment just now. Nevermind then, I didn't get @Inject to work either, but it sure would be nice to have 馃槂

I'm a little bit confused about what should work and what shouldn't. Regarding to https://quarkus.io/guides/hibernate-orm#limitations, there is no support for JPA Calllbacks. But with _1.3.2.Final_, listeners itself (e.g. a method in a @EntityListeners annotated with @PostLoad is called) seem to work in my case. The only problem seems to be the not injected bean in the listener.

Am I missing something? Is the limitation only true in native mode?

I'm a little bit confused about what should work and what shouldn't. Regarding to https://quarkus.io/guides/hibernate-orm#limitations, there is no support for JPA Calllbacks. But with _1.3.2.Final_, listeners itself (e.g. a method in a @EntityListeners annotated with @PostLoad is called) seem to work in my case. The only problem seems to be the not injected bean in the listener.

That is also what my testing confirms.

Am I missing something? Is the limitation only true in native mode?

Indeed. In native mode @EntityListeners doesn't work at all. The only thing which apparently works is if your entity class itself has methods annotated with @PrePersist, etc.

public class JpaEntityListeners {

    @PrePersist
    public void prePersist(Object entity) {
        ReflectUtil.invoke(entity, "setCreateTime", LocalDateTime.now());
        ReflectUtil.invoke(entity, "setUpdateTime", LocalDateTime.now());
        JsonWebToken context = CDI.current().select(JsonWebToken.class).get();
        if (Objects.nonNull(context)) {
            String username = context.getClaim(Claims.preferred_username.name());
            if (Objects.nonNull(username)) {
                ReflectUtil.invoke(entity, "setCreateBy", username);
                ReflectUtil.invoke(entity, "setUpdateBy", username);
            }
        }
    }

    @PreUpdate
    public void preUpdate(Object entity) {
        ReflectUtil.invoke(entity, "setUpdateTime", LocalDateTime.now());
        JsonWebToken context = CDI.current().select(JsonWebToken.class).get();
        if (Objects.nonNull(context)) {
            String username = context.getClaim(Claims.preferred_username.name());
            if (Objects.nonNull(username)) {
                ReflectUtil.invoke(entity, "setUpdateBy", username);
            }
        }
    }
}

I use JsonWebToken context = CDI.current().select(JsonWebToken.class).get();

@HonoluluHenk Have you ever gotten round to looking into this?

Unfortunately, no.
And it seems like I won't find time anytime soon :(
sigh

I have the same use case -> so +1 for the "user demand"

For all those arriving here via google:

A workaround is to create some other bean and @Inject your bean there... just so Quarkus knows it needs to register your bean for CDI injection.

@ApplicationScoped
public class JPAEntityListenerInjectionWorkaround {
  @Inject FooBean fooBean;
}

Then retrieve your bean instance in the EntityListener like this:
FooBean fooBean = CDI.current().select(FooBean.class).get();

Thank you so much, you saved me 24 hours searching for injecting Microprofile JsonWebToken to EntityListeners

I'm not quite sure what the original problem is but if the FooBean is simply removed because it's considered unused a simpler solution is to annotate the FooBean class with @io.quarkus.arc.Unremovable (such beans are never removed even if not injected anywhere in the app).

Was this page helpful?
0 / 5 - 0 ratings