2

I have a Spring project, and I need to configure Flyway. Using the default FlywayAutoConfiguration, Flyway migration is immediatly executed before everything else (caching, PostConstruct annotations, services). This is the behavior I expected (in term or startup workflow)

Unfortunatly, I need to override the default FlywayAutoConfiguration because I use a custom Flyway implementation, but that's not the my main problem here, my issue is really related to Spring Priority for configuration and initialization sequence.

So do use my own flyway, I first copied FlywayAutoConfiguration to my maven module, name it CustomFlywayAutoConfiguration and just adapt imports. I also change the property "spring.flyway.enabled" to false and create another one "spring.flywaycustom.enabled" to be able to activate mine and not the default one.

Doing that fully change the sequence of startup. Now, flyway is executed at the end of the startup sequence (after caching and other @PostConstruct that are in my project)

The following bean defined in CustomFlywayAutoConfiguration is now created onyl at the end of the startup sequence. With the default FlywayAutoConfiguration , was well created on the beginning.

@Bean
    @ConditionalOnMissingBean
    public FlywayMigrationInitializer flywayInitializer(Flyway flyway,
            ObjectProvider<FlywayMigrationStrategy> migrationStrategy) {
        return new FlywayMigrationInitializer(flyway, migrationStrategy.getIfAvailable());
    }

I tryied to play a lot with ordering (HIGHEST_PRECEDENCE, and LOWEST_PRECEDENCE)

  • @AutoConfigureOrder on configuration class
  • @Order on components
  • Try to Autowire FlywayMigrationInitializer to force the bean initialisation earlier

It's doesn't change anything, looks like Spring ignore @Order and @AutoConfigureOrder

Any idea why when Configuration is inside spring-boot-autoconfigure dependencies, it started as first priority, and when the same Configuration code is inside my project, I don't have the same ordering?

Thank you so much for your help.

Thibaut
  • 2,596
  • 2
  • 24
  • 24

2 Answers2

1

Thanks to your answer, Focusin my attention FlywayMigrationInitializerEntityManagerFactoryDependsOnPostProcessor help me to solve the issue.

@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@EnableConfigurationProperties({ DataSourceProperties.class, FlywayProperties.class })
@Import({ FlywayMigrationInitializerEntityManagerFactoryDependsOnPostProcessor.class }) // Very important to force flyway before
public class CustomFlywayConfiguration {

    @Bean
    public Flyway flyway(FlywayProperties properties, DataSourceProperties dataSourceProperties,
            ResourceLoader resourceLoader, ObjectProvider<DataSource> dataSource,
            @FlywayDataSource ObjectProvider<DataSource> flywayDataSource,
            ObjectProvider<FlywayConfigurationCustomizer> fluentConfigurationCustomizers,
            ObjectProvider<JavaMigration> javaMigrations, ObjectProvider<Callback> callbacks) {
        FluentConfiguration configuration = new FluentConfiguration(resourceLoader.getClassLoader());
        DataSource dataSourceToMigrate = configureDataSource(configuration, properties, dataSourceProperties, flywayDataSource.getIfAvailable(), dataSource.getIfUnique());
        checkLocationExists(dataSourceToMigrate, properties, resourceLoader);
        configureProperties(configuration, properties);
        List<Callback> orderedCallbacks = callbacks.orderedStream().collect(Collectors.toList());
        configureCallbacks(configuration, orderedCallbacks);
        fluentConfigurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration));
        configureFlywayCallbacks(configuration, orderedCallbacks);
        List<JavaMigration> migrations = javaMigrations.stream().collect(Collectors.toList());
        configureJavaMigrations(configuration, migrations);
        return new CustomFlyway(configuration);
    }


    /**
     * FlywayAutoConfiguration.FlywayConfiguration is conditioned to the missing flyway bean. @Import annotation are not executed in this case.
     * 
     * So if we declare our own Flyway bean, we also need to create this bean to trigger the flyway migration.
     * 
     * The main issue is now that bean is create after the @PostConstruct init method in MyInitComponent.
     * 
     */
    @Bean
    public FlywayMigrationInitializer flywayInitializer(Flyway flyway,
            ObjectProvider<FlywayMigrationStrategy> migrationStrategy) {
        return new FlywayMigrationInitializer(flyway, migrationStrategy.getIfAvailable());
    }

    /**
     * Thanks to that, it's now working, because make sure it's required before to create EntityManager
     */
    // @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class)
    // @ConditionalOnBean(AbstractEntityManagerFactoryBean.class)
    static class FlywayMigrationInitializerEntityManagerFactoryDependsOnPostProcessor
            extends EntityManagerFactoryDependsOnPostProcessor {

        FlywayMigrationInitializerEntityManagerFactoryDependsOnPostProcessor() {
            super(FlywayMigrationInitializer.class);
        }

    }

...

So that confirm we can't easily override the Flyway bean without copy some logic from FlywayAutoConfiguration.

I created a small project to reproduce the bug, with the solution I found. https://github.com/w3blogfr/flyway-issue-demo

I don't know if it's necessary to fix it or not in spring auto-configuration project?

I checked the history to find back the commit https://github.com/spring-projects/spring-boot/commit/795303d6676c163af899e87364846d9763055cf8

And the ticket was this one. https://github.com/spring-projects/spring-boot/issues/18382

The change looks technical and probably he missed that was an @ConditionalOnClass

Thibaut
  • 2,596
  • 2
  • 24
  • 24
0

None of the annotations that you've described have an impact on runtime semantics:

  • @AutoConfigureOrder works only on auto-configurations (so if you've copied the auto-configuration as a user config, it won't even be considered). This is used to order how auto-configurations are parsed (typical use case: make sure an auto-config contribute a bean definition of type X before another one check if a bean X is available).
  • @Order orders components of the same type. Doesn't have any effect as to "when" something occurs

Forcing initialisation using an injection point is a good idea but it'll only work if the component you're injecting it into is initialised itself at the right time.

The auto-configuration has a bunch of post-processors that create links between various components that use the DataSource and the FlywayMigrationInitializer. For instance, FlywayMigrationInitializerEntityManagerFactoryDependsOnPostProcessor makes sure that FlywayMigrationIntializer is a dependency of the entity manager so that the database is migrated before the EntityManager is made available to other components. That link is what's making sure Flyway is executed at the right time. I don't know why that's not working with your copy, a sample we can run ourselves shared on GitHub could help us figure out.

With all that said, please don't copy an auto-configuration in your own project. I'd advise to describe your use case in more details and open an issue so that we consider improving the auto-configuration.

Stephane Nicoll
  • 31,977
  • 9
  • 97
  • 89