diff --git a/spring-boot-devtools/pom.xml b/spring-boot-devtools/pom.xml
index db189e46ec..c35f8480f2 100644
--- a/spring-boot-devtools/pom.xml
+++ b/spring-boot-devtools/pom.xml
@@ -116,6 +116,11 @@
postgresql
test
+
+ org.apache.tomcat
+ tomcat-jdbc
+ test
+
org.springframework.boot
spring-boot-starter-thymeleaf
diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java
index 0e76695220..6e446e38f7 100644
--- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java
+++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java
@@ -23,18 +23,24 @@ import java.util.Set;
import javax.sql.DataSource;
import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
+import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
+import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration.DevToolsDataSourceCondition;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.ConfigurationCondition;
+import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
@@ -109,20 +115,37 @@ public class DevToolsDataSourceAutoConfiguration {
}
- static class DevToolsDataSourceCondition extends AllNestedConditions {
-
- DevToolsDataSourceCondition() {
- super(ConfigurationPhase.REGISTER_BEAN);
- }
-
- @ConditionalOnBean(DataSource.class)
- static final class DataSourceBean {
+ static class DevToolsDataSourceCondition extends SpringBootCondition
+ implements ConfigurationCondition {
+ @Override
+ public ConfigurationPhase getConfigurationPhase() {
+ return ConfigurationPhase.REGISTER_BEAN;
}
- @ConditionalOnBean(DataSourceProperties.class)
- static final class DataSourcePropertiesBean {
-
+ @Override
+ public ConditionOutcome getMatchOutcome(ConditionContext context,
+ AnnotatedTypeMetadata metadata) {
+ String[] dataSourceBeanNames = context.getBeanFactory()
+ .getBeanNamesForType(DataSource.class);
+ if (dataSourceBeanNames.length != 1) {
+ return ConditionOutcome
+ .noMatch("A single DataSource bean was not found in the context");
+ }
+ if (context.getBeanFactory()
+ .getBeanNamesForType(DataSourceProperties.class).length != 1) {
+ return ConditionOutcome.noMatch(
+ "A single DataSourceProperties bean was not found in the context");
+ }
+ BeanDefinition dataSourceDefinition = context.getRegistry()
+ .getBeanDefinition(dataSourceBeanNames[0]);
+ if (dataSourceDefinition instanceof AnnotatedBeanDefinition
+ && ((AnnotatedBeanDefinition) dataSourceDefinition)
+ .getFactoryMethodMetadata().getDeclaringClassName()
+ .startsWith(DataSourceAutoConfiguration.class.getName())) {
+ return ConditionOutcome.match("Found auto-configured DataSource");
+ }
+ return ConditionOutcome.noMatch("DataSource was not auto-configured");
}
}
diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/AbstractDevToolsDataSourceAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/AbstractDevToolsDataSourceAutoConfigurationTests.java
new file mode 100644
index 0000000000..61d341cb92
--- /dev/null
+++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/AbstractDevToolsDataSourceAutoConfigurationTests.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2012-2016 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.devtools.autoconfigure;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collection;
+
+import javax.sql.DataSource;
+
+import org.junit.Test;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.EnvironmentTestUtils;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Base class for tests for {@link DevToolsDataSourceAutoConfiguration}.
+ *
+ * @author Andy Wilkinson
+ */
+public class AbstractDevToolsDataSourceAutoConfigurationTests {
+
+ @Test
+ public void singleManuallyConfiguredDataSourceIsNotClosed() throws SQLException {
+ ConfigurableApplicationContext context = createContext(
+ DataSourcePropertiesConfiguration.class,
+ SingleDataSourceConfiguration.class);
+ DataSource dataSource = context.getBean(DataSource.class);
+ Statement statement = configureDataSourceBehaviour(dataSource);
+ verify(statement, times(0)).execute("SHUTDOWN");
+ }
+
+ @Test
+ public void multipleDataSourcesAreIgnored() throws SQLException {
+ ConfigurableApplicationContext context = createContext(
+ DataSourcePropertiesConfiguration.class,
+ MultipleDataSourcesConfiguration.class);
+ Collection dataSources = context.getBeansOfType(DataSource.class)
+ .values();
+ for (DataSource dataSource : dataSources) {
+ Statement statement = configureDataSourceBehaviour(dataSource);
+ verify(statement, times(0)).execute("SHUTDOWN");
+ }
+ }
+
+ protected final Statement configureDataSourceBehaviour(DataSource dataSource)
+ throws SQLException {
+ Connection connection = mock(Connection.class);
+ Statement statement = mock(Statement.class);
+ doReturn(connection).when(dataSource).getConnection();
+ given(connection.createStatement()).willReturn(statement);
+ return statement;
+ }
+
+ protected final ConfigurableApplicationContext createContext(String driverClassName,
+ Class>... classes) {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ context.register(classes);
+ context.register(DevToolsDataSourceAutoConfiguration.class);
+ if (driverClassName != null) {
+ EnvironmentTestUtils.addEnvironment(context,
+ "spring.datasource.driver-class-name:" + driverClassName);
+ }
+ context.refresh();
+ return context;
+ }
+
+ protected final ConfigurableApplicationContext createContext(Class>... classes) {
+ return this.createContext(null, classes);
+ }
+
+ @Configuration
+ static class SingleDataSourceConfiguration {
+
+ @Bean
+ public DataSource dataSource() {
+ return mock(DataSource.class);
+ }
+
+ }
+
+ @Configuration
+ static class MultipleDataSourcesConfiguration {
+
+ @Bean
+ public DataSource dataSourceOne() {
+ return mock(DataSource.class);
+ }
+
+ @Bean
+ public DataSource dataSourceTwo() {
+ return mock(DataSource.class);
+ }
+
+ }
+
+ @Configuration
+ @EnableConfigurationProperties(DataSourceProperties.class)
+ static class DataSourcePropertiesConfiguration {
+
+ }
+
+ @Configuration
+ static class DataSourceSpyConfiguration {
+
+ @Bean
+ public DataSourceSpyBeanPostProcessor dataSourceSpyBeanPostProcessor() {
+ return new DataSourceSpyBeanPostProcessor();
+ }
+
+ }
+
+ private static class DataSourceSpyBeanPostProcessor implements BeanPostProcessor {
+
+ @Override
+ public Object postProcessBeforeInitialization(Object bean, String beanName)
+ throws BeansException {
+ if (bean instanceof DataSource) {
+ bean = spy(bean);
+ }
+ return bean;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName)
+ throws BeansException {
+ return bean;
+ }
+
+ }
+
+}
diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationTests.java
deleted file mode 100644
index 8e5a711e7c..0000000000
--- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationTests.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright 2012-2016 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.devtools.autoconfigure;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-import javax.persistence.EntityManagerFactory;
-import javax.sql.DataSource;
-
-import org.junit.Test;
-import org.mockito.InOrder;
-
-import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.boot.test.EnvironmentTestUtils;
-import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
-
-import static org.hamcrest.Matchers.is;
-import static org.junit.Assert.assertThat;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-/**
- * Tests for {@link DevToolsDataSourceAutoConfiguration}.
- *
- * @author Andy Wilkinson
- */
-public class DevToolsDataSourceAutoConfigurationTests {
-
- @Test
- public void embeddedDatabaseIsNotShutDown() throws SQLException {
- ConfigurableApplicationContext context = createContextWithDriver("org.h2.Driver",
- EmbeddedDatabaseConfiguration.class);
- DataSource dataSource = context.getBean(DataSource.class);
- context.close();
- verify(dataSource, times(0)).getConnection();
- }
-
- @Test
- public void externalDatabaseIsNotShutDown() throws SQLException {
- ConfigurableApplicationContext context = createContextWithDriver(
- "org.postgresql.Driver", DataSourceConfiguration.class);
- DataSource dataSource = context.getBean(DataSource.class);
- context.close();
- verify(dataSource, times(0)).getConnection();
- }
-
- @Test
- public void nonEmbeddedInMemoryDatabaseConfiguredWithDriverIsShutDown()
- throws SQLException {
- ConfigurableApplicationContext context = createContextWithDriver("org.h2.Driver",
- DataSourceConfiguration.class);
- DataSource dataSource = context.getBean(DataSource.class);
- Connection connection = mock(Connection.class);
- given(dataSource.getConnection()).willReturn(connection);
- Statement statement = mock(Statement.class);
- given(connection.createStatement()).willReturn(statement);
- context.close();
- verify(statement).execute("SHUTDOWN");
- }
-
- @Test
- public void nonEmbeddedInMemoryDatabaseConfiguredWithUrlIsShutDown()
- throws SQLException {
- ConfigurableApplicationContext context = createContextWithUrl("jdbc:h2:mem:test",
- DataSourceConfiguration.class);
- DataSource dataSource = context.getBean(DataSource.class);
- Connection connection = mock(Connection.class);
- given(dataSource.getConnection()).willReturn(connection);
- Statement statement = mock(Statement.class);
- given(connection.createStatement()).willReturn(statement);
- context.close();
- verify(statement).execute("SHUTDOWN");
- }
-
- @Test
- public void configurationBacksOffWithoutDataSourceProperties() throws SQLException {
- ConfigurableApplicationContext context = createContext("org.h2.Driver",
- NoDataSourcePropertiesConfiguration.class);
- assertThat(
- context.getBeansOfType(DevToolsDataSourceAutoConfiguration.class).size(),
- is(0));
- }
-
- @Test
- public void entityManagerFactoryIsClosedBeforeDatabaseIsShutDown()
- throws SQLException {
- ConfigurableApplicationContext context = createContextWithUrl("jdbc:h2:mem:test",
- DataSourceConfiguration.class, EntityManagerFactoryConfiguration.class);
- DataSource dataSource = context.getBean(DataSource.class);
- Connection connection = mock(Connection.class);
- given(dataSource.getConnection()).willReturn(connection);
- Statement statement = mock(Statement.class);
- given(connection.createStatement()).willReturn(statement);
- EntityManagerFactory entityManagerFactory = context
- .getBean(EntityManagerFactory.class);
- context.close();
- InOrder inOrder = inOrder(statement, entityManagerFactory);
- inOrder.verify(statement).execute("SHUTDOWN");
- inOrder.verify(entityManagerFactory).close();
- }
-
- private ConfigurableApplicationContext createContextWithDriver(String driver,
- Class>... classes) {
- return createContext("spring.datasource.driver-class-name:" + driver, classes);
- }
-
- private ConfigurableApplicationContext createContextWithUrl(String url,
- Class>... classes) {
- return createContext("spring.datasource.url:" + url, classes);
- }
-
- private ConfigurableApplicationContext createContext(String property,
- Class>... classes) {
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
- context.register(classes);
- context.register(DevToolsDataSourceAutoConfiguration.class);
- EnvironmentTestUtils.addEnvironment(context, property);
- context.refresh();
- return context;
- }
-
- @Configuration
- @EnableConfigurationProperties(DataSourceProperties.class)
- static class EmbeddedDatabaseConfiguration {
-
- @Bean
- public EmbeddedDatabase embeddedDatabase() {
- return mock(EmbeddedDatabase.class);
- }
- }
-
- @Configuration
- @EnableConfigurationProperties(DataSourceProperties.class)
- static class DataSourceConfiguration {
-
- @Bean
- public DataSource dataSource() {
- return mock(DataSource.class);
- }
-
- }
-
- @Configuration
- static class NoDataSourcePropertiesConfiguration {
-
- @Bean
- public DataSource dataSource() {
- return mock(DataSource.class);
- }
-
- }
-
- @Configuration
- static class EntityManagerFactoryConfiguration {
-
- @Bean
- public EntityManagerFactory entityManagerFactory() {
- return mock(EntityManagerFactory.class);
- }
-
- }
-
-}
diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsEmbeddedDataSourceAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsEmbeddedDataSourceAutoConfigurationTests.java
new file mode 100644
index 0000000000..107af415d8
--- /dev/null
+++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsEmbeddedDataSourceAutoConfigurationTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012-2016 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.devtools.autoconfigure;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import javax.sql.DataSource;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.devtools.test.ClassPathExclusions;
+import org.springframework.boot.devtools.test.FilteredClassPathRunner;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link DevToolsDataSourceAutoConfiguration} with an embedded data source.
+ *
+ * @author Andy Wilkinson
+ */
+@RunWith(FilteredClassPathRunner.class)
+@ClassPathExclusions("tomcat-jdbc-*.jar")
+public class DevToolsEmbeddedDataSourceAutoConfigurationTests
+ extends AbstractDevToolsDataSourceAutoConfigurationTests {
+
+ @Test
+ public void autoConfiguredDataSourceIsNotShutdown() throws SQLException {
+ ConfigurableApplicationContext context = createContext(
+ DataSourceAutoConfiguration.class, DataSourceSpyConfiguration.class);
+ Statement statement = configureDataSourceBehaviour(
+ context.getBean(DataSource.class));
+ context.close();
+ verify(statement, times(0)).execute("SHUTDOWN");
+ }
+
+}
diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java
new file mode 100644
index 0000000000..050ae6308f
--- /dev/null
+++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012-2016 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.devtools.autoconfigure;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import javax.sql.DataSource;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.devtools.test.FilteredClassPathRunner;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link DevToolsDataSourceAutoConfiguration} with a pooled data source.
+ *
+ * @author Andy Wilkinson
+ */
+@RunWith(FilteredClassPathRunner.class)
+public class DevToolsPooledDataSourceAutoConfigurationTests
+ extends AbstractDevToolsDataSourceAutoConfigurationTests {
+
+ @Test
+ public void autoConfiguredInMemoryDataSourceIsShutdown() throws SQLException {
+ ConfigurableApplicationContext context = createContext(
+ DataSourceAutoConfiguration.class, DataSourceSpyConfiguration.class);
+ Statement statement = configureDataSourceBehaviour(
+ context.getBean(DataSource.class));
+ context.close();
+ verify(statement).execute("SHUTDOWN");
+ }
+
+ @Test
+ public void autoConfiguredExternalDataSourceIsNotShutdown() throws SQLException {
+ ConfigurableApplicationContext context = createContext("org.postgresql.Driver",
+ DataSourceAutoConfiguration.class, DataSourceSpyConfiguration.class);
+ Statement statement = configureDataSourceBehaviour(
+ context.getBean(DataSource.class));
+ context.close();
+ verify(statement, times(0)).execute("SHUTDOWN");
+ }
+
+}
diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/ClassPathExclusions.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/ClassPathExclusions.java
new file mode 100644
index 0000000000..a6dafff28a
--- /dev/null
+++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/ClassPathExclusions.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2012-2016 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.devtools.test;
+
+import java.io.File;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used in combination with {@link FilteredClassPathRunner} to exclude entries
+ * from the classpath.
+ *
+ * @author Andy Wilkinson
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ClassPathExclusions {
+
+ /**
+ * One or more Ant-style patterns that identify entries to be excluded from the class
+ * path. Matching is performed against an entry's {@link File#getName() file name}.
+ * For example, to exclude Hibernate Validator from the classpath,
+ * {@code "hibernate-validator-*.jar"} can be used.
+ * @return the exclusion patterns
+ */
+ String[] value();
+
+}
diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/FilteredClassPathRunner.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/FilteredClassPathRunner.java
new file mode 100644
index 0000000000..9e9e3839af
--- /dev/null
+++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/FilteredClassPathRunner.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2012-2016 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.devtools.test;
+
+import java.io.File;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.TestClass;
+
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.StringUtils;
+
+/**
+ * A custom {@link BlockJUnit4ClassRunner} that runs tests using a filtered class path.
+ * Entries are excluded from the class path using {@link ClassPathExclusions} on the test
+ * class. A class loader is created with the customized class path and is used both to
+ * load the test class and as the thread context class loader while the test is being run.
+ *
+ * @author Andy Wilkinson
+ */
+public class FilteredClassPathRunner extends BlockJUnit4ClassRunner {
+
+ public FilteredClassPathRunner(Class> testClass) throws InitializationError {
+ super(testClass);
+ }
+
+ @Override
+ protected TestClass createTestClass(Class> testClass) {
+ try {
+ ClassLoader classLoader = createTestClassLoader(testClass);
+ return new FilteredTestClass(classLoader, testClass.getName());
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private URLClassLoader createTestClassLoader(Class> testClass) throws Exception {
+ URLClassLoader classLoader = (URLClassLoader) this.getClass().getClassLoader();
+ return new URLClassLoader(filterUrls(extractUrls(classLoader), testClass),
+ classLoader.getParent());
+ }
+
+ private URL[] extractUrls(URLClassLoader classLoader) throws Exception {
+ List extractedUrls = new ArrayList();
+ for (URL url : classLoader.getURLs()) {
+ if (isSurefireBooterJar(url)) {
+ extractedUrls.addAll(extractUrlsFromManifestClassPath(url));
+ }
+ else {
+ extractedUrls.add(url);
+ }
+ }
+ return extractedUrls.toArray(new URL[extractedUrls.size()]);
+ }
+
+ private boolean isSurefireBooterJar(URL url) {
+ return url.getPath().contains("surefirebooter");
+ }
+
+ private List extractUrlsFromManifestClassPath(URL booterJar) throws Exception {
+ List urls = new ArrayList();
+ for (String entry : getClassPath(booterJar)) {
+ urls.add(new URL(entry));
+ }
+ return urls;
+ }
+
+ private String[] getClassPath(URL booterJar) throws Exception {
+ JarFile jarFile = new JarFile(new File(booterJar.toURI()));
+ try {
+ return StringUtils.delimitedListToStringArray(jarFile.getManifest()
+ .getMainAttributes().getValue(Attributes.Name.CLASS_PATH), " ");
+ }
+ finally {
+ jarFile.close();
+ }
+ }
+
+ private URL[] filterUrls(URL[] urls, Class> testClass) throws Exception {
+ ClassPathEntryFilter filter = new ClassPathEntryFilter(testClass);
+ List filteredUrls = new ArrayList();
+ for (URL url : urls) {
+ if (!filter.isExcluded(url)) {
+ filteredUrls.add(url);
+ }
+ }
+ return filteredUrls.toArray(new URL[filteredUrls.size()]);
+ }
+
+ /**
+ * Filter for class path entries.
+ */
+ private static final class ClassPathEntryFilter {
+
+ private final List exclusions;
+
+ private final AntPathMatcher matcher = new AntPathMatcher();
+
+ private ClassPathEntryFilter(Class> testClass) throws Exception {
+ ClassPathExclusions exclusions = AnnotationUtils.findAnnotation(testClass,
+ ClassPathExclusions.class);
+ this.exclusions = exclusions == null ? Collections.emptyList()
+ : Arrays.asList(exclusions.value());
+ }
+
+ private boolean isExcluded(URL url) throws Exception {
+ if (!"file".equals(url.getProtocol())) {
+ return false;
+ }
+ String name = new File(url.toURI()).getName();
+ for (String exclusion : this.exclusions) {
+ if (this.matcher.match(exclusion, name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Filtered version of JUnit's {@link TestClass}.
+ */
+ private static final class FilteredTestClass extends TestClass {
+
+ private final ClassLoader classLoader;
+
+ FilteredTestClass(ClassLoader classLoader, String testClassName)
+ throws ClassNotFoundException {
+ super(classLoader.loadClass(testClassName));
+ this.classLoader = classLoader;
+ }
+
+ @Override
+ public List getAnnotatedMethods(
+ Class extends Annotation> annotationClass) {
+ try {
+ return getAnnotatedMethods(annotationClass.getName());
+ }
+ catch (ClassNotFoundException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private List getAnnotatedMethods(String annotationClassName)
+ throws ClassNotFoundException {
+ Class extends Annotation> annotationClass = (Class extends Annotation>) this.classLoader
+ .loadClass(annotationClassName);
+ List methods = super.getAnnotatedMethods(annotationClass);
+ return wrapFrameworkMethods(methods);
+ }
+
+ private List wrapFrameworkMethods(
+ List methods) {
+ List wrapped = new ArrayList(
+ methods.size());
+ for (FrameworkMethod frameworkMethod : methods) {
+ wrapped.add(new FilteredFrameworkMethod(this.classLoader,
+ frameworkMethod.getMethod()));
+ }
+ return wrapped;
+ }
+
+ }
+
+ /**
+ * Filtered version of JUnit's {@link FrameworkMethod}.
+ */
+ private static final class FilteredFrameworkMethod extends FrameworkMethod {
+
+ private final ClassLoader classLoader;
+
+ private FilteredFrameworkMethod(ClassLoader classLoader, Method method) {
+ super(method);
+ this.classLoader = classLoader;
+ }
+
+ @Override
+ public Object invokeExplosively(Object target, Object... params)
+ throws Throwable {
+ ClassLoader originalClassLoader = Thread.currentThread()
+ .getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(this.classLoader);
+ try {
+ return super.invokeExplosively(target, params);
+ }
+ finally {
+ Thread.currentThread().setContextClassLoader(originalClassLoader);
+ }
+ }
+
+ }
+
+}