Commit 97cc5a7f authored by Phillip Webb's avatar Phillip Webb

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
parent a4143b88
...@@ -166,6 +166,11 @@ ...@@ -166,6 +166,11 @@
<artifactId>hsqldb</artifactId> <artifactId>hsqldb</artifactId>
<version>2.2.9</version> <version>2.2.9</version>
</dependency> </dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>3.0.2</version>
</dependency>
<dependency> <dependency>
<groupId>org.projectreactor</groupId> <groupId>org.projectreactor</groupId>
<artifactId>reactor-spring</artifactId> <artifactId>reactor-spring</artifactId>
......
...@@ -64,6 +64,11 @@ ...@@ -64,6 +64,11 @@
<artifactId>hibernate-validator</artifactId> <artifactId>hibernate-validator</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
......
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<ConfigurableApplicationContext>,
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()));
}
}
}
/*
* 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<Class<?>> 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;
}
}
}
...@@ -4,4 +4,5 @@ org.springframework.boot.context.initializer.ConfigFileApplicationContextInitial ...@@ -4,4 +4,5 @@ org.springframework.boot.context.initializer.ConfigFileApplicationContextInitial
org.springframework.boot.context.initializer.ContextIdApplicationContextInitializer,\ org.springframework.boot.context.initializer.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.initializer.EnvironmentDelegateApplicationContextInitializer,\ org.springframework.boot.context.initializer.EnvironmentDelegateApplicationContextInitializer,\
org.springframework.boot.context.initializer.LoggingApplicationContextInitializer,\ org.springframework.boot.context.initializer.LoggingApplicationContextInitializer,\
org.springframework.boot.context.initializer.VcapApplicationContextInitializer org.springframework.boot.context.initializer.VcapApplicationContextInitializer,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorInitializer
/*
* 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 {
}
}
/*
* 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<Class<?>> implementations = resolver.findImplementations(Logger.class,
"liquibase.logging.core");
assertThat(implementations.size(), greaterThan(0));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment