[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]
This commit is contained in:
Dave Syer 2013-05-16 14:05:07 +01:00
parent ad2e311f2f
commit 73f28a3809
10 changed files with 307 additions and 119 deletions

View File

@ -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
* <code>@Configuration</code> 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<String> basePackages = new ArrayList<String>();
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<String> getBasePackages(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap((metadata == null ? null : metadata.getAnnotationAttributes(
ComponentScan.class.getName(), true)));
if (attributes != null) {
List<String> basePackages = new ArrayList<String>();
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<String> 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;
}
}

View File

@ -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 {
}

View File

@ -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);
}

View File

@ -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<String> factories = new ArrayList<String>(
@ -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<String> basePackages = new ArrayList<String>();
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<String> getBasePackages(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap((metadata == null ? null : metadata.getAnnotationAttributes(
ComponentScan.class.getName(), true)));
if (attributes != null) {
List<String> basePackages = new ArrayList<String>();
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<String> 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;
}
}

View File

@ -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;
}

View File

@ -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<String> beanClassesFound, List<String> beanNamesFound) {
return !super.evaluate(beanClassesFound, beanNamesFound);
}
}

View File

@ -60,6 +60,9 @@ class OnMissingClassCondition implements Condition {
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Match result is: true");
}
return true;
}

View File

@ -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 {
}
}

View File

@ -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();
}
}

View File

@ -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<City, Long> {
Page<City> findAll(Pageable pageable);
Page<City> findByNameLikeAndCountryLikeAllIgnoringCase(String name, String country,
Pageable pageable);
City findByNameAndCountryAllIgnoringCase(String name, String country);
}