Skip to content

Commit e12ed6a

Browse files
committed
Align CDI instantiation of AttributeConverters/EntityListeners without an explicit scope on the Jakarta Persistence spec
.. while preserving a sane/unsuprising (and pre-existing) behavior when explicit annotations are added. See the javadoc of `QuarkusArcBeanContainer` for details.
1 parent b831589 commit e12ed6a

17 files changed

+314
-55
lines changed

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/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: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,37 @@ 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

3333
// This can only return `theData` if Hibernate ORM correctly instantiates MyDataConverter through CDI
3434
// so that MyDataConversionService is injected.
35-
return em.find(EntityWithAttributeConverters.class, entity.getId()).getMyDataRequiringCDI().getContent();
35+
return em.find(EntityWithAttributeConverters.class, entity.getId()).getMyDataRequiringCDIExplicitScope().getContent();
36+
}
37+
38+
@GET
39+
@Path("/with-cdi-implicit-scope")
40+
@Produces(MediaType.TEXT_PLAIN)
41+
@Transactional
42+
public String withCdiImplicitScope(@RestQuery String theData) {
43+
EntityWithAttributeConverters entity = new EntityWithAttributeConverters();
44+
entity.setMyDataRequiringCDIImplicitScope(new MyDataRequiringCDI(theData));
45+
em.persist(entity);
46+
47+
em.flush();
48+
em.clear();
49+
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()).getMyDataRequiringCDIImplicitScope().getContent();
3653
}
3754

3855
@GET

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ public class EntityWithAttributeConverters {
1111
@GeneratedValue
1212
private Long id;
1313

14-
@Convert(converter = MyDataRequiringCDIConverter.class)
15-
private MyDataRequiringCDI myDataRequiringCDI;
14+
@Convert(converter = MyDataRequiringCDIExplicitScopeConverter.class)
15+
private MyDataRequiringCDI myDataRequiringCDIExplicitScope;
16+
17+
@Convert(converter = MyDataRequiringCDIImplicitScopeConverter.class)
18+
private MyDataRequiringCDI myDataRequiringCDIImplicitScope;
1619

1720
@Convert(converter = MyDataNotRequiringCDIConverter.class)
1821
private MyDataNotRequiringCDI myDataNotRequiringCDI;
@@ -25,12 +28,20 @@ public void setId(Long id) {
2528
this.id = id;
2629
}
2730

28-
public MyDataRequiringCDI getMyDataRequiringCDI() {
29-
return myDataRequiringCDI;
31+
public MyDataRequiringCDI getMyDataRequiringCDIExplicitScope() {
32+
return myDataRequiringCDIExplicitScope;
33+
}
34+
35+
public void setMyDataRequiringCDIExplicitScope(MyDataRequiringCDI myDataRequiringCDI) {
36+
this.myDataRequiringCDIExplicitScope = myDataRequiringCDI;
37+
}
38+
39+
public MyDataRequiringCDI getMyDataRequiringCDIImplicitScope() {
40+
return myDataRequiringCDIImplicitScope;
3041
}
3142

32-
public void setMyDataRequiringCDI(MyDataRequiringCDI myDataRequiringCDI) {
33-
this.myDataRequiringCDI = myDataRequiringCDI;
43+
public void setMyDataRequiringCDIImplicitScope(MyDataRequiringCDI myDataRequiringCDI) {
44+
this.myDataRequiringCDIImplicitScope = myDataRequiringCDI;
3445
}
3546

3647
public MyDataNotRequiringCDI getMyDataNotRequiringCDI() {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package io.quarkus.it.jpa.attributeconverter;
22

3+
import jakarta.enterprise.inject.Vetoed;
34
import jakarta.inject.Inject;
45
import jakarta.persistence.AttributeConverter;
56
import jakarta.persistence.Converter;
67

78
@Converter
9+
@Vetoed // We really don't want CDI for some reason.
810
public class MyDataNotRequiringCDIConverter implements AttributeConverter<MyDataNotRequiringCDI, String> {
911
// This will always be null.
1012
// It's only here to check that this class is instantiated without relying on CDI
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,25 @@
77

88
@Converter
99
@ApplicationScoped
10-
public class MyDataRequiringCDIConverter implements AttributeConverter<MyDataRequiringCDI, String> {
10+
public class MyDataRequiringCDIExplicitScopeConverter implements AttributeConverter<MyDataRequiringCDI, String> {
1111
@Inject
1212
MyCdiContext cdiContext;
1313

1414
@Override
1515
public String convertToDatabaseColumn(MyDataRequiringCDI attribute) {
16+
if (attribute == null) {
17+
return null;
18+
}
1619
MyCdiContext.checkAvailable(cdiContext);
17-
return attribute == null ? null : attribute.getContent();
20+
return attribute.getContent();
1821
}
1922

2023
@Override
2124
public MyDataRequiringCDI convertToEntityAttribute(String dbData) {
25+
if (dbData == null) {
26+
return null;
27+
}
2228
MyCdiContext.checkAvailable(cdiContext);
23-
return dbData == null ? null : new MyDataRequiringCDI(dbData);
29+
return new MyDataRequiringCDI(dbData);
2430
}
2531
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.quarkus.it.jpa.attributeconverter;
2+
3+
import jakarta.inject.Inject;
4+
import jakarta.persistence.AttributeConverter;
5+
import jakarta.persistence.Converter;
6+
7+
@Converter
8+
// No CDI scope here: it's implicit, which is allowed by the JPA spec
9+
public class MyDataRequiringCDIImplicitScopeConverter implements AttributeConverter<MyDataRequiringCDI, String> {
10+
@Inject
11+
MyCdiContext cdiContext;
12+
13+
@Override
14+
public String convertToDatabaseColumn(MyDataRequiringCDI attribute) {
15+
if (attribute == null) {
16+
return null;
17+
}
18+
MyCdiContext.checkAvailable(cdiContext);
19+
return attribute.getContent();
20+
}
21+
22+
@Override
23+
public MyDataRequiringCDI convertToEntityAttribute(String dbData) {
24+
if (dbData == null) {
25+
return null;
26+
}
27+
MyCdiContext.checkAvailable(cdiContext);
28+
return new MyDataRequiringCDI(dbData);
29+
}
30+
}

integration-tests/jpa/src/main/java/io/quarkus/it/jpa/entitylistener/CustomEntityListenersAnnotation.java

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)