Commit 096ace68 authored by Phillip Webb's avatar Phillip Webb

Add @EntityScan annotation

Add an @EntityScan annotation that can be used to configure the
`packagesToScan` attribute on `LocalContainerEntityManagerFactoryBean`.

Fixed gh-239
parent 8db1d0e0
......@@ -94,6 +94,11 @@
<artifactId>jul-to-slf4j</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
......
/*
* 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.
* <p>
* 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.
* <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
*/
@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.
* <p>
* {@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.
*/
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.
* <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.
*/
Class<?>[] basePackageClasses() default {};
}
/*
* 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<String> packagesToScan = new LinkedHashSet<String>();
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<String>(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<ContextRefreshedEvent> {
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.");
}
}
}
/*
* 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;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment