Create spring-boot-jpa module

This commit is contained in:
Stéphane Nicoll
2025-03-19 18:07:28 +01:00
committed by Phillip Webb
parent 81d4528eee
commit 079713039d
89 changed files with 385 additions and 316 deletions

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
/**
* Configures the base packages used by auto-configuration when scanning for entity
* classes. Refer to the documentation of the data technology you are using for more
* details.
* <p>
* One of {@link #basePackageClasses()}, {@link #basePackages()} or its alias
* {@link #value()} may be specified to define specific packages to scan. If specific
* packages are not defined scanning will occur from the package of the class with this
* annotation.
*
* @author Phillip Webb
* @since 4.0.0
* @see EntityScanPackages
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EntityScanPackages.Registrar.class)
public @interface EntityScan {
/**
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
* declarations e.g.: {@code @EntityScan("org.my.pkg")} instead of
* {@code @EntityScan(basePackages="org.my.pkg")}.
* @return the base packages to scan
*/
@AliasFor("basePackages")
String[] value() default {};
/**
* Base packages to scan for entities. {@link #value()} is an alias for (and mutually
* exclusive with) this attribute.
* <p>
* Use {@link #basePackageClasses()} for a type-safe alternative to String-based
* package names.
* @return the base packages to scan
*/
@AliasFor("value")
String[] basePackages() default {};
/**
* Type-safe alternative to {@link #basePackages()} for specifying the packages to
* scan for entities. The package of each class specified will be scanned.
* <p>
* Consider creating a special no-op marker class or interface in each package that
* serves no purpose other than being referenced by this attribute.
* @return classes from the base packages to scan
*/
Class<?>[] basePackageClasses() default {};
}

View File

