Test Config Beans Loaded into Spring Context During App Run - java

I'm working on a spring boot (v 2.2.4) app, specifically to add integration tests which leverage Testcontainers to instantiate a docker container that runs a Postgres instance for tests to perform database transactions against. The tests push our database schema into the Postgres instance via Liquibase. I implemented this following this guide. The connection to the test time Postgres is managed by a class called TestPostgresConfig.java (See below). The liquibase operations are performed by a SpringLiquibase object defined in the same class. I run into a problem when I try running the application after successfully building. The issue is the Spring context tries to instantiate the SpringLiquibase bean at runtime (fails due to db.changelog-master.yaml not being found) and I don't want it to do so:
WARN [main]
org.springframework.context.support.AbstractApplicationContext:
Exception encountered during context initialization - cancelling
refresh attempt:
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'liquibase' defined in class path
resource
[org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration$LiquibaseConfiguration.class]:
Invocation of init method failed; nested exception is
liquibase.exception.ChangeLogParseException: Error parsing
classpath:db/changelog/changelog-master.yaml
Cause by java.io.FileNotFoundException class path resource
[db/changelog/changelog-master.yaml] cannot be resolved to URL because
it does not exist
This file does not exist, will never exist in this project, and liquibase should not be trying to push change logs at runtime in the first place. I need help figuring out why Spring tries to load the liquibase bean so I can keep that from happening at runtime.
My set up:
#SpringBootApplication
#EnableRetry
#EnableCommonModule
#EnableScheduling
#Slf4j
#EnableConfigurationProperties({
ExternalProperties.class,
ApplicationProperties.class
})
public class MyApplication implements WebMvcConfigurer, CommandLineRunner {
#Autowired
MyService myService;
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
public void run(String... args) throws Exception {
myService.doSomething();
}
}
TestPostgresConfig.java:
#TestConfiguration
#Profile("integration")
public class TestPostgresConfig {
#Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("org.postgresql.Driver");
ds.setUrl(format("jdbc:postgresql://%s:%s/%s", MyIT.psqlContainer.getContainerIpAddress(),
MyIT.psqlContainer.getMappedPort(5432), MyIT.psqlContainer.getDatabaseName()));
ds.setUsername(MyIT.psqlContainer.getUsername());
ds.setPassword(MyIT.psqlContainer.getPassword());
ds.setSchema(MyIT.psqlContainer.getDatabaseName());
return ds;
}
#Bean
public SpringLiquibase springLiquibase(DataSource dataSource) throws SQLException {
tryToCreateSchema(dataSource);
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setDropFirst(true);
liquibase.setDataSource(dataSource);
liquibase.setDefaultSchema("the_schema");
// This and all supported liquibase changelog files are copied onto my classpath
// via the maven assembly plugin. The config to do this has been omitted for the
// sake of brevity
// see this URL for how I did it:
// https://blog.sonatype.com/2008/04/how-to-share-resources-across-projects-in-maven/
liquibase.setChangeLog("classpath:/test/location/of/liquibase.changelog-root.yml");
return liquibase;
}
private void tryToCreateSchema(DataSource dataSource) throws SQLException {
String CREATE_SCHEMA_QUERY = "CREATE SCHEMA IF NOT EXISTS test";
dataSource.getConnection().createStatement().execute(CREATE_SCHEMA_QUERY);
}
}
MyIT.java:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(classes=CommonConfig.class)
#ActiveProfile("integration")
#Import(TestPostgresConfig.class)
public class MyIT {
#ClassRule
public static PostgreSQLContainer psqlContainer = new PostgreSQLContainer("postgres:13.1")
.withDatabseName("test-database-instance")
.withUsername("divdiff")
.withPassword("theseAreNotTheDroidsForYou123");
#BeforeClass
public static void init() {
System.setProperty("spring.datasource.url", "jdbc:postgresql://"
+ psqlContainer.getHost() + ":"
+ psqlContainer.getMappedPort(5432) + "/"
+ psqlContainer.getDatabaseName()));
System.setProperty("spring.datasource.username", psqlContainer.getUsername());
System.setProperty("spring.datasource.password", psqlContainer.getPassword());
}
#Before
public void setUp() {
// code to set up my test
}
#Test
public void testMyCodeEndToEnd() {
// my test implementation
}
}
MyConfig.java:
#Configuration
#ComponentScan(basePackages = "my.code")
#EntityScan("my.code")
#Slf4j
public class MyConfig {
#Bean
public KeyStore keyStore() {
//load keystore and set javax.net.ssl.keystore* properties
}
#Bean
public KeyStore trustStore() {
//load truststore and set javax.net.ssl.truststore* properties
}
#Bean
public RestTemplate restTemplate() {
//Set up and load SSL Context with key and trust store
//Create HTTPClient and connection stuff
//Look at this link for a similar set up
//https://www.baeldung.com/rest-template
}
}
application-integration.yml
spring:
jpa:
properties:
hibernate:
enable_lazy_load_no_trans: true
profiles:
active: default
server:
ssl:
# My key and trust store values
application:
unrelated-app-properties:
# propertie values below
Package structure:
app-project/src/main/java/com/my/code/MyApplication.java
app-project/src/main/java/com/my/code/service/MyService.java
app-project/src/test/java/my/code/OTHER-TEST-CLASSES-LIVE-HERE...
app-project/src/test/java/integration/MyIT.java
app-project/src/test/java/integration/TestPostgresConfig.java
app-project/src/test/resources/application-integration.yml
my-common-project/src/main/java/common/config/MyConfig.java
YOUR HELP IS MUCH APPRECIATED!!! :D

