Skip to content

Commit 5e1b5af

Browse files
committed
Register hints for types exposed via PersistenceManagedTypes
Add binding hints for managed types and hints for @EntityListeners, @IdClass and @converter. Closes gh-29096
1 parent d373435 commit 5e1b5af

File tree

7 files changed

+261
-2
lines changed

7 files changed

+261
-2
lines changed

spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,25 @@
2121

2222
import javax.lang.model.element.Modifier;
2323

24+
import jakarta.persistence.Converter;
25+
import jakarta.persistence.EntityListeners;
26+
import jakarta.persistence.IdClass;
27+
2428
import org.springframework.aot.generate.GeneratedMethod;
2529
import org.springframework.aot.generate.GenerationContext;
30+
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
31+
import org.springframework.aot.hint.MemberCategory;
32+
import org.springframework.aot.hint.RuntimeHints;
2633
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
2734
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
2835
import org.springframework.beans.factory.aot.BeanRegistrationCode;
2936
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
3037
import org.springframework.beans.factory.support.RegisteredBean;
38+
import org.springframework.core.annotation.AnnotationUtils;
3139
import org.springframework.javapoet.CodeBlock;
3240
import org.springframework.javapoet.ParameterizedTypeName;
3341
import org.springframework.lang.Nullable;
42+
import org.springframework.util.ClassUtils;
3443

3544
/**
3645
* {@link BeanRegistrationAotProcessor} implementations for persistence managed
@@ -40,6 +49,7 @@
4049
* and replaced by a hard-coded list of managed class names and packages.
4150
*
4251
* @author Stephane Nicoll
52+
* @author Sebastien Deleuze
4353
* @since 6.0
4454
*/
4555
class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
@@ -60,6 +70,8 @@ private static class JpaManagedTypesBeanRegistrationCodeFragments extends BeanRe
6070

6171
private final RegisteredBean registeredBean;
6272

