Introduce PersistenceManagedTypes and AOT processing of managed entities

This commit adds PersistenceManagedTypes, an abstraction that represents
what typically happens at runtime when a persistence unit is built based
on classpath scanning.

PersistenceManagedTypesScanner extracts the logic that used to be in
DefaultPersistenceUnitManager and the latter can be configured with
a PersistenceManagedTypes instance.

This commits adds a bean registration AOT contribution that retrieves
the result of the configured instance and replaces it with the list
of managed entities. This has the result of making sure scanning, if
any, does not happen at runtime. This also could help if additional
hints for managed entities are required.

Closes gh-28287
This commit is contained in:
Stephane Nicoll
2022-08-05 09:47:51 +02:00
parent eac616a83e
commit 4fb4e54c73
13 changed files with 781 additions and 96 deletions

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2002-2022 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.domain2.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String firstName;
private String lastName;
public Integer getId() {
return this.id;
}
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}

View File

@@ -0,0 +1,7 @@
/**
* Sample package-info for testing purposes.
*/
@TypeDef(name = "test", typeClass = Object.class)
package org.springframework.orm.jpa.domain2;
import org.hibernate.annotations.TypeDef;

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2002-2022 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.persistenceunit;
import java.util.function.BiConsumer;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generator.compile.Compiled;
import org.springframework.aot.test.generator.compile.TestCompiler;
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;
import org.springframework.core.testfixture.aot.generate.TestGenerationContext;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.domain.DriversLicense;
import org.springframework.orm.jpa.domain.Person;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link PersistenceManagedTypesBeanRegistrationAotProcessor}.
*
* @author Stephane Nicoll
*/
class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
@Test
void processEntityManagerWithPackagesToScan() {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(EntityManagerWithPackagesToScanConfiguration.class);
compile(context, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(
initializer);
PersistenceManagedTypes persistenceManagedTypes = freshApplicationContext.getBean(
"persistenceManagedTypes", PersistenceManagedTypes.class);
assertThat(persistenceManagedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
DriversLicense.class.getName(), Person.class.getName());
assertThat(persistenceManagedTypes.getManagedPackages()).isEmpty();
assertThat(freshApplicationContext.getBean(
EntityManagerWithPackagesToScanConfiguration.class).scanningInvoked).isFalse();
});
}
@SuppressWarnings("unchecked")
private void compile(GenericApplicationContext applicationContext,
BiConsumer<ApplicationContextInitializer<GenericApplicationContext>, Compiled> result) {
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
TestGenerationContext generationContext = new TestGenerationContext();
generator.processAheadOfTime(applicationContext, generationContext);
generationContext.writeGeneratedContent();
TestCompiler.forSystem().withFiles(generationContext.getGeneratedFiles()).compile(compiled ->
result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled));
}
private GenericApplicationContext toFreshApplicationContext(
ApplicationContextInitializer<GenericApplicationContext> initializer) {
GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
initializer.initialize(freshApplicationContext);
freshApplicationContext.refresh();
return freshApplicationContext;
}
@Configuration(proxyBeanMethods = false)
public static class EntityManagerWithPackagesToScanConfiguration {
private boolean scanningInvoked;
@Bean
public DataSource mockDataSource() {
return mock(DataSource.class);
}
@Bean
public HibernateJpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
jpaVendorAdapter.setDatabase(Database.HSQL);
return jpaVendorAdapter;
}
@Bean
public PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) {
this.scanningInvoked = true;
return new PersistenceManagedTypesScanner(resourceLoader)
.scan("org.springframework.orm.jpa.domain");
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
JpaVendorAdapter jpaVendorAdapter, PersistenceManagedTypes persistenceManagedTypes) {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter);
entityManagerFactoryBean.setManagedTypes(persistenceManagedTypes);
return entityManagerFactoryBean;
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2002-2022 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.persistenceunit;
import org.junit.jupiter.api.Test;
import org.springframework.context.testfixture.index.CandidateComponentsTestClassLoader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.orm.jpa.domain.DriversLicense;
import org.springframework.orm.jpa.domain.Person;
import org.springframework.orm.jpa.domain2.entity.User;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PersistenceManagedTypesScanner}.
*
* @author Stephane Nicoll
*/
class PersistenceManagedTypesScannerTests {
private final PersistenceManagedTypesScanner scanner = new PersistenceManagedTypesScanner(new DefaultResourceLoader());
@Test
void scanPackageWithOnlyEntities() {
PersistenceManagedTypes managedTypes = this.scanner.scan("org.springframework.orm.jpa.domain");
assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
Person.class.getName(), DriversLicense.class.getName());
assertThat(managedTypes.getManagedPackages()).isEmpty();
}
@Test
void scanPackageWithEntitiesAndManagedPackages() {
PersistenceManagedTypes managedTypes = this.scanner.scan("org.springframework.orm.jpa.domain2");
assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(User.class.getName());
assertThat(managedTypes.getManagedPackages()).containsExactlyInAnyOrder(
"org.springframework.orm.jpa.domain2");
}
@Test
void scanPackageUsesIndexIfPresent() {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader(
CandidateComponentsTestClassLoader.index(getClass().getClassLoader(),
new ClassPathResource("test-spring.components", getClass())));
PersistenceManagedTypes managedTypes = new PersistenceManagedTypesScanner(resourceLoader).scan("com.example");
assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
"com.example.domain.Person", "com.example.domain.Address");
assertThat(managedTypes.getManagedPackages()).containsExactlyInAnyOrder(
"com.example.domain");
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2002-2022 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.persistenceunit;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link PersistenceManagedTypes}.
*
* @author Stephane Nicoll
*/
class PersistenceManagedTypesTests {
@Test
void createWithManagedClassNames() {
PersistenceManagedTypes managedTypes = PersistenceManagedTypes.of(
"com.example.One", "com.example.Two");
assertThat(managedTypes.getManagedClassNames()).containsExactly(
"com.example.One", "com.example.Two");
assertThat(managedTypes.getManagedPackages()).isEmpty();
assertThat(managedTypes.getPersistenceUnitRootUrl()).isNull();
}
@Test
void createWithNullManagedClasses() {
assertThatIllegalArgumentException().isThrownBy(() -> PersistenceManagedTypes.of(null));
}
@Test
void createWithManagedClassNamesAndPackages() {
PersistenceManagedTypes managedTypes = PersistenceManagedTypes.of(
List.of("com.example.One", "com.example.Two"), List.of("com.example"));
assertThat(managedTypes.getManagedClassNames()).containsExactly(
"com.example.One", "com.example.Two");
assertThat(managedTypes.getManagedPackages()).containsExactly("com.example");
assertThat(managedTypes.getPersistenceUnitRootUrl()).isNull();
}
}

View File

@@ -0,0 +1,4 @@
com.example.domain.Person=jakarta.persistence.Entity
com.example.domain.Address=jakarta.persistence.Entity
com.example.domain=package-info