17

I need to do some data migration, which is too complex to do it in a liquibase changeset. We use spring

That's why I wrote a class implementing the liquibase.change.custom.CustomTaskChange class. I then reference it from within a changeset.

All is fine to this point.

My question is: Is it possible to get access to the other spring beans from within such a class?

When I try to use an autowired bean in this class, it's null, which makes me think that the autowiring is simply not done at this point?

I've also read in some other thread, that the Liquibase bean must be initialized before all other beans, is that correct?

Here is a snippet of the class I wrote:

@Component
public class UpdateJob2 implements CustomTaskChange {

private String param1;

@Autowired
private SomeBean someBean;

@Override
public void execute(Database database) throws CustomChangeException {
    try {
        List<SomeObject> titleTypes = someBean.getSomeObjects(
                param1
        );
    } catch (Exception e) {         
        throw new CustomChangeException();
    }
...

I get an exception and when debugging I can see that someBean is null.

Here is the config for the SpringLiquibase:

@Configuration
@EnableTransactionManagement(proxyTargetClass = true)
@ComponentScan({
"xxx.xxx.."})
public class DatabaseConfiguration {

@Bean
public SpringLiquibase springLiquibase() {
    SpringLiquibase liquibase = new SpringLiquibase();
    liquibase.setDataSource(dataSource());
    liquibase.setChangeLog("classpath:liquibase-changelog.xml");
    return liquibase;
}
...

Some more config:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
     http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

    <includeAll path="dbschema"/>

</databaseChangeLog>

And here the call from the changeset:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
     http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

<changeSet id="201509281536" author="sr">
        <customChange class="xxx.xxx.xxx.UpdateJob2">
            <param name="param1" value="2" />
        </customChange>
</changeSet>

SebastianRiemer
  • 1,495
  • 2
  • 20
  • 33

3 Answers3

7

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.

vince
  • 176
  • 1
  • 4
  • 1
    How do you get a hold of BeanAwareSpringLiquibase.getBean(...) from the CustomTaskChange object? – George Stanchev Oct 13 '21 at 17:46
  • 1
    BeanAwareSpringLiquibase.getBean is static, just call it. – vince Oct 19 '21 at 02:12
  • If this is only creating static methods to access the context, is there any benefit to extending SpringLiquibase? Would it be any different than be a standalone bean that exposes the application context? – mouse_8b Feb 19 '22 at 18:51
  • this doesn't work when you are about to create new schema with Liquibase. The SpringLiquibase runs before the schema creation and as it doesn't find the schema (even defined with preliquibase-postgresql.sql or spring.liquibase.default-schema), it throws an error. Also, if you set 'shouldRun' false, it ignore the schema but fails when trying to map JPA entities with DB tables with Table not found error (you will have JPA entities mapping to DB tables to be created) Is there any solution to this problem with Liquibase? – Sanjay Amin Sep 16 '22 at 18:26
  • @SanjayAmin Sorry I didn't try with schema change – vince Sep 20 '22 at 08:58
4

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

Community
  • 1
  • 1
Roland Weisleder
  • 9,668
  • 7
  • 37
  • 59
  • Thanks for the response. Good to know that I am not missing something here. I think I'll go with a custom solution. – SebastianRiemer Sep 29 '15 at 08:00
  • @ThomasStubbe The solution is to expose the ApplicationContext in a bean managed by Spring. Then the CustomChange can use the ApplicationContext to access other Spring beans. If this doesn't work for you, please create a new question with more details. – Roland Weisleder Apr 13 '18 at 14:54
  • I bypassed the problem by using a application migration after startup... The problem with Liquibase I think is that liquibase runs before the spring the spring beans are initialized. – Thomas Stubbe Apr 13 '18 at 15:35
1

I accomplished this by overriding the Spring Liquibase configuration and setting a static field on the custom task. Setting the fields in the configuration ensures that it is set before the changeset runs.

This is not possible to do with every bean, because some beans (like JPA repositories) are dependent on the liquibase bean. Liquibase runs changelogs when the SpringLiquibase bean is initialized, but the entire Spring context is not completely loaded at that point. If you try to autowire a bean that depends on liquibase, you'll get an exception on startup.

I also think that this technique is safer than exposing the entire application context statically. Only the fields that are needed are passed to the task, and those are not publicly accessible afterward.

/**
    Task that has a static member that will be set in the LiquibaseConfiguration class.
*/
public class MyCustomTask implements CustomTaskChange {
    private static MyBean myBean;

    public static void setMyBean(MyBean myBean) {
        MyCustomTask.myBean = myBean;
    }

    @Override
    public void execute(Database database) throws CustomChangeException {

        try {
            JdbcConnection jdbcConnection = (JdbcConnection) database.getConnection();

            // do stuff using myBean

        } catch (DatabaseException | SQLException e) {
            throw new CustomChangeException(e);
        }
    }

}

/**
    Extend SpringBoot Liquibase Auto-Configuration
    org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseConfiguration
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(SpringLiquibase.class)
@EnableConfigurationProperties({DataSourceProperties.class, LiquibaseProperties.class})
public static class MyLiquibaseConfiguration
        extends LiquibaseAutoConfiguration.LiquibaseConfiguration {

    /**
     * Autowire myBean and set it on {@link MyCustomTask}.
     *
     * @param properties The {@link LiquibaseProperties} to configure Liquibase.
     * @param myBean my bean.
     */
    public MigrationLiquibaseConfiguration(LiquibaseProperties properties, MyBean myBean) {
        super(properties);
        MyCustomTask.setMyBean(myBean);
    }

}
mouse_8b
  • 514
  • 5
  • 8