From 73f28a3809981dbd9457dcff05c05b546f6a8d24 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 16 May 2013 14:05:07 +0100 Subject: [PATCH] [bs-22] Add tests for JPA auto configuration * Extracted the component scan detector so it can be used without @EnableAutoConfiguration * Added unit tests * Improve logging in @Conditional processing [#48127729] --- .../data/JpaComponentScanDetector.java | 153 ++++++++++++++++++ .../JpaRepositoriesAutoConfiguration.java | 2 +- .../annotation/AbstractOnBeanCondition.java | 12 +- ...EnableAutoConfigurationImportSelector.java | 112 +------------ .../context/annotation/OnClassCondition.java | 2 +- .../annotation/OnMissingBeanCondition.java | 8 +- .../annotation/OnMissingClassCondition.java | 3 + ...JpaRepositoriesAutoConfigurationTests.java | 58 +++++++ .../autoconfigure/data/test/City.java | 60 +++++++ .../data/test/CityRepository.java | 16 ++ 10 files changed, 307 insertions(+), 119 deletions(-) create mode 100644 spring-bootstrap/src/main/java/org/springframework/bootstrap/autoconfigure/data/JpaComponentScanDetector.java create mode 100644 spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/JpaRepositoriesAutoConfigurationTests.java create mode 100644 spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/test/City.java create mode 100644 spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/test/CityRepository.java diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/autoconfigure/data/JpaComponentScanDetector.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/autoconfigure/data/JpaComponentScanDetector.java new file mode 100644 index 0000000000..214d694c8c --- /dev/null +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/autoconfigure/data/JpaComponentScanDetector.java @@ -0,0 +1,153 @@ +/* + * Copyright 2012-2013 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.bootstrap.autoconfigure.data; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.bootstrap.context.annotation.AutoConfigurationUtils; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Helper to detect a component scan declared in the enclosing context (normally on a + * @Configuration class). Once the component scan is detected, the base + * packages are stored for retrieval later by the {@link JpaRepositoriesAutoConfiguration} + * . + * + * @author Dave Syer + * + */ +class JpaComponentScanDetector implements ImportBeanDefinitionRegistrar, BeanFactoryAware { + + private final Log logger = LogFactory.getLog(getClass()); + + private BeanFactory beanFactory; + + private MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + final BeanDefinitionRegistry registry) { + storeComponentScanBasePackages(); + } + + private void storeComponentScanBasePackages() { + if (this.beanFactory instanceof ConfigurableListableBeanFactory) { + storeComponentScanBasePackages((ConfigurableListableBeanFactory) this.beanFactory); + } else { + if (this.logger.isWarnEnabled()) { + this.logger + .warn("Unable to read @ComponentScan annotations for auto-configure"); + } + } + } + + private void storeComponentScanBasePackages( + ConfigurableListableBeanFactory beanFactory) { + List basePackages = new ArrayList(); + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); + String[] basePackagesAttribute = (String[]) beanDefinition + .getAttribute("componentScanBasePackages"); + if (basePackagesAttribute != null) { + basePackages.addAll(Arrays.asList(basePackagesAttribute)); + } + AnnotationMetadata metadata = getMetadata(beanDefinition); + basePackages.addAll(getBasePackages(metadata)); + } + AutoConfigurationUtils.storeBasePackages(beanFactory, basePackages); + } + + private AnnotationMetadata getMetadata(BeanDefinition beanDefinition) { + if (beanDefinition instanceof AbstractBeanDefinition + && ((AbstractBeanDefinition) beanDefinition).hasBeanClass()) { + Class beanClass = ((AbstractBeanDefinition) beanDefinition).getBeanClass(); + if (Enhancer.isEnhanced(beanClass)) { + beanClass = beanClass.getSuperclass(); + } + return new StandardAnnotationMetadata(beanClass, true); + } + String className = beanDefinition.getBeanClassName(); + if (className != null) { + try { + MetadataReader metadataReader = this.metadataReaderFactory + .getMetadataReader(className); + return metadataReader.getAnnotationMetadata(); + } catch (IOException ex) { + if (this.logger.isDebugEnabled()) { + this.logger.debug( + "Could not find class file for introspecting @ComponentScan classes: " + + className, ex); + } + } + } + return null; + } + + private List getBasePackages(AnnotationMetadata metadata) { + AnnotationAttributes attributes = AnnotationAttributes + .fromMap((metadata == null ? null : metadata.getAnnotationAttributes( + ComponentScan.class.getName(), true))); + if (attributes != null) { + List basePackages = new ArrayList(); + addAllHavingText(basePackages, attributes.getStringArray("value")); + addAllHavingText(basePackages, attributes.getStringArray("basePackages")); + for (String packageClass : attributes.getStringArray("basePackageClasses")) { + basePackages.add(ClassUtils.getPackageName(packageClass)); + } + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(metadata.getClassName())); + } + return basePackages; + } + return Collections.emptyList(); + } + + private void addAllHavingText(List list, String[] strings) { + for (String s : strings) { + if (StringUtils.hasText(s)) { + list.add(s); + } + } + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } +} diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/autoconfigure/data/JpaRepositoriesAutoConfiguration.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/autoconfigure/data/JpaRepositoriesAutoConfiguration.java index 46aa141359..0528b949cf 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/autoconfigure/data/JpaRepositoriesAutoConfiguration.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/autoconfigure/data/JpaRepositoriesAutoConfiguration.java @@ -34,7 +34,7 @@ import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; @Configuration @ConditionalOnClass(JpaRepository.class) @ConditionalOnMissingBean(JpaRepositoryFactoryBean.class) -@Import(JpaRepositoriesAutoConfigureRegistrar.class) +@Import({ JpaComponentScanDetector.class, JpaRepositoriesAutoConfigureRegistrar.class }) public class JpaRepositoriesAutoConfiguration { } diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AbstractOnBeanCondition.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AbstractOnBeanCondition.java index b2ef7ee110..65f2a9e8b4 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AbstractOnBeanCondition.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AbstractOnBeanCondition.java @@ -79,12 +79,20 @@ abstract class AbstractOnBeanCondition implements Condition { if (this.logger.isDebugEnabled()) { if (!beanClasses.isEmpty()) { this.logger.debug("Looking for beans with class: " + beanClasses); - this.logger.debug("Found beans with classes: " + beanClassesFound); + if (beanClassesFound.isEmpty()) { + this.logger.debug("Found no beans"); + } else { + this.logger.debug("Found beans with classes: " + beanClassesFound); + } } if (!beanNames.isEmpty()) { this.logger.debug("Looking for beans with names: " + beanNames); - this.logger.debug("Found beans with names: " + beanNamesFound); + if (beanNamesFound.isEmpty()) { + this.logger.debug("Found no beans"); + } else { + this.logger.debug("Found beans with names: " + beanNamesFound); + } } this.logger.debug("Match result is: " + result); } diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfigurationImportSelector.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfigurationImportSelector.java index 49055f10e8..df1e3b4bb3 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfigurationImportSelector.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfigurationImportSelector.java @@ -16,35 +16,17 @@ package org.springframework.bootstrap.context.annotation; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.cglib.proxy.Enhancer; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.DeferredImportSelector; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.Order; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.core.type.StandardAnnotationMetadata; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; /** * {@link DeferredImportSelector} to handle {@link EnableAutoConfiguration @@ -55,19 +37,12 @@ import org.springframework.util.StringUtils; */ @Order(Ordered.LOWEST_PRECEDENCE) class EnableAutoConfigurationImportSelector implements DeferredImportSelector, - BeanClassLoaderAware, BeanFactoryAware { - - private final Log logger = LogFactory.getLog(getClass()); + BeanClassLoaderAware { private ClassLoader beanClassLoader; - private BeanFactory beanFactory; - - private MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); - @Override public String[] selectImports(AnnotationMetadata metadata) { - storeComponentScanBasePackages(); AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata .getAnnotationAttributes(EnableAutoConfiguration.class.getName(), true)); List factories = new ArrayList( @@ -77,94 +52,9 @@ class EnableAutoConfigurationImportSelector implements DeferredImportSelector, return factories.toArray(new String[factories.size()]); } - private void storeComponentScanBasePackages() { - if (this.beanFactory instanceof ConfigurableListableBeanFactory) { - storeComponentScanBasePackages((ConfigurableListableBeanFactory) this.beanFactory); - } else { - if (this.logger.isWarnEnabled()) { - this.logger - .warn("Unable to read @ComponentScan annotations for auto-configure"); - } - } - } - - private void storeComponentScanBasePackages( - ConfigurableListableBeanFactory beanFactory) { - List basePackages = new ArrayList(); - for (String beanName : beanFactory.getBeanDefinitionNames()) { - BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); - String[] basePackagesAttribute = (String[]) beanDefinition - .getAttribute("componentScanBasePackages"); - if (basePackagesAttribute != null) { - basePackages.addAll(Arrays.asList(basePackagesAttribute)); - } - AnnotationMetadata metadata = getMetadata(beanDefinition); - basePackages.addAll(getBasePackages(metadata)); - } - AutoConfigurationUtils.storeBasePackages(beanFactory, basePackages); - } - - private AnnotationMetadata getMetadata(BeanDefinition beanDefinition) { - if (beanDefinition instanceof AbstractBeanDefinition - && ((AbstractBeanDefinition) beanDefinition).hasBeanClass()) { - Class beanClass = ((AbstractBeanDefinition) beanDefinition).getBeanClass(); - if (Enhancer.isEnhanced(beanClass)) { - beanClass = beanClass.getSuperclass(); - } - return new StandardAnnotationMetadata(beanClass, true); - } - String className = beanDefinition.getBeanClassName(); - if (className != null) { - try { - MetadataReader metadataReader = this.metadataReaderFactory - .getMetadataReader(className); - return metadataReader.getAnnotationMetadata(); - } catch (IOException ex) { - if (this.logger.isDebugEnabled()) { - this.logger.debug( - "Could not find class file for introspecting @ComponentScan classes: " - + className, ex); - } - } - } - return null; - } - - private List getBasePackages(AnnotationMetadata metadata) { - AnnotationAttributes attributes = AnnotationAttributes - .fromMap((metadata == null ? null : metadata.getAnnotationAttributes( - ComponentScan.class.getName(), true))); - if (attributes != null) { - List basePackages = new ArrayList(); - addAllHavingText(basePackages, attributes.getStringArray("value")); - addAllHavingText(basePackages, attributes.getStringArray("basePackages")); - for (String packageClass : attributes.getStringArray("basePackageClasses")) { - basePackages.add(ClassUtils.getPackageName(packageClass)); - } - if (basePackages.isEmpty()) { - basePackages.add(ClassUtils.getPackageName(metadata.getClassName())); - } - return basePackages; - } - return Collections.emptyList(); - } - - private void addAllHavingText(List list, String[] strings) { - for (String s : strings) { - if (StringUtils.hasText(s)) { - list.add(s); - } - } - } - @Override public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = classLoader; } - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - } diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnClassCondition.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnClassCondition.java index 7b8e5fd4a4..85f62996ec 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnClassCondition.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnClassCondition.java @@ -62,7 +62,7 @@ class OnClassCondition implements Condition { } } if (logger.isDebugEnabled()) { - logger.debug("All classes found (search terminated with matches=true)"); + logger.debug("Match result is: true"); } return true; } diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnMissingBeanCondition.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnMissingBeanCondition.java index 8943e8aa1e..17bacf9054 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnMissingBeanCondition.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnMissingBeanCondition.java @@ -16,9 +16,9 @@ package org.springframework.bootstrap.context.annotation; +import java.util.List; + import org.springframework.context.annotation.Condition; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.core.type.AnnotatedTypeMetadata; /** * {@link Condition} that checks that specific beans are missing. @@ -34,7 +34,7 @@ class OnMissingBeanCondition extends AbstractOnBeanCondition { } @Override - public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { - return !super.matches(context, metadata); + protected boolean evaluate(List beanClassesFound, List beanNamesFound) { + return !super.evaluate(beanClassesFound, beanNamesFound); } } diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnMissingClassCondition.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnMissingClassCondition.java index 3e01b13a22..43697887f4 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnMissingClassCondition.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/OnMissingClassCondition.java @@ -60,6 +60,9 @@ class OnMissingClassCondition implements Condition { } } } + if (logger.isDebugEnabled()) { + logger.debug("Match result is: true"); + } return true; } diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/JpaRepositoriesAutoConfigurationTests.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/JpaRepositoriesAutoConfigurationTests.java new file mode 100644 index 0000000000..2134662cf8 --- /dev/null +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/JpaRepositoriesAutoConfigurationTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2013 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.bootstrap.autoconfigure.data; + +import javax.persistence.EntityManagerFactory; + +import org.junit.Test; +import org.springframework.bootstrap.autoconfigure.data.test.CityRepository; +import org.springframework.bootstrap.autoconfigure.jdbc.EmbeddedDatabaseAutoConfiguration; +import org.springframework.bootstrap.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Dave Syer + * + */ +public class JpaRepositoriesAutoConfigurationTests { + + private AnnotationConfigApplicationContext context; + + @Test + public void testDefaultRepositoryConfiguration() throws Exception { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(TestConfiguration.class, + EmbeddedDatabaseAutoConfiguration.class, + JpaRepositoriesAutoConfiguration.class, + HibernateJpaAutoConfiguration.class); + this.context.refresh(); + assertNotNull(this.context.getBean(CityRepository.class)); + assertNotNull(this.context.getBean(PlatformTransactionManager.class)); + assertNotNull(this.context.getBean(EntityManagerFactory.class)); + } + + @Configuration + @ComponentScan("org.springframework.bootstrap.autoconfigure.data.test") + protected static class TestConfiguration { + + } + +} diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/test/City.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/test/City.java new file mode 100644 index 0000000000..91d6911b59 --- /dev/null +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/test/City.java @@ -0,0 +1,60 @@ +package org.springframework.bootstrap.autoconfigure.data.test; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class City implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String state; + + @Column(nullable = false) + private String country; + + @Column(nullable = false) + private String map; + + protected City() { + } + + public City(String name, String country) { + super(); + this.name = name; + this.country = country; + } + + public String getName() { + return this.name; + } + + public String getState() { + return this.state; + } + + public String getCountry() { + return this.country; + } + + public String getMap() { + return this.map; + } + + @Override + public String toString() { + return getName() + "," + getState() + "," + getCountry(); + } +} diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/test/CityRepository.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/test/CityRepository.java new file mode 100644 index 0000000000..7037700fcc --- /dev/null +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/autoconfigure/data/test/CityRepository.java @@ -0,0 +1,16 @@ +package org.springframework.bootstrap.autoconfigure.data.test; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; + +public interface CityRepository extends Repository { + + Page findAll(Pageable pageable); + + Page findByNameLikeAndCountryLikeAllIgnoringCase(String name, String country, + Pageable pageable); + + City findByNameAndCountryAllIgnoringCase(String name, String country); + +}