From 4da1511ed3a7835b60201acc9f4b2c527b09ad20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 4 Jun 2024 15:31:29 +0200 Subject: [PATCH] Infer hints for Hibernate generators This commit updates PersistenceManagedTypesBeanRegistrationAotProcessor in order to infer hints for Hibernate annotations meta annotated with `@ValueGenerationType` (like `@CreationTimestamp`) and `@IdGeneratorType`. `@GenericGenerator` is not supported as it is deprecated as of Hibernate 6.5. Closes gh-32842 --- ...agedTypesBeanRegistrationAotProcessor.java | 57 ++++++++++------- .../orm/jpa/hibernate/domain/Book.java | 62 +++++++++++++++++++ ...ypesBeanRegistrationAotProcessorTests.java | 46 +++++++++++--- 3 files changed, 134 insertions(+), 31 deletions(-) create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/domain/Book.java diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java index 481b37abea2..800d5a70204 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java @@ -188,38 +188,53 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr @SuppressWarnings("unchecked") private void contributeHibernateHints(RuntimeHints hints, @Nullable ClassLoader classLoader, Class managedClass) { - Class embeddableInstantiatorClass = loadEmbeddableInstantiatorClass(classLoader); - if (embeddableInstantiatorClass == null) { - return; - } ReflectionHints reflection = hints.reflection(); - registerInstantiatorForReflection(reflection, - AnnotationUtils.findAnnotation(managedClass, embeddableInstantiatorClass)); - ReflectionUtils.doWithFields(managedClass, field -> { - registerInstantiatorForReflection(reflection, - AnnotationUtils.findAnnotation(field, embeddableInstantiatorClass)); - registerInstantiatorForReflection(reflection, - AnnotationUtils.findAnnotation(field.getType(), embeddableInstantiatorClass)); - }); - } - private void registerInstantiatorForReflection(ReflectionHints reflection, @Nullable Annotation annotation) { - if (annotation == null) { - return; + Class embeddableInstantiatorClass = loadClass("org.hibernate.annotations.EmbeddableInstantiator", classLoader); + if (embeddableInstantiatorClass != null) { + registerForReflection(reflection, + AnnotationUtils.findAnnotation(managedClass, embeddableInstantiatorClass), "value"); + ReflectionUtils.doWithFields(managedClass, field -> { + registerForReflection(reflection, + AnnotationUtils.findAnnotation(field, embeddableInstantiatorClass), "value"); + registerForReflection(reflection, + AnnotationUtils.findAnnotation(field.getType(), embeddableInstantiatorClass), "value"); + }); + } + + Class valueGenerationTypeClass = loadClass("org.hibernate.annotations.ValueGenerationType", classLoader); + if (valueGenerationTypeClass != null) { + ReflectionUtils.doWithFields(managedClass, field -> registerForReflection(reflection, + AnnotationUtils.findAnnotation(field, valueGenerationTypeClass), "generatedBy")); + ReflectionUtils.doWithMethods(managedClass, method -> registerForReflection(reflection, + AnnotationUtils.findAnnotation(method, valueGenerationTypeClass), "generatedBy")); + } + + Class idGeneratorTypeClass = loadClass("org.hibernate.annotations.IdGeneratorType", classLoader); + if (idGeneratorTypeClass != null) { + ReflectionUtils.doWithFields(managedClass, field -> registerForReflection(reflection, + AnnotationUtils.findAnnotation(field, idGeneratorTypeClass), "value")); + ReflectionUtils.doWithMethods(managedClass, method -> registerForReflection(reflection, + AnnotationUtils.findAnnotation(method, idGeneratorTypeClass), "value")); } - Class embeddableInstantiatorClass = (Class) AnnotationUtils.getAnnotationAttributes(annotation).get("value"); - reflection.registerType(embeddableInstantiatorClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); } @Nullable - private static Class loadEmbeddableInstantiatorClass(@Nullable ClassLoader classLoader) { + private static Class loadClass(String className, @Nullable ClassLoader classLoader) { try { - return (Class) ClassUtils.forName( - "org.hibernate.annotations.EmbeddableInstantiator", classLoader); + return (Class) ClassUtils.forName(className, classLoader); } catch (ClassNotFoundException ex) { return null; } } + + private void registerForReflection(ReflectionHints reflection, @Nullable Annotation annotation, String attribute) { + if (annotation == null) { + return; + } + Class embeddableInstantiatorClass = (Class) AnnotationUtils.getAnnotationAttributes(annotation).get(attribute); + reflection.registerType(embeddableInstantiatorClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/domain/Book.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/domain/Book.java new file mode 100644 index 00000000000..d418cfb65a4 --- /dev/null +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/domain/Book.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.jpa.hibernate.domain; + +import java.time.Instant; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.CreationTimestamp; + +@Entity +public class Book { + + @Id + private Long id; + + private String title; + + @CreationTimestamp + private Instant createdOn; + + public Book() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Instant getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Instant createdOn) { + this.createdOn = createdOn; + } +} diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java index 04ef5dd0df3..64f01029d07 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.function.Consumer; import javax.sql.DataSource; +import org.hibernate.tuple.CreationTimestampGeneration; import org.junit.jupiter.api.Test; import org.springframework.aot.hint.MemberCategory; @@ -30,7 +31,6 @@ import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.aot.ApplicationContextAotGenerator; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.io.ResourceLoader; @@ -64,7 +64,7 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests { @Test void processEntityManagerWithPackagesToScan() { GenericApplicationContext context = new AnnotationConfigApplicationContext(); - context.registerBean(EntityManagerWithPackagesToScanConfiguration.class); + context.registerBean(JpaDomainConfiguration.class); compile(context, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext( initializer); @@ -75,14 +75,14 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests { EmployeeLocationConverter.class.getName()); assertThat(persistenceManagedTypes.getManagedPackages()).isEmpty(); assertThat(freshApplicationContext.getBean( - EntityManagerWithPackagesToScanConfiguration.class).scanningInvoked).isFalse(); + JpaDomainConfiguration.class).scanningInvoked).isFalse(); }); } @Test - void contributeHints() { + void contributeJpaHints() { GenericApplicationContext context = new AnnotationConfigApplicationContext(); - context.registerBean(EntityManagerWithPackagesToScanConfiguration.class); + context.registerBean(JpaDomainConfiguration.class); contributeHints(context, hints -> { assertThat(RuntimeHintsPredicates.reflection().onType(DriversLicense.class) .withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints); @@ -108,6 +108,15 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests { }); } + @Test + void contributeHibernateHints() { + GenericApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean(HibernateDomainConfiguration.class); + contributeHints(context, hints -> + assertThat(RuntimeHintsPredicates.reflection().onType(CreationTimestampGeneration.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(hints)); + } + @SuppressWarnings("unchecked") private void compile(GenericApplicationContext applicationContext, @@ -135,10 +144,25 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests { result.accept(generationContext.getRuntimeHints()); } - @Configuration(proxyBeanMethods = false) - public static class EntityManagerWithPackagesToScanConfiguration { + public static class JpaDomainConfiguration extends AbstractEntityManagerWithPackagesToScanConfiguration { + + @Override + protected String packageToScan() { + return "org.springframework.orm.jpa.domain"; + } + } - private boolean scanningInvoked; + public static class HibernateDomainConfiguration extends AbstractEntityManagerWithPackagesToScanConfiguration { + + @Override + protected String packageToScan() { + return "org.springframework.orm.jpa.hibernate.domain"; + } + } + + public abstract static class AbstractEntityManagerWithPackagesToScanConfiguration { + + protected boolean scanningInvoked; @Bean public DataSource mockDataSource() { @@ -156,7 +180,7 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests { public PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) { this.scanningInvoked = true; return new PersistenceManagedTypesScanner(resourceLoader) - .scan("org.springframework.orm.jpa.domain"); + .scan(packageToScan()); } @Bean @@ -169,6 +193,8 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests { return entityManagerFactoryBean; } + protected abstract String packageToScan(); + } }