Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@Inject Injection into EntityListener not working #6948

Closed
HonoluluHenk opened this issue Feb 3, 2020 · 34 comments · Fixed by #20708
Closed

@Inject Injection into EntityListener not working #6948

HonoluluHenk opened this issue Feb 3, 2020 · 34 comments · Fixed by #20708
Assignees
Labels
area/hibernate-orm Hibernate ORM kind/enhancement New feature or request
Milestone

Comments

@HonoluluHenk
Copy link

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 {
}
@HonoluluHenk HonoluluHenk added the kind/bug Something isn't working label Feb 3, 2020
@mkouba mkouba added the area/hibernate-orm Hibernate ORM label Feb 3, 2020
@Sanne
Copy link
Member

Sanne commented Feb 3, 2020

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?

@HonoluluHenk
Copy link
Author

OMG, sorry for missing this :(

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

sure!

@HonoluluHenk
Copy link
Author

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();

@Sanne
Copy link
Member

Sanne commented Feb 3, 2020

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.

@mkueng1
Copy link

mkueng1 commented Feb 11, 2020

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

@HonoluluHenk
Copy link
Author

See pr #7296 for possible fix/workaround

@mkouba
Copy link
Contributor

mkouba commented Feb 20, 2020

@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

@HonoluluHenk
Copy link
Author

I'll have a look into it...

@Sanne
Copy link
Member

Sanne commented Feb 21, 2020

@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?)

@knutwannheden
Copy link
Contributor

@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.

@Sanne
Copy link
Member

Sanne commented Mar 26, 2020

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.

@knutwannheden
Copy link
Contributor

@Sanne Thanks for the heads-up.

@j-be
Copy link
Contributor

j-be commented Mar 31, 2020

@knutwannheden For completness: the workaround from #6948 (comment) 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().

@knutwannheden
Copy link
Contributor

knutwannheden commented Mar 31, 2020

@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.

@j-be
Copy link
Contributor

j-be commented Mar 31, 2020

@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, ...)?

@knutwannheden
Copy link
Contributor

@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.

@j-be
Copy link
Contributor

j-be commented Mar 31, 2020

@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 😃

@wicksim
Copy link

wicksim commented Apr 9, 2020

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?

@knutwannheden
Copy link
Contributor

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.

@SoftMeng
Copy link

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();

@famod
Copy link
Member

famod commented Aug 3, 2020

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

@HonoluluHenk
Copy link
Author

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

@Maicon96
Copy link

Maicon96 commented Oct 7, 2020

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

@thangthan
Copy link

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

@mkouba
Copy link
Contributor

mkouba commented Oct 19, 2020

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).

@eliaharm
Copy link

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();

Dear @SoftMeng
That didn't work for me when persisting an object with REST resources for Hibernate ORM with Panache
it is returning null

@j-be
Copy link
Contributor

j-be commented Jan 24, 2021

@eliaharm I think you still need to @Inject JsonWebToken somewhere (it doesn't really matter where or if it is used there, as long as the bean injecting it is used, or marked as @io.quarkus.arc.Unremovable).

I think Quarkus is like "you don't use the bean? I'll not pack it then" on buildtime, and CDI.current() lookup alone do not classify as "use".

@ghost
Copy link

ghost commented Mar 23, 2021

+1

@TheParad0X
Copy link

I am facing the same issue. What is currently the easiest way to inject a bean into my Entitylistener (that will also work in native mode) ? As most users here, I want to automatically update the created_by / updated_by column of my entities.

@j-be
Copy link
Contributor

j-be commented Aug 31, 2021

@TheParad0X CDI.current().select(FooBean.class).get(); still works fine for me in Quarkus 2.1. Just make sure the bean is injected somewhere else, or that it carries @io.quarkus.arc.Unremovable.

@yrodiere yrodiere added kind/enhancement New feature or request and removed kind/bug Something isn't working labels Oct 11, 2021
@yrodiere yrodiere self-assigned this Oct 11, 2021
@yrodiere yrodiere linked a pull request Nov 2, 2021 that will close this issue
@yrodiere
Copy link
Member

yrodiere commented Nov 2, 2021

Fixed by #20708

@yrodiere yrodiere closed this as completed Nov 2, 2021
@yrodiere yrodiere added this to the 2.5 - main milestone Nov 2, 2021
cbianco added a commit to smclab/openk9 that referenced this issue Feb 9, 2022
@Felk
Copy link
Contributor

Felk commented Mar 15, 2022

Is this actually fixed? I am using Quarkus 2.7.1 and wrote an entity listener with @ApplicationScoped that @Injects an EntityManager. The injected instance stays null, but doing CDI.current().select(EntityManager.class).get(); works.

@yrodiere
Copy link
Member

CDI injection in entity listeners does work, at least in the general case: https://github.com/quarkusio/quarkus/pull/20708/files#diff-7ed082417d8730c92d0dcf5c59e547878a5c298311afbd529f1a5c9591f150ccR73

I suppose injecting an entity manager into a component that is itself used by the entity manager could lead to cyclic dependency problems. If you do have problems, please open an issue with a reproducer, and we'll have a look.

@Felk
Copy link
Contributor

Felk commented Mar 16, 2022

@yrodiere Thanks, I was able to narrow the problem down a bit and opened #24340, though it is trivially worked around (don't have your listener methods be private).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/hibernate-orm Hibernate ORM kind/enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.