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>
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:
Spring Boot invokes setResourceLoader, injecting resourceLoader(applicationContext) to BeanAwareSpringLiquibase.
Spring Boot invokes afterPropertiesSet, performing Liquibase update including customChange, by now you already have full access to applicationContext
PS:
Remember adding your Liquibase config bean package path to @ComponentScan or it will still use LiquibaseAutoConfiguration instead of our own LiquibaseConfig.
Prepare all beans you need in 'setUp' before 'execute' would be a better convention.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With