73+
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
74+
6375
public JpaManagedTypesBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments,
6476
RegisteredBean registeredBean) {
6577
super(codeFragments);
@@ -73,6 +85,7 @@ public CodeBlock generateInstanceSupplierCode(GenerationContext generationContex
7385
boolean allowDirectSupplierShortcut) {
7486
PersistenceManagedTypes persistenceManagedTypes = this.registeredBean.getBeanFactory()
7587
.getBean(this.registeredBean.getBeanName(), PersistenceManagedTypes.class);
88+
contributeHints(generationContext.getRuntimeHints(), persistenceManagedTypes.getManagedClassNames());
7689
GeneratedMethod generatedMethod = beanRegistrationCode.getMethods()
7790
.add("getInstance", method -> {
7891
Class<?> beanType = PersistenceManagedTypes.class;
@@ -93,5 +106,43 @@ private CodeBlock toCodeBlock(List<String> values) {
93106
return CodeBlock.join(values.stream().map(value -> CodeBlock.of("$S", value)).toList(), ", ");
94107
}
95108

109+
private void contributeHints(RuntimeHints hints, List<String> managedClassNames) {
110+
for (String managedClassName : managedClassNames) {
111+
try {
112+
Class<?> managedClass = ClassUtils.forName(managedClassName, null);
113+
this.bindingRegistrar.registerReflectionHints(hints.reflection(), managedClass);
114+
contributeEntityListenersHints(hints, managedClass);
115+
contributeIdClassHints(hints, managedClass);
116+
contributeConverterHints(hints, managedClass);
117+
}
118+
catch (ClassNotFoundException ex) {
119+
throw new IllegalArgumentException("Failed to instantiate the managed class: " + managedClassName, ex);
120+
}
121+
}
122+
}
123+
124+
private void contributeEntityListenersHints(RuntimeHints hints, Class<?> managedClass) {
125+
EntityListeners entityListeners = AnnotationUtils.findAnnotation(managedClass, EntityListeners.class);
126+
if (entityListeners != null) {
127+
for (Class<?> entityListener : entityListeners.value()) {
128+
hints.reflection().registerType(entityListener, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
129+
}
130+
}
131+
}
132+
133+
private void contributeIdClassHints(RuntimeHints hints, Class<?> managedClass) {
134+
IdClass idClass = AnnotationUtils.findAnnotation(managedClass, IdClass.class);
135+
if (idClass != null) {
136+
this.bindingRegistrar.registerReflectionHints(hints.reflection(), idClass.value());
137+
}
138+
}
139+
140+
private void contributeConverterHints(RuntimeHints hints, Class<?> managedClass) {
141+
Converter converter = AnnotationUtils.findAnnotation(managedClass, Converter.class);
142+
if (converter != null) {
143+
hints.reflection().registerType(managedClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
144+
}
145+
}
146+
96147
}
97148
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.orm.jpa.domain;
18+
19+
import jakarta.persistence.Column;
20+
import jakarta.persistence.Entity;
21+
import jakarta.persistence.Id;
22+
import jakarta.persistence.IdClass;
23+
24+
@Entity
25+
@IdClass(EmployeeId.class)
26+
public class Employee {
27+
28+
@Id
29+
@Column
30+
private String name;
31+
32+
@Id
33+
@Column
34+
private String department;
35+
36+
private EmployeeLocation location;
37+
38+
39+
public String getName() {
40+
return name;
41+
}
42+
43+
public void setName(String name) {
44+
this.name = name;
45+
}
46+
47+
public String getDepartment() {
48+
return department;
49+
}
50+
51+
public void setDepartment(String department) {
52+
this.department = department;
53+
}
54+
55+
public EmployeeLocation getLocation() {
56+
return location;
57+
}
58+
59+
public void setLocation(EmployeeLocation location) {
60+
this.location = location;
61+
}
62+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.orm.jpa.domain;
18+
19+
import java.io.Serial;
20+
import java.io.Serializable;
21+
22+
public class EmployeeId implements Serializable {
23+
24+
@Serial
25+
private static final long serialVersionUID = 1L;
26+
27+
private String name;
28+
private String department;
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.orm.jpa.domain;
18+
19+
public class EmployeeLocation {
20+
21+
private String location;
22+
23+
public String getLocation() {
24+
return location;
25+
}
26+
27+
public void setLocation(String location) {
28+
this.location = location;
29+
}
30+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.orm.jpa.domain;
18+
19+
import jakarta.persistence.AttributeConverter;
20+
import jakarta.persistence.Converter;
21+
22+
@Converter(autoApply = true)
23+
public class EmployeeLocationConverter implements AttributeConverter<EmployeeLocation, String> {
24+
25+
@Override
26+
public String convertToDatabaseColumn(EmployeeLocation employeeLocation) {
27+
if (employeeLocation != null) {
28+
return employeeLocation.getLocation();
29+
}
30+
return null;
31+
}
32+
33+
@Override
34+
public EmployeeLocation convertToEntityAttribute(String data) {
35+
if (data != null) {
36+
EmployeeLocation employeeLocation = new EmployeeLocation();
37+
employeeLocation.setLocation(data);
38+
return employeeLocation;
39+
}
40+
return null;
41+
}
42+
}

spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717
package org.springframework.orm.jpa.persistenceunit;
1818

1919
import java.util.function.BiConsumer;
20+
import java.util.function.Consumer;
2021

2122
import javax.sql.DataSource;
2223

2324
import org.junit.jupiter.api.Test;
2425

26+
import org.springframework.aot.hint.MemberCategory;
27+
import org.springframework.aot.hint.RuntimeHints;
28+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
2529
import org.springframework.aot.test.generate.TestGenerationContext;
2630
import org.springframework.aot.test.generate.compile.Compiled;
2731
import org.springframework.aot.test.generate.compile.TestCompiler;
@@ -35,7 +39,12 @@
3539
import org.springframework.orm.jpa.JpaVendorAdapter;
3640
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
3741
import org.springframework.orm.jpa.domain.DriversLicense;
42+
import org.springframework.orm.jpa.domain.Employee;
43+
import org.springframework.orm.jpa.domain.EmployeeId;
44+
import org.springframework.orm.jpa.domain.EmployeeLocation;
45+
import org.springframework.orm.jpa.domain.EmployeeLocationConverter;
3846
import org.springframework.orm.jpa.domain.Person;
47+
import org.springframework.orm.jpa.domain.PersonListener;
3948
import org.springframework.orm.jpa.vendor.Database;
4049
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
4150

@@ -46,6 +55,7 @@
4655
* Tests for {@link PersistenceManagedTypesBeanRegistrationAotProcessor}.
4756
*
4857
* @author Stephane Nicoll
58+
* @author Sebastien Deleuze
4959
*/
5060
class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
5161

@@ -59,13 +69,38 @@ void processEntityManagerWithPackagesToScan() {
5969
PersistenceManagedTypes persistenceManagedTypes = freshApplicationContext.getBean(
6070
"persistenceManagedTypes", PersistenceManagedTypes.class);
6171
assertThat(persistenceManagedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
62-
DriversLicense.class.getName(), Person.class.getName());
72+
DriversLicense.class.getName(), Person.class.getName(), Employee.class.getName(),
73+
EmployeeLocationConverter.class.getName());
6374
assertThat(persistenceManagedTypes.getManagedPackages()).isEmpty();
6475
assertThat(freshApplicationContext.getBean(
6576
EntityManagerWithPackagesToScanConfiguration.class).scanningInvoked).isFalse();
6677
});
6778
}
6879

80+
@Test
81+
void contributeHints() {
82+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
83+
context.registerBean(EntityManagerWithPackagesToScanConfiguration.class);
84+
contributeHints(context, hints -> {
85+
assertThat(RuntimeHintsPredicates.reflection().onType(DriversLicense.class)
86+
.withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints);
87+
assertThat(RuntimeHintsPredicates.reflection().onType(Person.class)
88+
.withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints);
89+
assertThat(RuntimeHintsPredicates.reflection().onType(PersonListener.class)
90+
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS))
91+
.accepts(hints);
92+
assertThat(RuntimeHintsPredicates.reflection().onType(Employee.class)
93+
.withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints);
94+
assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeId.class)
95+
.withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints);
96+
assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeLocationConverter.class)
97+
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS))
98+
.accepts(hints);
99+
assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeLocation.class)
100+
.withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints);
101+
});
102+
}
103+
69104

