Skip to content

Commit 021b167

Browse files
authored
Merge pull request #50489 from yrodiere/i50470
Align CDI instantiation of AttributeConverters/EntityListeners without an explicit scope on the Jakarta Persistence spec
2 parents b831589 + 5f7d181 commit 021b167

File tree

29 files changed

+828
-145
lines changed

29 files changed

+828
-145
lines changed

docs/src/main/asciidoc/hibernate-orm.adoc

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,10 @@ using the `@PersistenceUnit` annotation at the class level is not supported in t
464464

465465
Note that, similarly to what we do with the configuration property, we take into account the annotated package but also all its subpackages.
466466

467-
==== CDI integration
467+
=== CDI integration
468+
469+
[[injecting-entry-points]]
470+
==== Injecting entry points
468471

469472
If you are familiar with using Hibernate ORM in Quarkus, you probably already have injected the `EntityManager` using CDI:
470473

@@ -540,6 +543,40 @@ These components can also be injected with a specific persistence unit qualifier
540543
CriteriaBuilder criteriaBuilder;
541544
----
542545

546+
[[plugging-in-converters-entity-listeners]]
547+
==== Plugging in converters and entity listeners
548+
549+
The Quarkus extension for Hibernate ORM supports injecting link:{hibernate-orm-docs-url}#basic-jpa-convert[attribute converters] and link:{hibernate-orm-docs-url}#events-jpa-callbacks[entity listeners] into Hibernate ORM.
550+
551+
Just use them as you would in vanilla Hibernate ORM (see documentation linked above), and rely on CDI features (`@Inject`, `@PostConstruct`, `@PreDestroy`, ...) in the converter/listener implementation as needed.
552+
553+
When no CDI scope is specified, the converter/listener will behave as if it was `@Dependent` and will be instantiated once per persistence unit it's used in.
554+
555+
You can force a CDI scope by annotating the converter/listener class with for example `@ApplicationScoped`.
556+
557+
In very exotic scenarios, if you need to prevent usage of CDI in a converter/listener even though it uses CDI annotations (`@Inject`, ...), annotate it with `@Vetoed`. The class will then be instantiated by Hibernate ORM through its default constructor.
558+
559+
[[plugging-in-other-custom-components]]
560+
==== Plugging in other custom components
561+
562+
The Quarkus extension for Hibernate ORM will automatically
563+
inject components annotated with `@PersistenceUnitExtension` into Hibernate Search.
564+
565+
The annotation can optionally target a specific persistence unit with `@PersistenceUnitExtension(name = "nameOfYourPU")`.
566+
567+
This feature is available for the following component types:
568+
569+
`org.hibernate.Interceptor`::
570+
See <<interceptors>>.
571+
`org.hibernate.resource.jdbc.spi.StatementInspector`::
572+
See <<statement_inspectors>>.
573+
`org.hibernate.type.format.FormatMapper`::
574+
See <<json_xml_serialization_deserialization>>.
575+
`io.quarkus.hibernate.orm.runtime.tenant.TenantResolver`::
576+
See <<multitenancy>>.
577+
`io.quarkus.hibernate.orm.runtime.tenant.TenantConnectionResolver`::
578+
See <<programmatically-resolving-tenants-connections>>.
579+
543580
[[persistence-unit-active]]
544581
=== Activate/deactivate persistence units
545582

@@ -1473,6 +1510,7 @@ quarkus.datasource.db-kind=postgresql <2>
14731510
<1> Enable discriminator multi-tenancy.
14741511
<2> xref:datasource.adoc[Configure the datasource].
14751512

1513+
[[programmatically-resolving-tenants-connections]]
14761514
=== Programmatically Resolving Tenants Connections
14771515

14781516
If you need a more dynamic configuration for the different tenants you want to support and don't want to end up with multiple entries in your configuration file,

extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import io.quarkus.arc.ActiveResult;
4040
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
4141
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
42+
import io.quarkus.arc.deployment.AutoAddScopeBuildItem;
4243
import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem;
4344
import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem;
4445
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
@@ -226,6 +227,7 @@ void generateHibernateBeans(HibernateOrmRecorder recorder,
226227

227228
@BuildStep
228229
void registerBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans,
230+
BuildProducer<AutoAddScopeBuildItem> autoAddScope,
229231
BuildProducer<UnremovableBeanBuildItem> unremovableBeans,
230232
Capabilities capabilities,
231233
List<PersistenceUnitDescriptorBuildItem> descriptors,
@@ -250,9 +252,26 @@ void registerBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans,
250252
.addBeanClasses(unremovableClasses.toArray(new Class<?>[unremovableClasses.size()]))
251253
.build());
252254

253-
// Some user-injectable beans are retrieved programmatically and shouldn't be removed
254-
unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(AttributeConverter.class));
255+
// For AttributeConverters and EntityListeners (which are all listed in getPotentialCdiBeanClassNames),
256+
// we want to achieve the behavior described in the javadoc of QuarkusArcBeanContainer.
257+
// In particular:
258+
// 1. They may be retrieved dynamically by Hibernate ORM, so if they are CDI beans, they should not be removed.
259+
// NOTE: We don't use .unremovable on AutoAddScopeBuildItem, because that would only make beans unremovable
260+
// when we automatically add a scope.
255261
unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(jpaModel.getPotentialCdiBeanClassNames()));
262+
// TODO: Remove, this is there for backwards compatibility.
263+
// It should only have an effect in edge cases where an attribute converter was imported from
264+
// a library not indexed in Jandex, but it's doubtful the attribute converter would work in that case.
265+
unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(AttributeConverter.class));
266+
// 2. Per spec, they may be handled as CDI beans even if they don't have a user-declared scope,
267+
// so we need them to default to @Dependent-scoped beans.
268+
// See https://github.com/quarkusio/quarkus/issues/50470
269+
autoAddScope.produce(AutoAddScopeBuildItem.builder()
270+
.match((clazz, annotations, index) -> jpaModel.getPotentialCdiBeanClassNames().contains(clazz.name()))
271+
.defaultScope(BuiltinScope.DEPENDENT)
272+
// ... but if they don't use CDI, we can safely default to instantiating in Hibernate ORM through reflection.
273+
.requiresContainerServices()
274+
.build());
256275
}
257276

258277
@BuildStep

extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ public JpaModelBuildItem discoverModelAndRegisterForReflection() throws BuildExc
142142
}
143143
}
144144

145+
// Hibernate ORM will fall back to instantiating these types using reflection if they are not CDI beans,
146+
// so we need to enable that.
147+
for (DotName javaType : collector.potentialCdiBeanTypes) {
148+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(javaType.toString()).constructors().build());
149+
}
150+
145151
return new JpaModelBuildItem(collector.packages, collector.entityTypes, managedClassNames,
146152
collector.potentialCdiBeanTypes, collector.xmlMappingsByPU);
147153
}

extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/cdi/QuarkusArcBeanContainer.java

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@
1111
import org.hibernate.resource.beans.container.spi.ContainedBeanImplementor;
1212
import org.hibernate.resource.beans.spi.BeanInstanceProducer;
1313

