feat: add spring boot autoconfiguration and starter modules with JDBC support

This commit is contained in:
0264408
2026-09-09 11:08:17 +08:00
parent 397a9c229b
commit c782e6cdc3
8 changed files with 351 additions and 0 deletions
@@ -0,0 +1,98 @@
package com.jetlumen.ordo.spring;
import com.jetlumen.ordo.api.AssigneeResolver;
import com.jetlumen.ordo.api.OrdoEngine;
import com.jetlumen.ordo.api.TransactionExecutor;
import com.jetlumen.ordo.api.repository.ApprovalTaskRepository;
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
import com.jetlumen.ordo.core.DefaultOrdoEngine;
import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcConnectionProvider;
import com.jetlumen.ordo.storage.jdbc.JdbcProcessDefinitionRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcProcessInstanceRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcTransactionExecutor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
import java.time.Clock;
/**
* Autoconfigures a JDBC-backed {@link OrdoEngine} when a {@link DataSource} bean is
* present. Any bean already defined by the application (e.g. a custom
* {@link AssigneeResolver} or repository implementation) takes precedence over the
* defaults created here.
*/
@AutoConfiguration
@ConditionalOnProperty(prefix = "ordo", name = "enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnClass(DataSource.class)
@ConditionalOnBean(DataSource.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class, FlywayAutoConfiguration.class})
@EnableConfigurationProperties(OrdoProperties.class)
public class OrdoJdbcAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public Clock ordoClock() {
return Clock.systemUTC();
}
@Bean
@ConditionalOnMissingBean
public AssigneeResolver ordoAssigneeResolver() {
return AssigneeResolver.direct();
}
@Bean
@ConditionalOnMissingBean
@DependsOnDatabaseInitialization
public JdbcConnectionProvider ordoJdbcConnectionProvider(DataSource dataSource) {
return new JdbcConnectionProvider(dataSource);
}
@Bean
@ConditionalOnMissingBean
public TransactionExecutor ordoTransactionExecutor(JdbcConnectionProvider connectionProvider) {
return new JdbcTransactionExecutor(connectionProvider);
}
@Bean
@ConditionalOnMissingBean
public ProcessDefinitionRepository ordoProcessDefinitionRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcProcessDefinitionRepository(connectionProvider);
}
@Bean
@ConditionalOnMissingBean
public ProcessInstanceRepository ordoProcessInstanceRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcProcessInstanceRepository(connectionProvider);
}
@Bean
@ConditionalOnMissingBean
public ApprovalTaskRepository ordoApprovalTaskRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcApprovalTaskRepository(connectionProvider);
}
@Bean
@ConditionalOnMissingBean
public OrdoEngine ordoEngine(Clock ordoClock,
AssigneeResolver ordoAssigneeResolver,
TransactionExecutor ordoTransactionExecutor,
ProcessDefinitionRepository ordoProcessDefinitionRepository,
ProcessInstanceRepository ordoProcessInstanceRepository,
ApprovalTaskRepository ordoApprovalTaskRepository) {
return new DefaultOrdoEngine(ordoClock, ordoAssigneeResolver, ordoTransactionExecutor,
ordoProcessDefinitionRepository, ordoProcessInstanceRepository, ordoApprovalTaskRepository);
}
}
@@ -0,0 +1,19 @@
package com.jetlumen.ordo.spring;
import org.springframework.boot.context.properties.ConfigurationProperties;
/** Configuration properties for the Ordo Spring Boot autoconfiguration. */
@ConfigurationProperties(prefix = "ordo")
public class OrdoProperties {
/** Whether the Ordo JDBC autoconfiguration should be applied. */
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
@@ -0,0 +1 @@
com.jetlumen.ordo.spring.OrdoJdbcAutoConfiguration
@@ -0,0 +1,86 @@
package com.jetlumen.ordo.spring;
import com.jetlumen.ordo.api.ApprovalStep;
import com.jetlumen.ordo.api.ApprovalTask;
import com.jetlumen.ordo.api.AssigneeResolver;
import com.jetlumen.ordo.api.OrdoEngine;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessInstance;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
class OrdoJdbcAutoConfigurationTest {
private static final ProcessDefinition LEAVE_REQUEST = new ProcessDefinition("leave-request", "Leave request",
List.of(new ApprovalStep("manager", "Manager approval", "maria")));
private final ApplicationContextRunner withDataSourceRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
DataSourceAutoConfiguration.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",
"spring.datasource.username=sa");
private final ApplicationContextRunner withoutDataSourceRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OrdoJdbcAutoConfiguration.class));
@Test
void assemblesJdbcBackedEngineWhenDataSourceIsPresent() {
withDataSourceRunner.run(context -> {
assertThat(context).hasSingleBean(OrdoEngine.class);
OrdoEngine engine = context.getBean(OrdoEngine.class);
engine.register(LEAVE_REQUEST);
ProcessInstance instance = engine.start("leave-request", "alice");
List<ApprovalTask> pending = engine.findPendingTasksByAssignee("maria");
assertThat(pending).hasSize(1);
ApprovalTask approved = engine.approve(pending.get(0).id(), "maria", "looks good");
assertThat(approved.status().name()).isEqualTo("APPROVED");
assertThat(engine.findInstance(instance.id())).isPresent();
});
}
@Test
void backsOffWhenNoDataSourceBeanIsPresent() {
withoutDataSourceRunner.run(context ->
assertThat(context).doesNotHaveBean(OrdoEngine.class));
}
@Test
void backsOffWhenDisabledViaProperty() {
withDataSourceRunner.withPropertyValues("ordo.enabled=false")
.run(context ->
assertThat(context).doesNotHaveBean(OrdoEngine.class));
}
@Test
void honoursUserDefinedAssigneeResolver() {
withDataSourceRunner.withUserConfiguration(CustomAssigneeResolverConfig.class)
.run(context -> {
assertThat(context).hasSingleBean(OrdoEngine.class);
assertThat(context.getBean(AssigneeResolver.class)).isNotNull();
assertThat(context.getBean(AssigneeResolver.class).resolve(null, null))
.isEqualTo("custom-assignee");
});
}
@Configuration
static class CustomAssigneeResolverConfig {
@Bean
AssigneeResolver ordoAssigneeResolver() {
return (step, ctx) -> "custom-assignee";
}
}
}