Spring Boot testing: Cannot bind #ConfigurationProperties - Ensure that #ConstructorBinding has not been applied - spring-boot

In a Spring Boot unit test, how can you mock #ConstructorBinding #ConfigurationProperties data class?
Setup
Both
Kotlin 1.4.30 (for unit tests and config classes)
Java 15 (with --enable-preview) (for business logic)
Spring Boot 2.4.2
Junit 5.7.1
Mockito (mockito-inline) 3.7.7
Maven 3.6.3_1
I want to test FtpService (a #Service, which has a RestTemplate) with different configurations.
Properties for the FtpService come from a Kotlin data class - UrlProperties - which is annotated with ConstructorBinding and #ConfigurationProperties.
Note: FtpService's constructor extracts a property from UrlProperties. This means that UrlProperties must be both mocked and stubbed before Spring loads FtpService
Error
When I try and mock UrlProperties so that I can set the properties for different tests, I either receive an error, or am unable to insert the bean
Cannot bind #ConfigurationProperties for bean 'urlProperties'. Ensure that #ConstructorBinding has not been applied to regular bean
Code
`#SpringBootTest` of FtpService | `src/test/kotlin/com/example/FtpServiceTest.kt`
import com.example.service.FtpService
import com.example.service.UrlProperties
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.context.annotation.Bean
import org.springframework.test.context.ContextConfiguration
#TestConfiguration
#SpringBootTest(classes = [FtpService::class])
#AutoConfigureWebClient(registerRestTemplate = true)
class FtpServiceTest
#Autowired constructor(
private val ftpService: FtpService
) {
// MockBean inserted into Spring Context too late,
// FtpService constructor throws NPE
// #MockBean
// lateinit var urlProperties: UrlProperties
#ContextConfiguration
class MyTestContext {
// error -
// > Cannot bind #ConfigurationProperties for bean 'urlProperties'.
// > Ensure that #ConstructorBinding has not been applied to regular bean
var urlProperties: UrlProperties = mock(UrlProperties::class.java)
#Bean
fun urlProperties() = urlProperties
// error -
// > Cannot bind #ConfigurationProperties for bean 'urlProperties'.
// > Ensure that #ConstructorBinding has not been applied to regular bean
// #Bean
// fun urlProperties(): UrlProperties {
// return UrlProperties(
// UrlProperties.FtpProperties(
// url = "ftp://localhost:21"
// ))
// }
}
#Test
fun `test fetch file root`() {
`when`(MyTestContext().urlProperties.ftp)
.thenReturn(UrlProperties.FtpProperties(
url = "ftp://localhost:21"
))
assertEquals("I'm fetching a file from ftp://localhost:21!",
ftpService.fetchFile())
}
#Test
fun `test fetch file folder`() {
`when`(MyTestContext().urlProperties.ftp)
.thenReturn(UrlProperties.FtpProperties(
url = "ftp://localhost:21/user/folder"
))
assertEquals("I'm fetching a file from ftp://localhost:21/user/folder!",
ftpService.fetchFile())
}
}
Workaround - manual definition every test
The only 'workaround' is to manually define all beans (which means I miss out on Spring Boot magic during testing) and in my view is more confusing.
Workaround - manual redefinition each test | `src/test/kotlin/com/example/FtpServiceTest2.kt`
import com.example.service.FtpService
import com.example.service.UrlProperties
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.springframework.boot.web.client.RestTemplateBuilder
class FtpServiceTest2 {
private val restTemplate =
RestTemplateBuilder()
.build()
private lateinit var ftpService: FtpService
private lateinit var urlProperties: UrlProperties
#BeforeEach
fun beforeEachTest() {
urlProperties = mock(UrlProperties::class.java)
`when`(urlProperties.ftp)
.thenReturn(UrlProperties.FtpProperties(
url = "default"
))
ftpService = FtpService(restTemplate, urlProperties)
}
/** this is the only test that allows me to redefine 'url' */
#Test
fun `test fetch file folder - redefine`() {
urlProperties = mock(UrlProperties::class.java)
`when`(urlProperties.ftp)
.thenReturn(UrlProperties.FtpProperties(
url = "ftp://localhost:21/redefine"
))
// redefine the service
ftpService = FtpService(restTemplate, urlProperties)
assertEquals("I'm fetching a file from ftp://localhost:21/redefine!",
ftpService.fetchFile())
}
#Test
fun `test default`() {
assertEquals("I'm fetching a file from default!",
ftpService.fetchFile())
}
#Test
fun `test fetch file root`() {
`when`(urlProperties.ftp)
.thenReturn(UrlProperties.FtpProperties(
url = "ftp://localhost:21"
))
assertEquals("I'm fetching a file from ftp://localhost:21!",
ftpService.fetchFile())
}
#Test
fun `test fetch file folder`() {
doReturn(
UrlProperties.FtpProperties(
url = "ftp://localhost:21/user/folder"
)).`when`(urlProperties).ftp
assertEquals("I'm fetching a file from ftp://localhost:21/user/folder!",
ftpService.fetchFile())
}
#Test
fun `test fetch file folder - reset`() {
Mockito.reset(urlProperties)
`when`(urlProperties.ftp)
.thenReturn(UrlProperties.FtpProperties(
url = "ftp://localhost:21/mockito/reset/when"
))
assertEquals("I'm fetching a file from ftp://localhost:21/mockito/reset/when!",
ftpService.fetchFile())
}
#Test
fun `test fetch file folder - reset & doReturn`() {
Mockito.reset(urlProperties)
doReturn(
UrlProperties.FtpProperties(
url = "ftp://localhost:21/reset/doReturn"
)).`when`(urlProperties).ftp
assertEquals("I'm fetching a file from ftp://localhost:21/reset/doReturn!",
ftpService.fetchFile())
}
}
Spring App | `src/main/kotlin/com/example/MyApp.kt`
package com.example
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
#SpringBootApplication
#EnableConfigurationProperties
#ConfigurationPropertiesScan
class MyApp
fun main(args: Array<String>) {
runApplication<MyApp>(*args)
}
Example #Service | `src/main/kotlin/com/example/service/FtpService.kt`
package com.example.service
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
#Service
class FtpService(
val restTemplate: RestTemplate,
urlProperties: UrlProperties,
val ftpProperties: UrlProperties.FtpProperties = urlProperties.ftp
) {
fun fetchFile(): String {
println(restTemplate)
return "I'm fetching a file from ${ftpProperties.url}!"
}
}
#ConfigurationProperties with #ConstructorBinding - `src/main/kotlin/com/example/service/UrlProperties.kt`
package com.example.service
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
#ConstructorBinding
#ConfigurationProperties("url")
data class UrlProperties(val ftp: FtpProperties) {
data class FtpProperties(
val url: String,
)
}

Related

Spring Kotlin #ConfigurationProperties for data class defined in dependency

I've got a library that has a configuration class (no spring configuration class) defined as a data class. I want a Bean of that configuration which can be configured via application.properties. The problem is that I don't know how to tell Spring to create ConfigurationProperties according to that external data class. I am not the author of the configuration class so I can't annotate the class itself. #ConfigurationProperties in conjunction with #Bean does not work as the properties are immutable. Is this even possible?
Maybe change scan packages to inlcude the packages that do you want.
#SpringBootApplication( scanBasePackages = )
take a look this:
Configuration using annotation #SpringBootApplication
If I understand correctly, do you need a way to turn a third-party object into a bean with properties from your application.properties file ?
Given an application.properties file:
third-party-config.params.simpleParam=foo
third-party-config.params.nested.nestedOne=bar1
third-party-config.params.nested.nestedTwo=bar2
Create a class to receive your params from properties file
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
#Configuration
#ConfigurationProperties(prefix = "third-party-config")
data class ThirdPartConfig(val params: Map<String, Any>)
Here is an example of the object that you want to use
class ThirdPartyObject(private val simpleParam: String, private val nested: Map<String, String>) {
fun printParams() =
"This is the simple param: $simpleParam and the others nested ${nested["nestedOne"]} and ${nested["nestedTwo"]}"
}
Create the configuration class with a method that turns your third-party object into an injectable bean.
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
#Configuration
class ThirdPartObjectConfig(private val thirdPartConfig: ThirdPartConfig) {
#Bean
fun thirdPartyObject(): ThirdPartyObject {
return ThirdPartObject(
simpleParam = thirdPartConfig.params["simpleParam"].toString(),
nested = getMapFromAny(
thirdPartConfig.params["nested"]
?: throw IllegalStateException("'nested' parameter must be declared in the app propertie file")
)
)
}
private fun getMapFromAny(unknownType: Any): Map<String, String> {
val asMap = unknownType as Map<*, *>
return mapOf(
"nestedOne" to asMap["nestedOne"].toString(),
"nestedTwo" to asMap["nestedTwo"].toString()
)
}
}
So now you can inject your third-party object as a bean with custom configurated params from your application.properties files
#SpringBootApplication
class StackoverflowAnswerApplication(private val thirdPartObject: ThirdPartObject): CommandLineRunner {
override fun run(vararg args: String?) {
println("Running --> ${thirdPartObject.printParams()}")
}
}

