diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml
index 44e6d34c92d..af7ab709e58 100644
--- a/spring-boot-dependencies/pom.xml
+++ b/spring-boot-dependencies/pom.xml
@@ -166,6 +166,11 @@
hsqldb
2.2.9
+
+ org.liquibase
+ liquibase-core
+ 3.0.2
+
org.projectreactor
reactor-spring
diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml
index 23ceb1d5ae5..2a0b340c977 100644
--- a/spring-boot/pom.xml
+++ b/spring-boot/pom.xml
@@ -64,6 +64,11 @@
hibernate-validator
true
+
+ org.liquibase
+ liquibase-core
+ true
+
org.slf4j
slf4j-api
diff --git a/spring-boot/src/main/java/org/springframework/boot/liquibase/LiquibaseServiceLocatorInitializer.java b/spring-boot/src/main/java/org/springframework/boot/liquibase/LiquibaseServiceLocatorInitializer.java
new file mode 100644
index 00000000000..2986b04c49a
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/liquibase/LiquibaseServiceLocatorInitializer.java
@@ -0,0 +1,50 @@
+package org.springframework.boot.liquibase;
+
+import liquibase.servicelocator.CustomResolverServiceLocator;
+import liquibase.servicelocator.ServiceLocator;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.SpringApplicationInitializer;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.util.ClassUtils;
+
+/**
+ * {@link SpringApplicationInitializer} that replaces the liquibase {@link ServiceLocator}
+ * with a version that works with Spring Boot executable archives.
+ *
+ * @author Phillip Webb
+ */
+public class LiquibaseServiceLocatorInitializer implements
+ ApplicationContextInitializer,
+ SpringApplicationInitializer {
+
+ static final Log logger = LogFactory
+ .getLog(LiquibaseServiceLocatorInitializer.class);
+
+ @Override
+ public void initialize(SpringApplication springApplication, String[] args) {
+ if (ClassUtils.isPresent("liquibase.servicelocator.ServiceLocator", null)) {
+ new LiquibasePresent().replaceServiceLocator();
+ }
+ }
+
+ @Override
+ public void initialize(ConfigurableApplicationContext applicationContext) {
+ }
+
+ /**
+ * Inner class to prevent class not found issues
+ */
+ private static class LiquibasePresent {
+
+ public void replaceServiceLocator() {
+ ServiceLocator.setInstance(new CustomResolverServiceLocator(
+ new SpringPackageScanClassResolver()));
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/liquibase/SpringPackageScanClassResolver.java b/spring-boot/src/main/java/org/springframework/boot/liquibase/SpringPackageScanClassResolver.java
new file mode 100644
index 00000000000..c9cf370238e
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/liquibase/SpringPackageScanClassResolver.java
@@ -0,0 +1,85 @@
+/*
+ * 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.boot.liquibase;
+
+import java.io.IOException;
+import java.util.Set;
+
+import liquibase.servicelocator.DefaultPackageScanClassResolver;
+import liquibase.servicelocator.PackageScanClassResolver;
+import liquibase.servicelocator.PackageScanFilter;
+
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Liquibase {@link PackageScanClassResolver} implementation that uses Spring's resource
+ * scanning to locate classes. This variant is safe to use with Spring Boot packaged
+ * executable JARs.
+ *
+ * @author Phillip Webb
+ */
+public class SpringPackageScanClassResolver extends DefaultPackageScanClassResolver {
+
+ @Override
+ protected void find(PackageScanFilter test, String packageName, ClassLoader loader,
+ Set> classes) {
+ MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(
+ loader);
+ try {
+ Resource[] resources = scan(loader, packageName);
+ for (Resource resource : resources) {
+ Class> candidate = loadClass(loader, metadataReaderFactory, resource);
+ if (candidate != null && test.matches(candidate)) {
+ classes.add(candidate);
+ }
+ }
+ }
+ catch (IOException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private Resource[] scan(ClassLoader loader, String packageName) throws IOException {
+ ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(loader);
+ String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ + ClassUtils.convertClassNameToResourcePath(packageName) + "/**/*.class";
+ Resource[] resources = resolver.getResources(pattern);
+ return resources;
+ }
+
+ private Class> loadClass(ClassLoader loader, MetadataReaderFactory readerFactory,
+ Resource resource) {
+ try {
+ MetadataReader reader = readerFactory.getMetadataReader(resource);
+ return ClassUtils.forName(reader.getClassMetadata().getClassName(), loader);
+ }
+ catch (Exception ex) {
+ if (LiquibaseServiceLocatorInitializer.logger.isWarnEnabled()) {
+ LiquibaseServiceLocatorInitializer.logger.warn(
+ "Ignoring cadidate class resource " + resource, ex);
+ }
+ return null;
+ }
+ }
+
+}
diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories
index d784cb00053..bd31e2ea5e2 100644
--- a/spring-boot/src/main/resources/META-INF/spring.factories
+++ b/spring-boot/src/main/resources/META-INF/spring.factories
@@ -4,4 +4,5 @@ org.springframework.boot.context.initializer.ConfigFileApplicationContextInitial
org.springframework.boot.context.initializer.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.initializer.EnvironmentDelegateApplicationContextInitializer,\
org.springframework.boot.context.initializer.LoggingApplicationContextInitializer,\
-org.springframework.boot.context.initializer.VcapApplicationContextInitializer
+org.springframework.boot.context.initializer.VcapApplicationContextInitializer,\
+org.springframework.boot.liquibase.LiquibaseServiceLocatorInitializer
diff --git a/spring-boot/src/test/java/org/springframework/boot/liquibase/LiquibaseServiceLocatorInitializerTests.java b/spring-boot/src/test/java/org/springframework/boot/liquibase/LiquibaseServiceLocatorInitializerTests.java
new file mode 100644
index 00000000000..efc3ba2fe63
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/liquibase/LiquibaseServiceLocatorInitializerTests.java
@@ -0,0 +1,55 @@
+/*
+ * 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.boot.liquibase;
+
+import java.lang.reflect.Field;
+
+import liquibase.servicelocator.ServiceLocator;
+
+import org.junit.Test;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.util.ReflectionUtils;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Tests for {@link LiquibaseServiceLocatorInitializer}.
+ *
+ * @author Phillip Webb
+ */
+public class LiquibaseServiceLocatorInitializerTests {
+
+ @Test
+ public void replacesServiceLocator() throws Exception {
+ SpringApplication application = new SpringApplication(Conf.class);
+ application.setWebEnvironment(false);
+ application.run();
+ ServiceLocator instance = ServiceLocator.getInstance();
+ Field field = ReflectionUtils.findField(ServiceLocator.class, "classResolver");
+ field.setAccessible(true);
+ Object resolver = field.get(instance);
+ assertThat(resolver, instanceOf(SpringPackageScanClassResolver.class));
+ }
+
+ @Configuration
+ public static class Conf {
+
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/liquibase/SpringPackageScanClassResolverTests.java b/spring-boot/src/test/java/org/springframework/boot/liquibase/SpringPackageScanClassResolverTests.java
new file mode 100644
index 00000000000..c8c2ed62c7d
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/liquibase/SpringPackageScanClassResolverTests.java
@@ -0,0 +1,44 @@
+/*
+ * 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.boot.liquibase;
+
+import java.util.Set;
+
+import liquibase.logging.Logger;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.greaterThan;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Tests for SpringPackageScanClassResolver.
+ *
+ * @author Phillip Webb
+ */
+public class SpringPackageScanClassResolverTests {
+
+ @Test
+ public void testScan() {
+ SpringPackageScanClassResolver resolver = new SpringPackageScanClassResolver();
+ resolver.addClassLoader(getClass().getClassLoader());
+ Set> implementations = resolver.findImplementations(Logger.class,
+ "liquibase.logging.core");
+ assertThat(implementations.size(), greaterThan(0));
+ }
+
+}