14+
/**
15+
* A {@link org.hibernate.resource.beans.container.spi.BeanContainer} that works with other build-time elements in Quarkus to
16+
* achieve a behavior that is as unsurprising as possible, while complying with the Jakarta Persistence spec where possible.
17+
* The effective behavior is as follows:
18+
* <ol>
19+
* <li>When a class is annotated with a scope, such as <code>@ApplicationScoped</code> or <code>@Dependent</code>, we will
20+
* comply with that (with some gotchas, see last item).
21+
* This is not in line with the behavior from the Jakarta Persitence spec, which would ignore explicit scopes.
22+
* <li>When a class is NOT annotated with any scope, it will behave as a <code>@Dependent</code> CDI bean,
23+
* at least for attribute converters and entity listeners (with some gotchas, see last item).
24+
* This is in line with what the Jakarta Persistence spec requires.
25+
* <li>Regardless of the above, when a class is annotated with @Vetoed, it will never be instantiated through CDI, but rather
26+
* through Hibernate ORM's reflection instantiation.
27+
* This may or may not be in line with the behavior described in the Jakarta Persistence spec -- I did not check -- but
28+
* seems sensible and intuitive enough.
29+
* <li>Hibernate ORM's internal behavior means components may be cached at the persistence unit level,
30+
* so even for <code>@Dependent</code> components, there would be at most one instance per persistence unit.
31+
* TODO: this is what we've always done and what we assume in tests, but is this what we want?
32+
* <p>
33+
* Note this behavior is only possible because we give attribute converters and entity listeners the dependent scope by default:
34+
* see {@code io.quarkus.hibernate.orm.deployment.HibernateOrmCdiProcessor#registerBeans}.
35+
*/
1436
@Singleton
1537
public class QuarkusArcBeanContainer extends AbstractCdiBeanContainer {
1638

@@ -25,18 +47,15 @@ public BeanManager getUsableBeanManager() {
2547
@Override
2648
public <B> ContainedBean<B> getBean(Class<B> beanType, LifecycleOptions lifecycleOptions,
2749
BeanInstanceProducer fallbackProducer) {
28-
// We can only support these lifecycle options. See QuarkusBeanContainerLifecycleOptions.
29-
// Usually that's what we get passed (when using QuarkusManagedBeanRegistry),
30-
// but in some cases Hibernate ORM calls the bean cotnainer directly and bypasses the registry,
31-
// so we need to override options in that case.
32-
return super.getBean(beanType, QuarkusBeanContainerLifecycleOptions.INSTANCE, fallbackProducer);
50+
// Overriding lifecycle options; see QuarkusBeanContainerLifecycleOptions.
51+
return super.getBean(beanType, QuarkusBeanContainerLifecycleOptions.of(lifecycleOptions), fallbackProducer);
3352
}
3453

3554
@Override
3655
public <B> ContainedBean<B> getBean(String beanName, Class<B> beanType, LifecycleOptions lifecycleOptions,
3756
BeanInstanceProducer fallbackProducer) {
38-
// Overriding lifecycle options; see comments in the other getBean() method.
39-
return super.getBean(beanName, beanType, QuarkusBeanContainerLifecycleOptions.INSTANCE, fallbackProducer);
57+
// Overriding lifecycle options; see QuarkusBeanContainerLifecycleOptions.
58+
return super.getBean(beanName, beanType, QuarkusBeanContainerLifecycleOptions.of(lifecycleOptions), fallbackProducer);
4059
}
4160

4261
@Override

extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/cdi/QuarkusBeanContainerLifecycleOptions.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,50 @@
22

33
import org.hibernate.resource.beans.container.spi.BeanContainer;
44

5+
/**
6+
* An override of lifecycle options covering what can actually be supported in Quarkus.
7+
* <p>
8+
* Usually the {@link #DEFAULT} is used (when using {@link QuarkusManagedBeanRegistry}),
9+
* but in some cases Hibernate ORM calls the bean container directly and bypasses the registry,
10+
* so we need to override options in that case -- see {@link #of(BeanContainer.LifecycleOptions)}.
11+
*/
512
final class QuarkusBeanContainerLifecycleOptions implements BeanContainer.LifecycleOptions {
6-
public static final QuarkusBeanContainerLifecycleOptions INSTANCE = new QuarkusBeanContainerLifecycleOptions();
13+
private static final QuarkusBeanContainerLifecycleOptions WITH_CACHE = new QuarkusBeanContainerLifecycleOptions(true);
14+
private static final QuarkusBeanContainerLifecycleOptions WITHOUT_CACHE = new QuarkusBeanContainerLifecycleOptions(false);
715

8-
private QuarkusBeanContainerLifecycleOptions() {
16+
// Caching is the default in Hibernate ORM, see ManagedBeanRegistryImpl#canUseCachedReferences.
17+
public static final QuarkusBeanContainerLifecycleOptions DEFAULT = WITH_CACHE;
18+
19+
public static QuarkusBeanContainerLifecycleOptions of(BeanContainer.LifecycleOptions options) {
20+
if (options.canUseCachedReferences()) {
21+
return WITH_CACHE;
22+
} else {
23+
return WITHOUT_CACHE;
24+
}
25+
}
26+
27+
private final boolean cache;
28+
29+
private QuarkusBeanContainerLifecycleOptions(boolean cache) {
30+
this.cache = cache;
931
}
1032

1133
@Override
1234
public boolean useJpaCompliantCreation() {
1335
// Arc doesn't support all the BeanManager methods required to implement JPA-compliant bean creation.
14-
// Anyway, JPA-compliant bean creation means we completely disregard the scope of beans
36+
37+
// In any case, JPA-compliant bean creation means we completely disregard the scope of beans
1538
// (e.g. @Dependent, @ApplicationScoped), which doesn't seem wise.
16-
// So we're probably better off this way.
39+
// What we do instead in Quarkus is:
40+
// 1. Disable JPA-compliant creation, so we look up CDI beans and fall back to reflection if there is none.
41+
// 2. Add a default scope to relevant bean types -- see io.quarkus.hibernate.orm.deployment.HibernateOrmCdiProcessor.registerBeans
42+
// In effect, this gives us scope-compliant creation when classes are annotated with CDI scopes,
43+
// and spec-compliant creation when they are not.
1744
return false;
1845
}
1946

2047
@Override
2148
public boolean canUseCachedReferences() {
22-
// Let Arc do the caching based on scopes
23-
return false;
49+
return cache;
2450
}
2551
}

extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/cdi/QuarkusManagedBeanRegistry.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
/**
1313
* A replacement for ManagedBeanRegistryImpl that:
1414
* <ul>
15-
* <li>forces the use of QuarkusManagedBeanRegistry,
15+
* <li>forces the use of {@link QuarkusArcBeanContainer},
1616
* which works with Arc and respects configured scopes when instantiating CDI beans.</li>
1717
* <li>is not stoppable and leaves the release of beans to {@link QuarkusArcBeanContainer},
1818
* so that the bean container and its beans can be reused between static init and runtime init,
@@ -47,15 +47,15 @@ public BeanContainer getBeanContainer() {
4747
@Override
4848
public <T> ManagedBean<T> getBean(Class<T> beanClass, BeanInstanceProducer fallbackBeanInstanceProducer) {
4949
return new ContainedBeanManagedBeanAdapter<>(beanClass,
50-
beanContainer.getBean(beanClass, QuarkusBeanContainerLifecycleOptions.INSTANCE,
50+
beanContainer.getBean(beanClass, QuarkusBeanContainerLifecycleOptions.DEFAULT,
5151
fallbackBeanInstanceProducer));
5252
}
5353

5454
@Override
5555
public <T> ManagedBean<T> getBean(String beanName, Class<T> beanContract,
5656
BeanInstanceProducer fallbackBeanInstanceProducer) {
5757
return new ContainedBeanManagedBeanAdapter<>(beanContract,
58-
beanContainer.getBean(beanName, beanContract, QuarkusBeanContainerLifecycleOptions.INSTANCE,
58+
beanContainer.getBean(beanName, beanContract, QuarkusBeanContainerLifecycleOptions.DEFAULT,
5959
fallbackBeanInstanceProducer));
6060
}
6161

integration-tests/jpa/src/main/java/io/quarkus/it/jpa/attributeconverter/AttributeConverterResource.java

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,66 @@ public class AttributeConverterResource {
1919
EntityManager em;
2020

2121
@GET
22-
@Path("/with-cdi")
22+
@Path("/with-cdi-explicit-scope")
2323
@Produces(MediaType.TEXT_PLAIN)
2424
@Transactional
25-
public String withCdi(@RestQuery String theData) {
25+
public String withCdiExplicitScope(@RestQuery String theData) {
2626
EntityWithAttributeConverters entity = new EntityWithAttributeConverters();
27-
entity.setMyDataRequiringCDI(new MyDataRequiringCDI(theData));
27+
entity.setMyDataRequiringCDIExplicitScope(new MyDataRequiringCDI(theData));
2828
em.persist(entity);
2929

3030
em.flush();
3131
em.clear();
3232

33-
// This can only return `theData` if Hibernate ORM correctly instantiates MyDataConverter through CDI
34-
// so that MyDataConversionService is injected.
35-
return em.find(EntityWithAttributeConverters.class, entity.getId()).getMyDataRequiringCDI().getContent();
33+
// This can only return `theData` if Hibernate ORM correctly instantiates the converter through CDI.
34+
return em.find(EntityWithAttributeConverters.class, entity.getId()).getMyDataRequiringCDIExplicitScope().getContent();
3635
}
3736

3837
@GET
39-
@Path("/without-cdi")
38+
@Path("/with-cdi-implicit-scope")
4039
@Produces(MediaType.TEXT_PLAIN)
4140
@Transactional
42-
public String withoutCdi(@RestQuery String theData) {
41+
public String withCdiImplicitScope(@RestQuery String theData) {
4342
EntityWithAttributeConverters entity = new EntityWithAttributeConverters();
44-
entity.setMyDataNotRequiringCDI(new MyDataNotRequiringCDI(theData));
43+
entity.setMyDataRequiringCDIImplicitScope(new MyDataRequiringCDI(theData));
4544
em.persist(entity);
4645

4746
em.flush();
4847
em.clear();
4948

50-
// This can only return `theData` if Hibernate ORM correctly instantiates MyDataConverter through CDI
51-
// so that MyDataConversionService is injected.
52-
return em.find(EntityWithAttributeConverters.class, entity.getId()).getMyDataNotRequiringCDI().getContent();
49+
// This can only return `theData` if Hibernate ORM correctly instantiates the converter through CDI.
50+
return em.find(EntityWithAttributeConverters.class, entity.getId()).getMyDataRequiringCDIImplicitScope().getContent();
51+
}
52+
53+
@GET
54+
@Path("/without-cdi-no-injection")
55+
@Produces(MediaType.TEXT_PLAIN)
56+
@Transactional
57+
public String withoutCdiNoInjection(@RestQuery String theData) {
58+
EntityWithAttributeConverters entity = new EntityWithAttributeConverters();
59+
entity.setMyDataNotRequiringCDINoInjection(new MyDataNotRequiringCDI(theData));
60+
em.persist(entity);
61+
62+
em.flush();
63+
em.clear();
64+
65+
// This can only return `theData` if Hibernate ORM correctly instantiates the converter through reflection.
66+
return em.find(EntityWithAttributeConverters.class, entity.getId()).getMyDataNotRequiringCDINoInjection().getContent();
67+
}
68+
69+
@GET
70+
@Path("/without-cdi-vetoed")
71+
@Produces(MediaType.TEXT_PLAIN)
72+
@Transactional
73+
public String withoutCdiVetoed(@RestQuery String theData) {
74+
EntityWithAttributeConverters entity = new EntityWithAttributeConverters();
75+
entity.setMyDataNotRequiringCDIVetoed(new MyDataNotRequiringCDI(theData));
76+
em.persist(entity);
77+
78+
em.flush();
79+
em.clear();
80+
81+
// This can only return `theData` if Hibernate ORM correctly instantiates the converter through reflection.
82+
return em.find(EntityWithAttributeConverters.class, entity.getId()).getMyDataNotRequiringCDIVetoed().getContent();
5383
}
5484
}

0 commit comments

Comments
 (0)