Spring beans in configuration file form a cycle (Kotlin, Redis)

I'm new to Kotlin and working on a Spring application, trying to set Redis configuration. I keep getting this problem:
org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'redisConfig': Requested bean is
currently in creation: Is there an unresolvable circular reference?
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| redisConfig defined in file [file]
└─────┘
I don't understand what exactly causes this problem and how to fix it. It seems to me that RedisConfig is being created inside RedisConfig, but I'm not sure and don't understand where this problem comes from.
Here's RedisConfig.kt
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.MessageListener
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.listener.ChannelTopic
import org.springframework.data.redis.listener.RedisMessageListenerContainer
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
#Configuration
class RedisConfig(val messageListener: MessageListener) {
#Value("\${spring.redis.host}")
lateinit var redisHost: String
#Value("\${spring.redis.port}")
lateinit var redisPort: String
#Value("\${spring.redis.topic}")
lateinit var redisTopic: String
#Bean
fun jedisConnectionFactory(): JedisConnectionFactory {
val config = RedisStandaloneConfiguration(redisHost, redisPort.toInt())
val jedisClientConfiguration = JedisClientConfiguration.builder().usePooling().build()
val factory = JedisConnectionFactory(config, jedisClientConfiguration)
factory.afterPropertiesSet()
return factory
}
#Bean
fun redisTemplate(): RedisTemplate<String, Any> {
val template: RedisTemplate<String, Any> = RedisTemplate()
template.connectionFactory = JedisConnectionFactory()
template.valueSerializer = GenericJackson2JsonRedisSerializer()
return template
}
#Bean
fun topic(): ChannelTopic = ChannelTopic(redisTopic)
#Bean
fun newMessageListener(): MessageListenerAdapter = MessageListenerAdapter(messageListener)
#Bean
fun redisContainer(): RedisMessageListenerContainer {
val container = RedisMessageListenerContainer()
container.connectionFactory = jedisConnectionFactory()
container.addMessageListener(newMessageListener(), topic())
return container
}
}
I believe you should use your method jedisConnectionFactory() instead of a new JedisConnectionFactory class, in redisTemplate method.
Basicly, you should do the same as you do in redisContainer()

The Right Why Database Seeder with kotlin x spring boot

