feat: add a pluggable JDBC dialect layer with MySQL support

Fold Flyway V1-V7 into per-dialect baselines and resolve Boot 3/4 Flyway customizer types without a DataSource creation cycle.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0264408
2026-09-16 11:05:14 +08:00
co-authored by Cursor
parent 9bda7c417c
commit 7d17157633
36 changed files with 1123 additions and 304 deletions
+8
View File
@@ -58,6 +58,14 @@
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
@@ -0,0 +1,123 @@
package com.jetlumen.ordo.spring;
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.configuration.FluentConfiguration;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
import java.lang.reflect.Proxy;
/**
* Points Flyway at the dialect migration directory before Flyway runs.
* Host {@code spring.flyway.locations} is left unchanged when set.
*
* <p>Does not inject {@link DataSource} or {@link SqlDialect} at bean-creation time
* (that would cycle with DataSource → Flyway → customizer). Dialect is resolved inside
* {@code customize} from the FluentConfiguration DataSource / {@code ordo.jdbc.dialect}.
*
* <p>FlywayConfigurationCustomizer moved between Boot 3 and Boot 4; a reflective
* {@link FactoryBean} supplies a proxy for whichever type is on the classpath.
*/
@AutoConfiguration
@ConditionalOnProperty(prefix = "ordo", name = "enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnClass({DataSource.class, Flyway.class})
@ConditionalOnBean(DataSource.class)
@AutoConfigureAfter(name = {
"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration"
})
@AutoConfigureBefore(name = {
"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration",
"org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration"
})
@EnableConfigurationProperties(OrdoProperties.class)
public class OrdoFlywayAutoConfiguration {
private static final String[] FLYWAY_CUSTOMIZER_TYPES = {
"org.springframework.boot.flyway.autoconfigure.FlywayConfigurationCustomizer",
"org.springframework.boot.autoconfigure.flyway.FlywayConfigurationCustomizer"
};
@Bean
@ConditionalOnClass(name = "org.flywaydb.core.api.configuration.FluentConfiguration")
public FactoryBean<Object> ordoFlywayConfigurationCustomizer(
OrdoProperties ordoProperties, Environment environment) {
Class<?> customizerType = resolveFlywayCustomizerType();
if (customizerType == null) {
return null;
}
return new FlywayLocationsCustomizerFactoryBean(customizerType, ordoProperties, environment);
}
static Class<?> resolveFlywayCustomizerType() {
ClassLoader classLoader = OrdoFlywayAutoConfiguration.class.getClassLoader();
for (String name : FLYWAY_CUSTOMIZER_TYPES) {
try {
return Class.forName(name, false, classLoader);
} catch (ClassNotFoundException ignored) {
// Boot 3 vs Boot 4
}
}
return null;
}
private static final class FlywayLocationsCustomizerFactoryBean implements FactoryBean<Object> {
private final Class<?> customizerType;
private final OrdoProperties ordoProperties;
private final Environment environment;
private FlywayLocationsCustomizerFactoryBean(Class<?> customizerType, OrdoProperties ordoProperties,
Environment environment) {
this.customizerType = customizerType;
this.ordoProperties = ordoProperties;
this.environment = environment;
}
@Override
public Object getObject() {
return Proxy.newProxyInstance(customizerType.getClassLoader(), new Class<?>[] {customizerType},
(proxy, method, args) -> {
String name = method.getName();
if ("customize".equals(name) && args != null && args.length == 1) {
applyLocations((FluentConfiguration) args[0]);
return null;
}
if ("equals".equals(name)) {
return proxy == args[0];
}
if ("hashCode".equals(name)) {
return System.identityHashCode(proxy);
}
if ("toString".equals(name)) {
return "OrdoFlywayLocationsCustomizer";
}
throw new UnsupportedOperationException(method.toString());
});
}
private void applyLocations(FluentConfiguration configuration) {
if (environment.containsProperty("spring.flyway.locations")) {
return;
}
SqlDialect dialect = SqlDialects.resolve(configuration.getDataSource(),
ordoProperties.getJdbc().getDialect());
configuration.locations(dialect.flywayLocations());
}
@Override
public Class<?> getObjectType() {
return customizerType;
}
}
}
@@ -19,6 +19,8 @@ import com.jetlumen.ordo.storage.jdbc.JdbcProcessDefinitionRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcProcessHistoryRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcProcessInstanceRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcTransactionExecutor;
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@@ -49,7 +51,8 @@ import java.time.Clock;
"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration",
// Spring Boot 4.x locations (JDBC/Flyway autoconfiguration moved to dedicated modules)
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration",
"org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration"
"org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration",
"com.jetlumen.ordo.spring.OrdoFlywayAutoConfiguration"
})
@EnableConfigurationProperties(OrdoProperties.class)
public class OrdoJdbcAutoConfiguration {
@@ -78,6 +81,12 @@ public class OrdoJdbcAutoConfiguration {
return ActionHandler.noop();
}
@Bean
@ConditionalOnMissingBean
public SqlDialect ordoSqlDialect(DataSource dataSource, OrdoProperties ordoProperties) {
return SqlDialects.resolve(dataSource, ordoProperties.getJdbc().getDialect());
}
@Bean
@ConditionalOnMissingBean
@DependsOnDatabaseInitialization
@@ -93,32 +102,37 @@ public class OrdoJdbcAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ProcessDefinitionRepository ordoProcessDefinitionRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcProcessDefinitionRepository(connectionProvider);
public ProcessDefinitionRepository ordoProcessDefinitionRepository(
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
return new JdbcProcessDefinitionRepository(connectionProvider, ordoSqlDialect);
}
@Bean
@ConditionalOnMissingBean
public ProcessInstanceRepository ordoProcessInstanceRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcProcessInstanceRepository(connectionProvider);
public ProcessInstanceRepository ordoProcessInstanceRepository(
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
return new JdbcProcessInstanceRepository(connectionProvider, ordoSqlDialect);
}
@Bean
@ConditionalOnMissingBean
public ApprovalTaskRepository ordoApprovalTaskRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcApprovalTaskRepository(connectionProvider);
public ApprovalTaskRepository ordoApprovalTaskRepository(
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
return new JdbcApprovalTaskRepository(connectionProvider, ordoSqlDialect);
}
@Bean
@ConditionalOnMissingBean
public ProcessHistoryRepository ordoProcessHistoryRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcProcessHistoryRepository(connectionProvider);
public ProcessHistoryRepository ordoProcessHistoryRepository(
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
return new JdbcProcessHistoryRepository(connectionProvider, ordoSqlDialect);
}
@Bean
@ConditionalOnMissingBean
public ActionExecutionRepository ordoActionExecutionRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcActionExecutionRepository(connectionProvider);
public ActionExecutionRepository ordoActionExecutionRepository(
JdbcConnectionProvider connectionProvider, SqlDialect ordoSqlDialect) {
return new JdbcActionExecutionRepository(connectionProvider, ordoSqlDialect);
}
@Bean
@@ -11,6 +11,7 @@ public class OrdoProperties {
private final Definitions definitions = new Definitions();
private final Due due = new Due();
private final Jdbc jdbc = new Jdbc();
public boolean isEnabled() {
return enabled;
@@ -28,6 +29,10 @@ public class OrdoProperties {
return due;
}
public Jdbc getJdbc() {
return jdbc;
}
public static class Definitions {
private String location = "classpath*:ordo/*.json";
@@ -52,4 +57,17 @@ public class OrdoProperties {
this.pollMs = pollMs;
}
}
public static class Jdbc {
/** Explicit dialect id ({@code postgresql}, {@code mysql}). Empty means detect from the DataSource. */
private String dialect;
public String getDialect() {
return dialect;
}
public void setDialect(String dialect) {
this.dialect = dialect;
}
}
}
@@ -1 +1,2 @@
com.jetlumen.ordo.spring.OrdoFlywayAutoConfiguration
com.jetlumen.ordo.spring.OrdoJdbcAutoConfiguration
@@ -13,6 +13,7 @@ import com.jetlumen.ordo.api.ProcessInstance;
import com.jetlumen.ordo.api.RoutingCondition;
import com.jetlumen.ordo.api.query.PageRequest;
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
@@ -34,7 +35,8 @@ class OrdoJdbcAutoConfigurationTest {
private final ApplicationContextRunner withDataSourceRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
DataSourceAutoConfiguration.class, FlywayAutoConfiguration.class, OrdoJdbcAutoConfiguration.class))
DataSourceAutoConfiguration.class, OrdoFlywayAutoConfiguration.class,
FlywayAutoConfiguration.class, OrdoJdbcAutoConfiguration.class))
.withPropertyValues(
"spring.datasource.url=jdbc:h2:mem:ordo_" + UUID.randomUUID()
+ ";MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1",
@@ -47,6 +49,7 @@ class OrdoJdbcAutoConfigurationTest {
void assemblesJdbcBackedEngineWhenDataSourceIsPresent() {
withDataSourceRunner.run(context -> {
assertThat(context).hasSingleBean(OrdoEngine.class);
assertThat(context.getBean(SqlDialect.class).id()).isEqualTo("postgresql");
OrdoEngine engine = context.getBean(OrdoEngine.class);
engine.publish(LEAVE_REQUEST);
@@ -60,6 +63,12 @@ class OrdoJdbcAutoConfigurationTest {
});
}
@Test
void honoursExplicitMysqlDialect() {
withDataSourceRunner.withPropertyValues("ordo.jdbc.dialect=mysql", "spring.flyway.enabled=false")
.run(context -> assertThat(context.getBean(SqlDialect.class).id()).isEqualTo("mysql"));
}
@Test
void backsOffWhenNoDataSourceBeanIsPresent() {
withoutDataSourceRunner.run(context ->