Use other spring beans in liquibase CustomTaskChange class

The classes referenced in your changeset.xml are not managed by Spring, so the cool stuff like DI will not work.

What you can do is to inject Spring beans into Non-Spring objects. See this answer: https://stackoverflow.com/a/1377740/4365460


I'm currently running through this problem as well...After hours of digging, I found 2 solutions, no AOP is needed.

Liquibase version: 4.1.1


Solution A

In the official example of customChange

https://docs.liquibase.com/change-types/community/custom-change.html

In CustomChange.setFileOpener, ResourceAccessor actually is an inner class SpringLiquibase$SpringResourceOpener, and it has a member 'resourceLoader', which is indeed an ApplicationContext. Unfortunately, it's private and no getter is available.

So here comes an ugly solution: USE REFLECTION TO GET IT AND INVOKE getBean


Solution B (More elegant)

Before we get started, let's see some basic facts about Liquibase. The official way of integrating Liquibase with Spring Boot is by using:

org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration$LiquibaseConfiguration

This is a conditional inner config bean for creating SpringLiquibase ONLY WHEN SpringLiquibase.class IS MISSING

@Configuration
@ConditionalOnMissingBean(SpringLiquibase.class)
@EnableConfigurationProperties({ DataSourceProperties.class,
        LiquibaseProperties.class })
@Import(LiquibaseJpaDependencyConfiguration.class)
public static class LiquibaseConfiguration {...}

So we can create our own SpringLiquibase by adding a liquibase config bean

@Getter
@Configuration
@EnableConfigurationProperties(LiquibaseProperties.class)
public class LiquibaseConfig {

    private DataSource dataSource;

    private LiquibaseProperties properties;

    public LiquibaseConfig(DataSource dataSource, LiquibaseProperties properties) {
        this.dataSource = dataSource;
        this.properties = properties;
    }

    @Bean
    public SpringLiquibase liquibase() {
        SpringLiquibase liquibase = new BeanAwareSpringLiquibase();
        liquibase.setDataSource(dataSource);
        liquibase.setChangeLog(this.properties.getChangeLog());
        liquibase.setContexts(this.properties.getContexts());
        liquibase.setDefaultSchema(this.properties.getDefaultSchema());
        liquibase.setDropFirst(this.properties.isDropFirst());
        liquibase.setShouldRun(this.properties.isEnabled());
        liquibase.setLabels(this.properties.getLabels());
        liquibase.setChangeLogParameters(this.properties.getParameters());
        liquibase.setRollbackFile(this.properties.getRollbackFile());
        return liquibase;
   }
}

inside which we new an extended class of SpringLiquibase: BeanAwareSpringLiquibase

public class BeanAwareSpringLiquibase extends SpringLiquibase {
private static ResourceLoader applicationContext;

public BeanAwareSpringLiquibase() {
}

public static final <T> T getBean(Class<T> beanClass) throws Exception {
    if (ApplicationContext.class.isInstance(applicationContext)) {
        return ((ApplicationContext)applicationContext).getBean(beanClass);
    } else {
        throw new Exception("Resource loader is not an instance of ApplicationContext");
    }
}

public static final <T> T getBean(String beanName) throws Exception {
    if (ApplicationContext.class.isInstance(applicationContext)) {
        return ((ApplicationContext)applicationContext).getBean(beanName);
    } else {
        throw new Exception("Resource loader is not an instance of ApplicationContext");
    }
}

@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
    super.setResourceLoader(resourceLoader);
    applicationContext = resourceLoader;
}}

BeanAwareSpringLiquibase has a static reference to ResourceLoader aforementioned. On Spring Bootstartup, 'setResourceLoader' defined by ResourceLoaderAware interface will be invoked automatically before 'afterPropertiesSet' defined by InitializingBean interface, thus the code execution will be like this:

  1. Spring Boot invokes setResourceLoader, injecting resourceLoader(applicationContext) to BeanAwareSpringLiquibase.

  2. Spring Boot invokes afterPropertiesSet, performing Liquibase update including customChange, by now you already have full access to applicationContext

PS:

  1. Remember adding your Liquibase config bean package path to @ComponentScan or it will still use LiquibaseAutoConfiguration instead of our own LiquibaseConfig.

  2. Prepare all beans you need in 'setUp' before 'execute' would be a better convention.