today I'm learning build API using kotlin and spring boot. In rails and laravel have a "tool" for database seeder, I want to know in kotlin and spring boot, I have been searched on google before and found this answer https://stackoverflow.com/a/45324578/1297435, in spring boot we can use #EventListerner like
#EventListener
public void userSeeder(ContextRefreshedEvent event) {
// my query
// check query size and iteration
}
That's in spring boot, but is there a way in kotlin?
// main/kotlin/com.myapp.api/seeder/UserSeeder.kt
package com.myapp.api.seeder
import org.springframework.context.event.ContextRefreshedEvent
import com.myapp.api.repository.*
import com.myapp.api.model.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
interface EventListener
#Component
class UserSeeder {
#Autowired
lateinit var repository: UserRepository
#EventListener
fun seedUsername(event: ContextRefreshedEvent) {
val users = repository.findByUsernameBlank()
if (users == null || users!!.size <= 0) {
//
} else {
//
}
}
}
#EventListener class doesn't work in kotlin or is it correct?
Error:(15, 6) Kotlin: This class does not have a constructor
You probably have an issue because you define EventListener as an interface instead of importing it from org.springframework.context.event. (See interface EventListener just below the imports.
But you your actual question: I typically use org.springframework.boot.ApplicationRunner for such tasks.
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
#Component
class UserSeeder(private val repository: UserRepository) : ApplicationRunner {
override fun run(args: ApplicationArguments) {
val users = repository.findByUsernameBlank()
if (users == null || users!!.size <= 0) {
//
} else {
//
}
}
}
BTW: I also used constructor based injection.

Annotation Injection for Spring with Kotlin

Kotlin cannot inject annotation at compile time such as by existing library Lombok. Is there any decent way to inject annotation for spring framework at runtime?
Assuming you are trying to inject logger annotation into Spring application.
Here's annotation class example: Log.kt
package com.example.util
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD)
#MustBeDocumented
annotation class Log
This class injects annotation at runtime: LogInjector.kt
package com.example.util
import org.slf4j.LoggerFactory
import org.springframework.beans.BeansException
import org.springframework.beans.factory.config.BeanPostProcessor
import org.springframework.stereotype.Component
import org.springframework.util.ReflectionUtils
import java.lang.reflect.Field
#Component
class LogInjector: BeanPostProcessor {
#Throws(BeansException::class)
override fun postProcessAfterInitialization(bean: Any, beanName: String): Any {
return bean
}
#Throws(BeansException::class)
override fun postProcessBeforeInitialization(bean: Any, name: String): Any {
ReflectionUtils.doWithFields(bean.javaClass,
#Throws(IllegalArgumentException::class, IllegalAccessException::class) { field: Field ->
// SAM conversion for Java interface
ReflectionUtils.makeAccessible(field)
if (field.getAnnotation(Log::class.java) != null) {
val log = LoggerFactory.getLogger(bean.javaClass)
field.set(bean, log)
}
}
)
return bean
}
}
Then, this class uses #Log annotation: GreetingController.kt
package com.example.web
import org.slf4j.Logger
import org.springframework.web.bind.annotation.*
#RestController
class GreetingController {
#Log lateinit private var logger: Logger
#RequestMapping("/greeting")
fun greeting(): String {
logger.info("Greeting endpoint was called")
return "Hello"
}
}
To avoid calling logger in null-safe like logger?.info('...'), this example marks the property with the late-initialized modifier.

Spring beans are not injected in flyway java based migration

