From 0fa0082b2a22065a9c720f8c8ae91c99fb7e9805 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 22 Aug 2013 16:31:51 -0700 Subject: [PATCH] Support for liquibase in executable jars Create LiquibaseServiceLocatorInitializer to replace the standard liquibase classpath scanning logic with SpringPackageScanClassResolver which will work correctly in Spring Boot packaged executable JARs. Issue: #55580628 --- spring-boot-dependencies/pom.xml | 5 ++ spring-boot/pom.xml | 5 ++ .../LiquibaseServiceLocatorInitializer.java | 50 +++++++++++ .../SpringPackageScanClassResolver.java | 85 +++++++++++++++++++ .../main/resources/META-INF/spring.factories | 3 +- ...quibaseServiceLocatorInitializerTests.java | 55 ++++++++++++ .../SpringPackageScanClassResolverTests.java | 44 ++++++++++ 7 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/liquibase/LiquibaseServiceLocatorInitializer.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/liquibase/SpringPackageScanClassResolver.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/liquibase/LiquibaseServiceLocatorInitializerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/liquibase/SpringPackageScanClassResolverTests.java 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)); + } + +}