I'm an idiot. The maven dependency I brought into for my tests was using provided scope instead of test:
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>project-with-db-changelogs</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>resources</classifier>
<type>zip</type>
<scope>provided</scope>
</dependency>
When it should have been test scope:
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>project-with-db-changelogs</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>resources</classifier>
<type>zip</type>
<scope>test</scope>
</dependency>
Per this link, "This is available only in compile-classpath and test-classpath", hence the liquibase code was being run in both my tests and the resulting jar. #amateur-hour

You can defile liqubase context as test
<changeSet author="name" id="id-of-file" context="test">
and have an application property like:
spring.liquibase.contexts=test
and add a liquibase bean like:
#Value("${spring.liquibase.contexts}")
private String liquibaseContexts;
#Bean
public SpringLiquibase liquibase() {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setDataSource(localDatabaseDataSource);
liquibase.setShouldRun(liquibaseEnabled);
liquibase.setChangeLog(localDatabaseLiquibaseChangeLog);
liquibase.setContexts(liquibaseContexts);
return liquibase;
}

Related

Spring Boot - manually run flyway before anything else (failure to initialize entityManagerFactory)

I'm having issues with getting flyway to play well with hibernate. I used to have the standard flyway settings, and it would run first on app startup. Great, exactly what I wanted.
Hibernate settings has spring.jpa.hibernate.ddl-auto=none
I then changed my main application to run flyway explicitly (because I want to conditionally run flyway in certain scenarios):
#Configuration
public class EmptyMigrationStrategyConfig {
#Bean
public FlywayMigrationStrategy flywayMigrationStrategy() {
return flyway -> {
// do nothing
};
}
}
#SpringBootApplication(exclude = {R2dbcAutoConfiguration.class})
#Configuration
#EnableRetry
public class MainApplication implements CommandLineRunner {
#Autowired
private Flyway flyway;
#Autowired
private ShutdownManager shutdownManager;
#Value("#{new Boolean('${app.migrations-only}')}")
boolean migrationsOnly;
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
flyway.migrate();
if (migrationsOnly) {
shutdownManager.initiateShutdown(0);
}
}
}
But now if I create a new migration script and add a POJO for an entity, the app fails to boot because hibernate is failing since flyway is no longer running first.
Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.MappingException: Could not determine type for: <entity> at <table>
What do I need to define so things run in the proper order?
Ok so the actual issue ended up being a typo in the hibernate model... regardless this is how you'd get flyway to run explicitly first:
#Configuration
public class EmptyMigrationStrategyConfig {
#Autowired
private ShutdownManager shutdownManager;
#Value("#{new Boolean('${app.migrations-only}')}")
boolean migrationsOnly;
#Bean
public FlywayMigrationStrategy flywayMigrationStrategy() {
return flyway -> {
flyway.migrate();
if (migrationsOnly) {
shutdownManager.initiateShutdown(0);
}
};
}
}
#SpringBootApplication(exclude = {R2dbcAutoConfiguration.class})
#Configuration
#EnableRetry
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}

How to create datasource bean at runtime without crashing app in Spring?

I am connecting to multiple datasources but sometimes some datasources may be offline and at that time I am geting errors on app and application is failing at startup.
I want to skip datasource configuration at startup... I have tried several ways by adding
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
to the application.properties and also I have tried adding
#SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
to the main class but still it tries to configure the datasource.
I also tried to use #Lazy annotation on all methods and on constructor as below but still getting error while creating fooEntityManagerFactory
#Lazy
#Configuration
#EnableJpaRepositories(basePackages = "com.heyo.tayo.repository.foo", entityManagerFactoryRef = "fooEntityManagerFactory", transactionManagerRef = "fooTransactionManager")
public class PersistencefooConfiguration {
#Autowired
private DbContextHolder dbContextHolder;
#Lazy
#Bean
#ConfigurationProperties("tay.datasource.foo")
public DataSourceProperties fooDataSourceProperties() {
return new DataSourceProperties();
}
#Lazy
#Bean
#ConfigurationProperties("tay.datasource.foo.configuration")
public DataSource fooDataSource() {
DataSource dataSource = fooDataSourceProperties().initializeDataSourceBuilder()
.type(BasicDataSource.class).build();
dbContextHolder.addNewAvailableDbType(DbTypeEnum.foo);
return dataSource;
}
#Lazy
#Bean(name = "fooEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean fooEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
//THE CODE IS FAILING AT BELOW RETURN CASE
return builder
.dataSource(fooDataSource())
.packages("com.heyo.tayo.model.foo")
.build();
}
#Lazy
#Bean
public PlatformTransactionManager fooTransactionManager(
final #Qualifier("fooEntityManagerFactory") LocalContainerEntityManagerFactoryBean fooEntityManagerFactory) {
return new JpaTransactionManager(fooEntityManagerFactory.getObject());
}
}
I have multiple classes like above for different configs for different datasources and I am adding them to available dbs static list at datasource Bean.
Here is my dbadapter factory class.
Here is my dbAdaptor factory that creates corresponding db adaptor
#Service
public class DbAdapterFactory {
#Autowired
private BeanFactory beanFactory;
#Autowired
private DbContextHolder dbContextHolder;
public DBAdapter dbAdapter(){
DbTypeEnum currentDb = dbContextHolder.getCurrentDb();
DBAdapter dbAdapter = null;
if(currentDb == DbTypeEnum.FOODB) {
dbAdapter = beanFactory.getBean(foodbadaptor.class);
} else {
dbAdapter = beanFactory.getBean(koodbadaptor.class);
}
return dbAdapter;
}
Here is db context holder that makes operation like setting default db or getting current db etc.:
#Component
public class DbContextHolder {
private DbTypeEnum dbType = DbTypeEnum.FOODB;
private Set<DbTypeEnum> availableDbTypes = new HashSet<>();
public void setCurrentDb(DbTypeEnum dbType) {
this.dbType = dbType;
}
public DbTypeEnum getCurrentDb() {
return this.dbType;
}
public List<DbTypeEnum> getAvailableDbTypes() {
return new ArrayList<>(availableDbTypes);
}
public void addNewAvailableDbType(DbTypeEnum dbTypeEnum) {
availableDbTypes.add(dbTypeEnum);
}
}
I made all #Lazy or tried #SpringBootApplication(exclude={DataSourceAutoConfiguration.class}) but still something is calling to create bean and getting error and app is closing. I want to use that config and datasource in a try-catch block and don't stop application at runtime. How can I achieve this or what am I missing on that configs or annotations ?
I believe that you can simply add in your application properties
spring.sql.init.continue-on-error=true
According to the Spring Boot 2.5.5 user guide:
https://docs.spring.io/spring-boot/docs/2.5.5/reference/htmlsingle/#howto-initialize-a-database-using-spring-jdbc
Spring Boot enables the fail-fast feature of its script-based database initializer. If the scripts cause exceptions, the application fails to start. You can tune that behavior by setting spring.sql.init.continue-on-error.
Depending on your spring boot version the property will be named either
spring.sql.init.continue-on-error
or before Spring Boot 2.5
spring.datasource.continue-on-error
It is so dumb but I solved the problem by adding following to application.properties.
spring.jpa.database=sql_server
I have no idea why I need to specify that explicitly in properties file but the problem is solved. I will search for it

How to update datasource bean in Spring?

My goal is to create a Webserver with Spring. It has to implement Multitenancy, which works great if you don't make it dynamic (adding, removing, changing). Is it possible to update the datasource bean in Spring?
My code:
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) throws IOException {
SpringApplication.run(MyApplication.class, args);
}
//Multitenancy
#Bean
public DataSource dataSource(){
//implements AbstractRoutingDataSource
CustomRoutingDataSource customDataSource = new CustomRoutingDataSource();
//logic here
return customDataSource;
}
}
What I've tried:
CustomRoutingDataSource c = context.getBean(CustomRoutingDataSource.class);
c.setTargetDataSources(CustomRoutingDataSource.getCustomDatasources());
which updates the bean(?) but doesn't update Spring's datasources, database connections are still missing if added with this method.
Simple solution for those with the same problem:
Add #RefreshScope
#Bean
#RefreshScope
public DataSource dataSource() {
CustomRoutingDataSource customDataSource = new CustomRoutingDataSource();
...
return customDataSource;
}
Add spring actuator endpoint in pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
POST to /actuator/refresh to update datasources!

How to embed in memory MariaDB4j to replace default Spring DataSource in JUnit Tests?

I am writing tests for Service that uses several data Jpa repositories. The problem is that some repositories use a lot of native queries with MySQL specific functions such as str_to_date(). So when I tried to test the service's method using H2 I got an error saying that H2 doesn't recognize function. I have tried using H2 in MySQL mode, but got the same error.
here mariaDB4j was proposed as a work-around. I have added dependency into Maven
<dependency>
<groupId>ch.vorburger.mariaDB4j</groupId>
<artifactId>mariaDB4j</artifactId>
<version>2.3.0</version>
<scope>test</scope>
</dependency>
But getting IllegalStateException : Failed to replace DataSource with an embedded database for tests. If you want an embedded database please put a supported one on the classpath or tune the replace attribute of #AutoConfigureTestDatabase.
My Test file looks this way:
#RunWith(SpringRunner.class)
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class TestPay {
#TestConfiguration
static class PaymentServiceTestContextConfiguration {
#Bean
public PaymentService paymentService(){
return new PaymentService();
}
}
#Autowired
private PaymentService paymentService;
#Autowired
private TarifRepository firstRepository;
#Autowired
private BuildingRepository secondRepository;
#Autowired
private ApartmentRepository thirdRepository;
/* Test cases here*/
}
The project is build with Annotation driven Spring Boot.
I build the following class that I reuse in every integration test that requires database access to mariadb. It could probably be improved (and I'd be happy for suggestions), but it works so far:
#RunWith(SpringRunner.class)
#SpringBootTest
#TestPropertySource(locations="classpath:application-junit.properties")
#DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) //otherwise mariadb is not cleaned up between tests
public abstract class MyIntegrationTest {
private static MariaDB4jSpringService DB;
#BeforeClass
public static void init() throws ManagedProcessException {
DB = new MariaDB4jSpringService();
DB.setDefaultPort(1234);
DB.start();
DB.getDB().createDB("yourtables");
DB.getDB().source("schema.sql"); // init scripts from /src/test/resources/schema.sql
}
#AfterClass
public static void cleanup() {
if (DB != null) DB.stop();
}
}
application-junit.properties:
spring.datasource.url=jdbc:mariadb://localhost:1234/yourtables
spring.datasource.username=root
spring.datasource.password=
It sounds like you need to explicitly declare your DataSource for tests. In the case of h2, there is likely already a datasource bean declared by a spring test dependency, but there may not be an off-the-shelf one provided by ch.vorburger.mariaDB4j.
Here's an example of an embedded MariaDB DataSource that I stole from elsewhere on the internet
import ch.vorburger.mariadb4j.DBConfigurationBuilder
import ch.vorburger.mariadb4j.springframework.MariaDB4jSpringService
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import javax.sql.DataSource
#Configuration
#Profile(['local', 'integrationTest'])
class EmbeddedMariaDbConfig {
#Bean
MariaDB4jSpringService mariaDB4jSpringService() {
new MariaDB4jSpringService()
}
#Bean
DataSource dataSource(MariaDB4jSpringService mariaDB4jSpringService,
#Value('${app.mariaDB4j.databaseName}') String databaseName,
#Value('${spring.datasource.username}') String datasourceUsername,
#Value('${spring.datasource.password}') String datasourcePassword,
#Value('${spring.datasource.driver-class-name}') String datasourceDriver) {
//Create our database with default root user and no password
mariaDB4jSpringService.getDB().createDB(databaseName)
DBConfigurationBuilder config = mariaDB4jSpringService.getConfiguration()
DataSourceBuilder
.create()
.username(datasourceUsername)
.password(datasourcePassword)
.url(config.getURL(databaseName))
.driverClassName(datasourceDriver)
.build();
}
}

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>
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.
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 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);
}
}

Categories

Resources