From 096ace6896c806e22e1ceedf8417ece834d05522 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 19 Jan 2014 21:40:39 -0800 Subject: [PATCH] Add @EntityScan annotation Add an @EntityScan annotation that can be used to configure the `packagesToScan` attribute on `LocalContainerEntityManagerFactoryBean`. Fixed gh-239 --- spring-boot/pom.xml | 5 + .../boot/orm/jpa/EntityScan.java | 78 ++++++++ .../boot/orm/jpa/EntityScanRegistrar.java | 124 +++++++++++++ .../boot/orm/jpa/EntityScanTests.java | 173 ++++++++++++++++++ 4 files changed, 380 insertions(+) create mode 100644 spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScan.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScanRegistrar.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/orm/jpa/EntityScanTests.java diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml index c2397a587bb..bf3961acee2 100644 --- a/spring-boot/pom.xml +++ b/spring-boot/pom.xml @@ -94,6 +94,11 @@ jul-to-slf4j true + + org.springframework + spring-orm + true + org.springframework spring-test diff --git a/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScan.java b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScan.java new file mode 100644 index 00000000000..3cb01309746 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScan.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2014 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 + * + * http://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.orm.jpa; + +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.orm.jpa.LocalContainerEntityManagerFactoryBean; + +/** + * Configures the {@link LocalContainerEntityManagerFactoryBean} to to scan for entity + * classes in the classpath. This annotation provides an alternative to manually setting + * {@link LocalContainerEntityManagerFactoryBean#setPackagesToScan(String...)} and is + * particularly useful if you want to configure entity scanning in a type-safe way, or if + * your {@link LocalContainerEntityManagerFactoryBean} is auto-configured. + *

+ * A {@link LocalContainerEntityManagerFactoryBean} must be configured within your Spring + * ApplicationContext in order to use entity scanning. Furthermore, any existing + * {@code packagesToScan} setting will be replaced. + *