70105
@SuppressWarnings("unchecked")
71106
private void compile(GenericApplicationContext applicationContext,
@@ -86,6 +121,13 @@ private GenericApplicationContext toFreshApplicationContext(
86121
return freshApplicationContext;
87122
}
88123

124+
private void contributeHints(GenericApplicationContext applicationContext, Consumer<RuntimeHints> result) {
125+
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
126+
TestGenerationContext generationContext = new TestGenerationContext();
127+
generator.processAheadOfTime(applicationContext, generationContext);
128+
result.accept(generationContext.getRuntimeHints());
129+
}
130+
89131
@Configuration(proxyBeanMethods = false)
90132
public static class EntityManagerWithPackagesToScanConfiguration {
91133

spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScannerTests.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import org.springframework.core.io.ClassPathResource;
2323
import org.springframework.core.io.DefaultResourceLoader;
2424
import org.springframework.orm.jpa.domain.DriversLicense;
25+
import org.springframework.orm.jpa.domain.Employee;
26+
import org.springframework.orm.jpa.domain.EmployeeLocationConverter;
2527
import org.springframework.orm.jpa.domain.Person;
2628
import org.springframework.orm.jpa.domain2.entity.User;
2729

@@ -40,7 +42,8 @@ class PersistenceManagedTypesScannerTests {
4042
void scanPackageWithOnlyEntities() {
4143
PersistenceManagedTypes managedTypes = this.scanner.scan("org.springframework.orm.jpa.domain");
4244
assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
43-
Person.class.getName(), DriversLicense.class.getName());
45+
Person.class.getName(), DriversLicense.class.getName(), Employee.class.getName(),
46+
EmployeeLocationConverter.class.getName());
4447
assertThat(managedTypes.getManagedPackages()).isEmpty();
4548
}
4649

0 commit comments

Comments
 (0)