I'm trying to inject component of configuration properties in the flyway migration java code but it always null.
I'm using spring boot with Flyway.
#Component
#ConfigurationProperties(prefix = "code")
public class CodesProp {
private String codePath;
}
Then inside Flyway migration code, trying to autowrire this component as following:
public class V1_4__Migrate_codes_metadata implements SpringJdbcMigration {
#Autowired
private CodesProp codesProp ;
public void migrate(JdbcTemplate jdbcTemplate) throws Exception {
codesProp.getCodePath();
}
Here, codesProp is always null.
Is there any way to inject spring beans inside flyway or make it initialized before flyway bean?
Thank You.
Flyway doesn't support dependency injection into SpringJdbcMigration implementations. It simply looks for classes on the classpath that implement SpringJdbcMigration and creates a new instance using the default constructor. This is performed in SpringJdbcMigrationResolver. When the migration is executed, SpringJdbcMigrationExecutor creates a new JdbcTemplate and then calls your migration implementation's migrate method.
If you really need dependencies to be injected into your Java-based migrations, I think you'll have to implement your own MigrationResolver that retrieves beans of a particular type from the application context and creates and returns a ResolvedMigration instance for each.
If like me, you don't want to wait for Flyway 4.1, you can use Flyway 4.0 and add the following to your Spring Boot application:
1) Create a ApplicationContextAwareSpringJdbcMigrationResolver class in your project:
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.MigrationType;
import org.flywaydb.core.api.MigrationVersion;
import org.flywaydb.core.api.configuration.FlywayConfiguration;
import org.flywaydb.core.api.migration.MigrationChecksumProvider;
import org.flywaydb.core.api.migration.MigrationInfoProvider;
import org.flywaydb.core.api.migration.spring.SpringJdbcMigration;
import org.flywaydb.core.api.resolver.ResolvedMigration;
import org.flywaydb.core.internal.resolver.MigrationInfoHelper;
import org.flywaydb.core.internal.resolver.ResolvedMigrationComparator;
import org.flywaydb.core.internal.resolver.ResolvedMigrationImpl;
import org.flywaydb.core.internal.resolver.spring.SpringJdbcMigrationExecutor;
import org.flywaydb.core.internal.resolver.spring.SpringJdbcMigrationResolver;
import org.flywaydb.core.internal.util.ClassUtils;
import org.flywaydb.core.internal.util.Location;
import org.flywaydb.core.internal.util.Pair;
import org.flywaydb.core.internal.util.StringUtils;
import org.flywaydb.core.internal.util.scanner.Scanner;
import org.springframework.context.ApplicationContext;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
/**
* Migration resolver for {#link SpringJdbcMigration}s which are registered in the given {#link ApplicationContext}.
* This resolver provides the ability to use other beans registered in the {#link ApplicationContext} and reference
* them via Spring's dependency injection facility inside the {#link SpringJdbcMigration}s.
*/
public class ApplicationContextAwareSpringJdbcMigrationResolver extends SpringJdbcMigrationResolver {
private final ApplicationContext applicationContext;
public ApplicationContextAwareSpringJdbcMigrationResolver(Scanner scanner, Location location, FlywayConfiguration configuration, ApplicationContext applicationContext) {
super(scanner, location, configuration);
this.applicationContext = applicationContext;
}
#SuppressWarnings("unchecked")
#Override
public Collection<ResolvedMigration> resolveMigrations() {
// get all beans of type SpringJdbcMigration from the application context
Map<String, SpringJdbcMigration> springJdbcMigrationBeans =
(Map<String, SpringJdbcMigration>) this.applicationContext.getBeansOfType(SpringJdbcMigration.class);
ArrayList<ResolvedMigration> resolvedMigrations = new ArrayList<ResolvedMigration>();
// resolve the migration and populate it with the migration info
for (SpringJdbcMigration springJdbcMigrationBean : springJdbcMigrationBeans.values()) {
ResolvedMigrationImpl resolvedMigration = extractMigrationInfo(springJdbcMigrationBean);
resolvedMigration.setPhysicalLocation(ClassUtils.getLocationOnDisk(springJdbcMigrationBean.getClass()));
resolvedMigration.setExecutor(new SpringJdbcMigrationExecutor(springJdbcMigrationBean));
resolvedMigrations.add(resolvedMigration);
}
Collections.sort(resolvedMigrations, new ResolvedMigrationComparator());
return resolvedMigrations;
}
ResolvedMigrationImpl extractMigrationInfo(SpringJdbcMigration springJdbcMigration) {
Integer checksum = null;
if (springJdbcMigration instanceof MigrationChecksumProvider) {
MigrationChecksumProvider version = (MigrationChecksumProvider) springJdbcMigration;
checksum = version.getChecksum();
}
String description;
MigrationVersion version1;
if (springJdbcMigration instanceof MigrationInfoProvider) {
MigrationInfoProvider resolvedMigration = (MigrationInfoProvider) springJdbcMigration;
version1 = resolvedMigration.getVersion();
description = resolvedMigration.getDescription();
if (!StringUtils.hasText(description)) {
throw new FlywayException("Missing description for migration " + version1);
}
} else {
String resolvedMigration1 = ClassUtils.getShortName(springJdbcMigration.getClass());
if (!resolvedMigration1.startsWith("V") && !resolvedMigration1.startsWith("R")) {
throw new FlywayException("Invalid Jdbc migration class name: " + springJdbcMigration.getClass()
.getName() + " => ensure it starts with V or R," + " or implement org.flywaydb.core.api.migration.MigrationInfoProvider for non-default naming");
}
String prefix = resolvedMigration1.substring(0, 1);
Pair info = MigrationInfoHelper.extractVersionAndDescription(resolvedMigration1, prefix, "__", "");
version1 = (MigrationVersion) info.getLeft();
description = (String) info.getRight();
}
ResolvedMigrationImpl resolvedMigration2 = new ResolvedMigrationImpl();
resolvedMigration2.setVersion(version1);
resolvedMigration2.setDescription(description);
resolvedMigration2.setScript(springJdbcMigration.getClass().getName());
resolvedMigration2.setChecksum(checksum);
resolvedMigration2.setType(MigrationType.SPRING_JDBC);
return resolvedMigration2;
}
}
2) Add a new configuration class to post process the Spring Boot generated Flyway instance:
import org.flywaydb.core.Flyway;
import org.flywaydb.core.internal.dbsupport.DbSupport;
import org.flywaydb.core.internal.dbsupport.h2.H2DbSupport;
import org.flywaydb.core.internal.dbsupport.mysql.MySQLDbSupport;
import com.pegusapps.zebra.infrastructure.repository.flyway.ApplicationContextAwareSpringJdbcMigrationResolver;
import org.flywaydb.core.internal.resolver.sql.SqlMigrationResolver;
import org.flywaydb.core.internal.util.Location;
import org.flywaydb.core.internal.util.PlaceholderReplacer;
import org.flywaydb.core.internal.util.scanner.Scanner;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.sql.SQLException;
#Configuration
#ComponentScan("db.migration")
public class FlywayConfiguration {
#Bean
public BeanPostProcessor postProcessFlyway(ApplicationContext context) {
return new BeanPostProcessor() {
#Override
public Object postProcessBeforeInitialization(Object o, String s) throws BeansException {
return o;
}
#Override
public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
if (o instanceof Flyway) {
Flyway flyway = (Flyway) o;
flyway.setSkipDefaultResolvers(true);
ApplicationContextAwareSpringJdbcMigrationResolver resolver = new ApplicationContextAwareSpringJdbcMigrationResolver(
new Scanner(Thread.currentThread().getContextClassLoader()),
new Location("classpath:db/migration"),
context.getBean(org.flywaydb.core.api.configuration.FlywayConfiguration.class),
context);
SqlMigrationResolver sqlMigrationResolver = null;
try {
sqlMigrationResolver = new SqlMigrationResolver(
getDbSupport(),
new Scanner(Thread.currentThread().getContextClassLoader()),
new Location("classpath:db/migration"),
PlaceholderReplacer.NO_PLACEHOLDERS,
"UTF-8",
"V",
"R",
"__",
".sql");
} catch (SQLException e) {
e.printStackTrace();
}
flyway.setResolvers(sqlMigrationResolver, resolver);
}
return o;
}
private DbSupport getDbSupport() throws SQLException {
DataSource dataSource = context.getBean(DataSource.class);
if( ((org.apache.tomcat.jdbc.pool.DataSource)dataSource).getDriverClassName().equals("org.h2.Driver"))
{
return new H2DbSupport(dataSource.getConnection());
}
else
{
return new MySQLDbSupport(dataSource.getConnection());
}
}
};
}
}
Note that I have some hardcoded dependencies on tomcat jdbc pool, h2 and mysql. If you are using something else, you will need to change the code there (If there is anybody that knows how to avoid it, please comment!)
Also note that the #ComponentScan package needs to match with where you will put the Java migration classes.
Also note that I had to add the SqlMigrationResolver back in since I want to support both the SQL and the Java flavor of the migrations.
3) Create a Java class in the db.migrations package that does the actual migration:
#Component
public class V2__add_default_surveys implements SpringJdbcMigration {
private final SurveyRepository surveyRepository;
#Autowired
public V2__add_surveys(SurveyRepository surveyRepository) {
this.surveyRepository = surveyRepository;
}
#Override
public void migrate(JdbcTemplate jdbcTemplate) throws Exception {
surveyRepository.save(...);
}
}
Note that you need to make the class a #Component and it needs to implement the SpringJdbcMigration. In this class, you can use Spring constructor injection for any Spring bean from your context you might need to do the migration(s).
Note: Be sure to disable ddl validation of Hibernate, because the validation seems to run before Flyway runs:
spring.jpa.hibernate.ddl-auto=none
In short do not autowire beans in your db migrations or even reference classes from your application!
If you refactor/delete/change classes you referenced in the migration it may not even compile or worse corrupt your migrations.
The overhead of using plain JDBC template for the migrations is not worth the risk.
If you are using deltaspike you can use BeanProvider to get a reference to your Class. Here is a DAO example, but it should work fine with your class too.
Change your DAO code:
public static UserDao getInstance() {
return BeanProvider.getContextualReference(UserDao.class, false, new DaoLiteral());
}
Then in your migration method:
UserDao userdao = UserDao.getInstance();
And there you've got your reference.
(referenced from: Flyway Migration with java)

Resources