+ * 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 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(EntityScanRegistrar.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")}. + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated entities. + *

+ * {@link #value()} is an alias for (and mutually exclusive with) this attribute. + *

+ * Use {@link #basePackageClasses()} for a type-safe alternative to String-based + * package names. + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for annotated entities. The package of each class specified will be scanned. + *

+ * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScanRegistrar.java b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScanRegistrar.java new file mode 100644 index 00000000000..07477ddd8cf --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScanRegistrar.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2014 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 + * + * http://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.orm.jpa; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * {@link ImportBeanDefinitionRegistrar} used by {@link EntityScan}. + * + * @author Phillip Webb + */ +class EntityScanRegistrar implements ImportBeanDefinitionRegistrar { + + private static final String BEAN_NAME = "entityScanBeanPostProcessor"; + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition(BEAN_NAME)) { + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClass(EntityScanBeanPostProcessor.class); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue( + getPackagesToScan(importingClassMetadata)); + registry.registerBeanDefinition(BEAN_NAME, beanDefinition); + } + } + + private String[] getPackagesToScan(AnnotationMetadata metadata) { + AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata + .getAnnotationAttributes(EntityScan.class.getName())); + String[] value = attributes.getStringArray("value"); + String[] basePackages = attributes.getStringArray("basePackages"); + Class[] basePackageClasses = attributes.getClassArray("basePackageClasses"); + + if (!ObjectUtils.isEmpty(value)) { + Assert.state(ObjectUtils.isEmpty(basePackages), + "@EntityScan basePackages and value attributes are mutually exclusive"); + } + + Set packagesToScan = new LinkedHashSet(); + packagesToScan.addAll(Arrays.asList(value)); + packagesToScan.addAll(Arrays.asList(basePackages)); + for (Class basePackageClass : basePackageClasses) { + packagesToScan.add(ClassUtils.getPackageName(basePackageClass)); + } + if (packagesToScan.isEmpty()) { + return new String[] { ClassUtils.getPackageName(metadata.getClassName()) }; + } + return new ArrayList(packagesToScan).toArray(new String[packagesToScan + .size()]); + } + + /** + * {@link BeanPostProcessor} to set + * {@link LocalContainerEntityManagerFactoryBean#setPackagesToScan(String...)} based + * on an {@link EntityScan} annotation. + */ + static class EntityScanBeanPostProcessor implements BeanPostProcessor, + ApplicationListener { + + private final String[] packagesToScan; + + private boolean processed; + + public EntityScanBeanPostProcessor(String[] packagesToScan) { + this.packagesToScan = packagesToScan; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + if (bean instanceof LocalContainerEntityManagerFactoryBean) { + LocalContainerEntityManagerFactoryBean factoryBean = (LocalContainerEntityManagerFactoryBean) bean; + factoryBean.setPackagesToScan(this.packagesToScan); + this.processed = true; + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + Assert.state(this.processed, "Unable to configure " + + "LocalContainerEntityManagerFactoryBean from @EntityScan, " + + "ensure an appropriate bean is registered."); + } + } +} diff --git a/spring-boot/src/test/java/org/springframework/boot/orm/jpa/EntityScanTests.java b/spring-boot/src/test/java/org/springframework/boot/orm/jpa/EntityScanTests.java new file mode 100644 index 00000000000..c0fe02df520 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/orm/jpa/EntityScanTests.java @@ -0,0 +1,173 @@ +/* + * Copyright 2012-2014 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 + * + * http://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.orm.jpa; + +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link EntityScan}. + * + * @author Phillip Webb + */ +public class EntityScanTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private AnnotationConfigApplicationContext context; + + @Test + public void testValue() throws Exception { + this.context = new AnnotationConfigApplicationContext(ValueConfig.class); + assertSetPackagesToScan("com.mycorp.entity"); + } + + @Test + public void basePackages() throws Exception { + this.context = new AnnotationConfigApplicationContext(BasePackagesConfig.class); + assertSetPackagesToScan("com.mycorp.entity2"); + } + + @Test + public void basePackageClasses() throws Exception { + this.context = new AnnotationConfigApplicationContext( + BasePackageClassesConfig.class); + assertSetPackagesToScan(getClass().getPackage().getName()); + } + + @Test + public void fromConfigurationClass() throws Exception { + this.context = new AnnotationConfigApplicationContext(FromConfigConfig.class); + assertSetPackagesToScan(getClass().getPackage().getName()); + } + + @Test + public void valueAndBasePackagesThrows() throws Exception { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("@EntityScan basePackages and value " + + "attributes are mutually exclusive"); + this.context = new AnnotationConfigApplicationContext(ValueAndBasePackages.class); + } + + @Test + public void valueAndBasePackageClassesMerges() throws Exception { + this.context = new AnnotationConfigApplicationContext( + ValueAndBasePackageClasses.class); + assertSetPackagesToScan("com.mycorp.entity", getClass().getPackage().getName()); + } + + @Test + public void basePackageAndBasePackageClassesMerges() throws Exception { + this.context = new AnnotationConfigApplicationContext( + BasePackagesAndBasePackageClasses.class); + assertSetPackagesToScan("com.mycorp.entity2", getClass().getPackage().getName()); + } + + @Test + public void needsEntityManageFactory() throws Exception { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Unable to configure " + + "LocalContainerEntityManagerFactoryBean from @EntityScan, " + + "ensure an appropriate bean is registered."); + this.context = new AnnotationConfigApplicationContext(MissingEntityManager.class); + } + + private void assertSetPackagesToScan(String... expected) { + String[] actual = this.context.getBean( + TestLocalContainerEntityManagerFactoryBean.class).getPackagesToScan(); + assertThat(actual, equalTo(expected)); + } + + @Configuration + static class BaseConfig { + + @Bean + public TestLocalContainerEntityManagerFactoryBean entityManagerFactoryBean() { + return new TestLocalContainerEntityManagerFactoryBean(); + } + + } + + @EntityScan("com.mycorp.entity") + static class ValueConfig extends BaseConfig { + } + + @EntityScan(basePackages = "com.mycorp.entity2") + static class BasePackagesConfig extends BaseConfig { + } + + @EntityScan(basePackageClasses = EntityScanTests.class) + static class BasePackageClassesConfig extends BaseConfig { + } + + @EntityScan + static class FromConfigConfig extends BaseConfig { + } + + @EntityScan(value = "com.mycorp.entity", basePackages = "com.mycorp") + static class ValueAndBasePackages extends BaseConfig { + } + + @EntityScan(value = "com.mycorp.entity", basePackageClasses = EntityScanTests.class) + static class ValueAndBasePackageClasses extends BaseConfig { + } + + @EntityScan(basePackages = "com.mycorp.entity2", basePackageClasses = EntityScanTests.class) + static class BasePackagesAndBasePackageClasses extends BaseConfig { + } + + @Configuration + @EntityScan("com.mycorp.entity") + static class MissingEntityManager { + } + + private static class TestLocalContainerEntityManagerFactoryBean extends + LocalContainerEntityManagerFactoryBean { + + private String[] packagesToScan; + + @Override + protected EntityManagerFactory createNativeEntityManagerFactory() + throws PersistenceException { + return mock(EntityManagerFactory.class); + } + + @Override + public void setPackagesToScan(String... packagesToScan) { + this.packagesToScan = packagesToScan; + } + + public String[] getPackagesToScan() { + return this.packagesToScan; + } + + } + +}