Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use other spring beans in liquibase CustomTaskChange class

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>

like image 302
SebastianRiemer Avatar asked Sep 28 '15 15:09

SebastianRiemer


1 Answers

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.

like image 126
vince Avatar answered Sep 19 '22 07:09

vince