@@ -0,0 +1,179 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Class for storing {@link EntityScan @EntityScan} specified packages for reference later
* (e.g. by JPA auto-configuration).
*
* @author Phillip Webb
* @since 4.0.0
* @see EntityScan
* @see EntityScanner
*/
public class EntityScanPackages {
private static final String BEAN = EntityScanPackages.class.getName();
private static final EntityScanPackages NONE = new EntityScanPackages();
private final List<String> packageNames;
EntityScanPackages(String... packageNames) {
List<String> packages = new ArrayList<>();
for (String name : packageNames) {
if (StringUtils.hasText(name)) {
packages.add(name);
}
}
this.packageNames = Collections.unmodifiableList(packages);
}
/**
* Return the package names specified from all {@link EntityScan @EntityScan}
* annotations.
* @return the entity scan package names
*/
public List<String> getPackageNames() {
return this.packageNames;
}
/**
* Return the {@link EntityScanPackages} for the given bean factory.
* @param beanFactory the source bean factory
* @return the {@link EntityScanPackages} for the bean factory (never {@code null})
*/
public static EntityScanPackages get(BeanFactory beanFactory) {
// Currently we only store a single base package, but we return a list to
// allow this to change in the future if needed
try {
return beanFactory.getBean(BEAN, EntityScanPackages.class);
}
catch (NoSuchBeanDefinitionException ex) {
return NONE;
}
}
/**
* Register the specified entity scan packages with the system.
* @param registry the source registry
* @param packageNames the package names to register
*/
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
Assert.notNull(registry, "'registry' must not be null");
Assert.notNull(packageNames, "'packageNames' must not be null");
register(registry, Arrays.asList(packageNames));
}
/**
* Register the specified entity scan packages with the system.
* @param registry the source registry
* @param packageNames the package names to register
*/
public static void register(BeanDefinitionRegistry registry, Collection<String> packageNames) {
Assert.notNull(registry, "'registry' must not be null");
Assert.notNull(packageNames, "'packageNames' must not be null");
if (registry.containsBeanDefinition(BEAN)) {
EntityScanPackagesBeanDefinition beanDefinition = (EntityScanPackagesBeanDefinition) registry
.getBeanDefinition(BEAN);
beanDefinition.addPackageNames(packageNames);
}
else {
registry.registerBeanDefinition(BEAN, new EntityScanPackagesBeanDefinition(packageNames));
}
}
/**
* {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
* configuration.
*/
static class Registrar implements ImportBeanDefinitionRegistrar {
private final Environment environment;
Registrar(Environment environment) {
this.environment = environment;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, getPackagesToScan(metadata));
}
private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap(metadata.getAnnotationAttributes(EntityScan.class.getName()));
Set<String> packagesToScan = new LinkedHashSet<>();
for (String basePackage : attributes.getStringArray("basePackages")) {
String[] tokenized = StringUtils.tokenizeToStringArray(
this.environment.resolvePlaceholders(basePackage),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(packagesToScan, tokenized);
}
for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
packagesToScan.add(this.environment.resolvePlaceholders(ClassUtils.getPackageName(basePackageClass)));
}
if (packagesToScan.isEmpty()) {
String packageName = ClassUtils.getPackageName(metadata.getClassName());
Assert.state(StringUtils.hasLength(packageName), "@EntityScan cannot be used with the default package");
return Collections.singleton(packageName);
}
return packagesToScan;
}
}
static class EntityScanPackagesBeanDefinition extends RootBeanDefinition {
private final Set<String> packageNames = new LinkedHashSet<>();
EntityScanPackagesBeanDefinition(Collection<String> packageNames) {
setBeanClass(EntityScanPackages.class);
setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
addPackageNames(packageNames);
}
private void addPackageNames(Collection<String> additionalPackageNames) {
this.packageNames.addAll(additionalPackageNames);
getConstructorArgumentValues().addIndexedArgumentValue(0, StringUtils.toStringArray(this.packageNames));
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* An entity scanner that searches the classpath from an {@link EntityScan @EntityScan}
* specified packages.
*
* @author Phillip Webb
* @since 4.0.0
*/
public class EntityScanner {
private final ApplicationContext context;
/**
* Create a new {@link EntityScanner} instance.
* @param context the source application context
*/
public EntityScanner(ApplicationContext context) {
Assert.notNull(context, "'context' must not be null");
this.context = context;
}
/**
* Scan for entities with the specified annotations.
* @param annotationTypes the annotation types used on the entities
* @return a set of entity classes
* @throws ClassNotFoundException if an entity class cannot be loaded
*/
@SafeVarargs
public final Set<Class<?>> scan(Class<? extends Annotation>... annotationTypes) throws ClassNotFoundException {
List<String> packages = getPackages();
if (packages.isEmpty()) {
return Collections.emptySet();
}
ClassPathScanningCandidateComponentProvider scanner = createClassPathScanningCandidateComponentProvider(
this.context);
for (Class<? extends Annotation> annotationType : annotationTypes) {
scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
}
Set<Class<?>> entitySet = new HashSet<>();
for (String basePackage : packages) {
if (StringUtils.hasText(basePackage)) {
for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) {
entitySet.add(ClassUtils.forName(candidate.getBeanClassName(), this.context.getClassLoader()));
}
}
}
return entitySet;
}
/**
* Create a {@link ClassPathScanningCandidateComponentProvider} to scan entities based
* on the specified {@link ApplicationContext}.
* @param context the {@link ApplicationContext} to use
* @return a {@link ClassPathScanningCandidateComponentProvider} suitable to scan
* entities
* @since 2.4.0
*/
protected ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider(
ApplicationContext context) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.setEnvironment(context.getEnvironment());
scanner.setResourceLoader(context);
return scanner;
}
private List<String> getPackages() {
List<String> packages = EntityScanPackages.get(this.context).getPackageNames();
if (packages.isEmpty() && AutoConfigurationPackages.has(this.context)) {
packages = AutoConfigurationPackages.get(this.context);
}
return packages;
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* General purpose domain annotations and classes.
*/
package org.springframework.boot.autoconfigure.domain;

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain;
import java.util.Collection;
import java.util.Collections;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationConfigurationException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link EntityScanPackages}.
*
* @author Phillip Webb
*/
class EntityScanPackagesTests {
private AnnotationConfigApplicationContext context;
@AfterEach
void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
void getWhenNoneRegisteredShouldReturnNone() {
this.context = new AnnotationConfigApplicationContext();
this.context.refresh();
EntityScanPackages packages = EntityScanPackages.get(this.context);
assertThat(packages).isNotNull();
assertThat(packages.getPackageNames()).isEmpty();
}
@Test
void getShouldReturnRegisterPackages() {
this.context = new AnnotationConfigApplicationContext();
EntityScanPackages.register(this.context, "a", "b");
EntityScanPackages.register(this.context, "b", "c");
this.context.refresh();
EntityScanPackages packages = EntityScanPackages.get(this.context);
assertThat(packages.getPackageNames()).containsExactly("a", "b", "c");
}
@Test
void registerFromArrayWhenRegistryIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> EntityScanPackages.register(null))
.withMessageContaining("'registry' must not be null");
}
@Test
void registerFromArrayWhenPackageNamesIsNullShouldThrowException() {
this.context = new AnnotationConfigApplicationContext();
assertThatIllegalArgumentException()
.isThrownBy(() -> EntityScanPackages.register(this.context, (String[]) null))
.withMessageContaining("'packageNames' must not be null");
}
@Test
void registerFromCollectionWhenRegistryIsNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> EntityScanPackages.register(null, Collections.emptyList()))
.withMessageContaining("'registry' must not be null");
}
@Test
void registerFromCollectionWhenPackageNamesIsNullShouldThrowException() {
this.context = new AnnotationConfigApplicationContext();
assertThatIllegalArgumentException()
.isThrownBy(() -> EntityScanPackages.register(this.context, (Collection<String>) null))
.withMessageContaining("'packageNames' must not be null");
}
@Test
void entityScanAnnotationWhenHasValueAttributeShouldSetupPackages() {
this.context = new AnnotationConfigApplicationContext(EntityScanValueConfig.class);
EntityScanPackages packages = EntityScanPackages.get(this.context);
assertThat(packages.getPackageNames()).containsExactly("a");
}
@Test
void entityScanAnnotationWhenHasValueAttributeShouldSetupPackagesAsm() {
this.context = new AnnotationConfigApplicationContext();
this.context.registerBeanDefinition("entityScanValueConfig",
new RootBeanDefinition(EntityScanValueConfig.class.getName()));
this.context.refresh();
EntityScanPackages packages = EntityScanPackages.get(this.context);
assertThat(packages.getPackageNames()).containsExactly("a");
}
@Test
void entityScanAnnotationWhenHasBasePackagesAttributeShouldSetupPackages() {
this.context = new AnnotationConfigApplicationContext(EntityScanBasePackagesConfig.class);
EntityScanPackages packages = EntityScanPackages.get(this.context);
assertThat(packages.getPackageNames()).containsExactly("b");
}
@Test
void entityScanAnnotationWhenHasValueAndBasePackagesAttributeShouldThrow() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> this.context = new AnnotationConfigApplicationContext(
EntityScanValueAndBasePackagesConfig.class));
}
@Test
void entityScanAnnotationWhenHasBasePackageClassesAttributeShouldSetupPackages() {
this.context = new AnnotationConfigApplicationContext(EntityScanBasePackageClassesConfig.class);
EntityScanPackages packages = EntityScanPackages.get(this.context);
assertThat(packages.getPackageNames()).containsExactly(getClass().getPackage().getName());
}
@Test
void entityScanAnnotationWhenNoAttributesShouldSetupPackages() {
this.context = new AnnotationConfigApplicationContext(EntityScanNoAttributesConfig.class);
EntityScanPackages packages = EntityScanPackages.get(this.context);
assertThat(packages.getPackageNames()).containsExactly(getClass().getPackage().getName());
}
@Test
void entityScanAnnotationWhenLoadingFromMultipleConfigsShouldCombinePackages() {
this.context = new AnnotationConfigApplicationContext(EntityScanValueConfig.class,
EntityScanBasePackagesConfig.class);
EntityScanPackages packages = EntityScanPackages.get(this.context);
assertThat(packages.getPackageNames()).containsExactly("a", "b");
}
@Configuration(proxyBeanMethods = false)
@EntityScan("a")
static class EntityScanValueConfig {
}
@Configuration(proxyBeanMethods = false)
@EntityScan(basePackages = "b")
static class EntityScanBasePackagesConfig {
}
@Configuration(proxyBeanMethods = false)
@EntityScan(value = "a", basePackages = "b")
static class EntityScanValueAndBasePackagesConfig {
}
@Configuration(proxyBeanMethods = false)
@EntityScan(basePackageClasses = EntityScanPackagesTests.class)
static class EntityScanBasePackageClassesConfig {
}
@Configuration(proxyBeanMethods = false)
@EntityScan
static class EntityScanNoAttributesConfig {
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain;
import java.util.Collections;
import java.util.Set;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Entity;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.domain.scan.a.EmbeddableA;
import org.springframework.boot.autoconfigure.domain.scan.a.EntityA;
import org.springframework.boot.autoconfigure.domain.scan.b.EmbeddableB;
import org.springframework.boot.autoconfigure.domain.scan.b.EntityB;
import org.springframework.boot.autoconfigure.domain.scan.c.EmbeddableC;
import org.springframework.boot.autoconfigure.domain.scan.c.EntityC;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.assertArg;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link EntityScanner}.
*
* @author Phillip Webb
*/
class EntityScannerTests {
@Test
void createWhenContextIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new EntityScanner(null))
.withMessageContaining("'context' must not be null");
}
@Test
void scanShouldScanFromSinglePackage() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanConfig.class);
EntityScanner scanner = new EntityScanner(context);
Set<Class<?>> scanned = scanner.scan(Entity.class);
assertThat(scanned).containsOnly(EntityA.class, EntityB.class, EntityC.class);
context.close();
}
@Test
void scanShouldScanFromResolvedPlaceholderPackage() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("com.example.entity-package=org.springframework.boot.autoconfigure.domain.scan")
.applyTo(context);
context.register(ScanPlaceholderConfig.class);
context.refresh();
EntityScanner scanner = new EntityScanner(context);
Set<Class<?>> scanned = scanner.scan(Entity.class);
assertThat(scanned).containsOnly(EntityA.class, EntityB.class, EntityC.class);
context.close();
}
@Test
void scanShouldScanFromMultiplePackages() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanAConfig.class,
ScanBConfig.class);
EntityScanner scanner = new EntityScanner(context);
Set<Class<?>> scanned = scanner.scan(Entity.class);
assertThat(scanned).containsOnly(EntityA.class, EntityB.class);
context.close();
}
@Test
void scanShouldFilterOnAnnotation() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanConfig.class);
EntityScanner scanner = new EntityScanner(context);
assertThat(scanner.scan(Entity.class)).containsOnly(EntityA.class, EntityB.class, EntityC.class);
assertThat(scanner.scan(Embeddable.class)).containsOnly(EmbeddableA.class, EmbeddableB.class,
EmbeddableC.class);
assertThat(scanner.scan(Entity.class, Embeddable.class)).containsOnly(EntityA.class, EntityB.class,
EntityC.class, EmbeddableA.class, EmbeddableB.class, EmbeddableC.class);
context.close();
}
@Test
void scanShouldUseCustomCandidateComponentProvider() throws ClassNotFoundException {
ClassPathScanningCandidateComponentProvider candidateComponentProvider = mock(
ClassPathScanningCandidateComponentProvider.class);
given(candidateComponentProvider.findCandidateComponents("org.springframework.boot.autoconfigure.domain.scan"))
.willReturn(Collections.emptySet());
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanConfig.class);
TestEntityScanner scanner = new TestEntityScanner(context, candidateComponentProvider);
scanner.scan(Entity.class);
then(candidateComponentProvider).should()
.addIncludeFilter(
assertArg((typeFilter) -> assertThat(typeFilter).isInstanceOfSatisfying(AnnotationTypeFilter.class,
(filter) -> assertThat(filter.getAnnotationType()).isEqualTo(Entity.class))));
then(candidateComponentProvider).should()
.findCandidateComponents("org.springframework.boot.autoconfigure.domain.scan");
then(candidateComponentProvider).shouldHaveNoMoreInteractions();
}
@Test
void scanShouldScanCommaSeparatedPackagesInPlaceholderPackage() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues
.of("com.example.entity-package=org.springframework.boot.autoconfigure.domain.scan.a,org.springframework.boot.autoconfigure.domain.scan.b")
.applyTo(context);
context.register(ScanPlaceholderConfig.class);
context.refresh();
EntityScanner scanner = new EntityScanner(context);
Set<Class<?>> scanned = scanner.scan(Entity.class);
assertThat(scanned).containsOnly(EntityA.class, EntityB.class);
context.close();
}
private static class TestEntityScanner extends EntityScanner {
private final ClassPathScanningCandidateComponentProvider candidateComponentProvider;
TestEntityScanner(ApplicationContext context,
ClassPathScanningCandidateComponentProvider candidateComponentProvider) {
super(context);
this.candidateComponentProvider = candidateComponentProvider;
}
@Override
protected ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider(
ApplicationContext context) {
return this.candidateComponentProvider;
}
}
@Configuration(proxyBeanMethods = false)
@EntityScan("org.springframework.boot.autoconfigure.domain.scan")
static class ScanConfig {
}
@Configuration(proxyBeanMethods = false)
@EntityScan(basePackageClasses = EntityA.class)
static class ScanAConfig {
}
@Configuration(proxyBeanMethods = false)
@EntityScan(basePackageClasses = EntityB.class)
static class ScanBConfig {
}
@Configuration(proxyBeanMethods = false)
@EntityScan("${com.example.entity-package}")
static class ScanPlaceholderConfig {
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain.scan.a;
import jakarta.persistence.Embeddable;
@Embeddable
public class EmbeddableA {
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain.scan.a;
import jakarta.persistence.Entity;
@Entity
public class EntityA {
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain.scan.b;
import jakarta.persistence.Embeddable;
@Embeddable
public class EmbeddableB {
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain.scan.b;
import jakarta.persistence.Entity;
@Entity
public class EntityB {
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain.scan.c;
import jakarta.persistence.Embeddable;
@Embeddable
public class EmbeddableC {
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.domain.scan.c;
import jakarta.persistence.Entity;
@Entity
public class EntityC {
}