extended analysis

This commit is contained in:
2026-06-14 16:32:51 +02:00
parent 2251a587d9
commit ab474c0766
125 changed files with 4180 additions and 317 deletions

View File

@@ -0,0 +1,30 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'click.kamil'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-artemis' // For JMS
implementation 'org.springframework.boot:spring-boot-starter-amqp' // For RabbitMQ
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}

View File

@@ -0,0 +1,15 @@
package click.kamil.examples.enterprise.api;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/api/enterprise/orders")
public interface OrderApi {
@PostMapping("/place")
void placeOrder();
@PostMapping("/{id}/cancel")
void cancelOrder(@PathVariable("id") String id);
}

View File

@@ -0,0 +1,22 @@
package click.kamil.examples.enterprise.api;
import click.kamil.examples.enterprise.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class OrderController implements OrderApi {
private final OrderService orderService;
@Override
public void placeOrder() {
orderService.place();
}
@Override
public void cancelOrder(String id) {
orderService.cancel(id);
}
}

View File

@@ -0,0 +1,20 @@
package click.kamil.examples.enterprise.api;
import click.kamil.examples.enterprise.service.ReactivePaymentService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequiredArgsConstructor
public class ReactivePaymentController {
private final ReactivePaymentService paymentService;
@PostMapping("/api/enterprise/payments")
public Mono<Void> pay(@RequestBody String orderId) {
return paymentService.processPayment(orderId);
}
}

View File

@@ -0,0 +1,20 @@
package click.kamil.examples.enterprise.api;
import click.kamil.examples.enterprise.constants.OrderEvents;
import lombok.RequiredArgsConstructor;
import org.springframework.statemachine.StateMachine;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@RequiredArgsConstructor
public class SecurityInterceptor implements HandlerInterceptor {
private final StateMachine<String, String> stateMachine;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
stateMachine.sendEvent(OrderEvents.AUDIT);
return true;
}
}

View File

@@ -0,0 +1,64 @@
package click.kamil.examples.enterprise.config;
import click.kamil.examples.enterprise.constants.OrderEvents;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
@Configuration
@EnableStateMachine(name = "enterpriseStateMachine")
public class EnterpriseStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
@Value("${order.initial.state:NEW}")
private String initialState;
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial(initialState)
.state("PENDING_PAYMENT")
.state("PAID")
.state("SHIPPED")
.choice("CHECK_AVAILABILITY")
.end("CANCELLED")
.end("RETURNED")
.end("DELIVERED");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("NEW").target("CHECK_AVAILABILITY")
.event(OrderEvents.PLACE)
.and()
.withChoice()
.source("CHECK_AVAILABILITY")
.first("PENDING_PAYMENT", (c) -> true)
.last("CANCELLED")
.and()
.withExternal()
.source("PENDING_PAYMENT").target("PAID")
.event(OrderEvents.PAY)
.and()
.withExternal()
.source("PAID").target("SHIPPED")
.event(OrderEvents.SHIP)
.and()
.withExternal()
.source("SHIPPED").target("DELIVERED")
.event("FINALIZE")
.and()
.withExternal()
.source("PAID").target("CANCELLED")
.event(OrderEvents.CANCEL)
.and()
.withExternal()
.source("DELIVERED").target("RETURNED")
.event(OrderEvents.RETURN);
}
}

View File

@@ -0,0 +1,10 @@
package click.kamil.examples.enterprise.constants;
public class OrderEvents {
public static final String PLACE = "PLACE_ORDER";
public static final String PAY = "PAY_ORDER";
public static final String SHIP = "SHIP_ORDER";
public static final String CANCEL = "CANCEL_ORDER";
public static final String RETURN = "RETURN_ORDER";
public static final String AUDIT = "AUDIT_EVENT";
}

View File

@@ -0,0 +1,19 @@
package click.kamil.examples.enterprise.messaging;
import click.kamil.examples.enterprise.constants.OrderEvents;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ReturnsRabbitListener {
private final StateMachine<String, String> stateMachine;
@RabbitListener(queues = "returns.queue")
public void onReturn(String orderId) {
stateMachine.sendEvent(OrderEvents.RETURN);
}
}

View File

@@ -0,0 +1,19 @@
package click.kamil.examples.enterprise.messaging;
import click.kamil.examples.enterprise.constants.OrderEvents;
import lombok.RequiredArgsConstructor;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ShippingJmsListener {
private final StateMachine<String, String> stateMachine;
@JmsListener(destination = "shipping.queue")
public void onShippingReady(String orderId) {
stateMachine.sendEvent(OrderEvents.SHIP);
}
}

View File

@@ -0,0 +1,11 @@
package click.kamil.examples.enterprise.service;
import org.springframework.stereotype.Service;
@Service
public class ExternalTaxService {
public void calculateTax(String orderId) {
// Third-party logic
System.out.println("Calculating tax for " + orderId);
}
}

View File

@@ -0,0 +1,6 @@
package click.kamil.examples.enterprise.service;
public interface OrderService {
void place();
void cancel(String id);
}

View File

@@ -0,0 +1,23 @@
package click.kamil.examples.enterprise.service;
import click.kamil.examples.enterprise.constants.OrderEvents;
import lombok.RequiredArgsConstructor;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final StateMachine<String, String> stateMachine;
@Override
public void place() {
stateMachine.sendEvent(OrderEvents.PLACE);
}
@Override
public void cancel(String id) {
stateMachine.sendEvent(OrderEvents.CANCEL);
}
}

View File

@@ -0,0 +1,22 @@
package click.kamil.examples.enterprise.service;
import lombok.RequiredArgsConstructor;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import click.kamil.examples.enterprise.constants.OrderEvents;
@Service
@RequiredArgsConstructor
public class ReactivePaymentService {
private final StateMachine<String, String> stateMachine;
public Mono<Void> processPayment(String orderId) {
return Mono.just(orderId)
.doOnSuccess(id -> {
stateMachine.sendEvent(OrderEvents.PAY);
})
.then();
}
}

View File

@@ -0,0 +1,6 @@
[
{
"methodFqn": "ExternalTaxService.calculateTax",
"event": "FINALIZE"
}
]

View File

@@ -0,0 +1,6 @@
build/
out/
.gradle/
.idea/
*.class
*.log

View File

@@ -0,0 +1,48 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'click.kamil'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
implementation 'io.projectreactor:reactor-core'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
bootJar {
enabled = false
}
jar {
enabled = true
}

View File

@@ -0,0 +1,55 @@
package click.kamil.examples.statemachine.extended.config;
import click.kamil.examples.statemachine.extended.constants.MyEvents;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.beans.factory.annotation.Value;
@Configuration
@EnableStateMachine(name = "extendedStateMachine")
public class ExtendedStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
@Value("${app.states.initial:START}")
private String initialState;
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial(initialState)
.state("PROCESSING")
.state("COMPLETED")
.state("CANCELLED");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("START").target("PROCESSING")
.event(MyEvents.SUBMIT)
.and()
.withExternal()
.source("PROCESSING").target("COMPLETED")
.event("FINISH")
.and()
.withExternal()
.source("PROCESSING").target("CANCELLED")
.event(MyEvents.CANCEL)
.and()
.withExternal()
.source("START").target("PROCESSING")
.event("REACTIVE_EVENT")
.and()
.withExternal()
.source("START").target("START")
.event("AUDIT_EVENT")
.and()
.withExternal()
.source("START").target("PROCESSING")
.event("EXTERNAL_TRIGGER");
}
}

View File

@@ -0,0 +1,6 @@
package click.kamil.examples.statemachine.extended.constants;
public class MyEvents {
public static final String SUBMIT = "SUBMIT_EVENT";
public static final String CANCEL = "CANCEL_EVENT";
}

View File

@@ -0,0 +1,11 @@
package click.kamil.examples.statemachine.extended.service;
import org.springframework.stereotype.Service;
@Service
public class ExternalNotificationService {
public void sendExternalNotification(String message) {
// Simulated third-party call that triggers a state change in an external system
System.out.println("Notifying external system: " + message);
}
}

View File

@@ -0,0 +1,31 @@
package click.kamil.examples.statemachine.extended.service;
import click.kamil.examples.statemachine.extended.constants.MyEvents;
import lombok.RequiredArgsConstructor;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.persist.StateMachinePersister;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class OrderService {
private final StateMachine<String, String> stateMachine;
private final StateMachinePersister<String, String, String> persister;
private final ExternalNotificationService notificationService;
public void processSubmit() {
// Business logic here...
notificationService.sendExternalNotification("Submit processed");
stateMachine.sendEvent(MyEvents.SUBMIT);
}
public void processCancel() {
// More business logic...
stateMachine.sendEvent(MyEvents.CANCEL);
}
public void resumeOrder(String orderId) throws Exception {
persister.restore(stateMachine, orderId);
}
}

View File

@@ -0,0 +1,22 @@
package click.kamil.examples.statemachine.extended.service;
import lombok.RequiredArgsConstructor;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
@RequiredArgsConstructor
public class ReactiveOrderService {
private final StateMachine<String, String> stateMachine;
public Mono<Void> processReactive(String orderId) {
return Mono.just(orderId)
.doOnNext(id -> {
// Hidden trigger inside a lambda!
stateMachine.sendEvent("REACTIVE_EVENT");
})
.then();
}
}

View File

@@ -0,0 +1,19 @@
package click.kamil.examples.statemachine.extended.web;
import lombok.RequiredArgsConstructor;
import org.springframework.statemachine.StateMachine;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@RequiredArgsConstructor
public class AuditInterceptor implements HandlerInterceptor {
private final StateMachine<String, String> stateMachine;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
stateMachine.sendEvent("AUDIT_EVENT");
return true;
}
}

View File

@@ -0,0 +1,46 @@
package click.kamil.examples.statemachine.extended.web;
import click.kamil.examples.statemachine.extended.constants.MyEvents;
import click.kamil.examples.statemachine.extended.service.OrderService;
import click.kamil.examples.statemachine.extended.service.ReactiveOrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.statemachine.StateMachine;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final StateMachine<String, String> stateMachine;
private final OrderService orderService;
private final ReactiveOrderService reactiveOrderService;
@PostMapping("/submit")
public void submitOrder() {
orderService.processSubmit();
}
@PostMapping("/cancel")
public void cancelOrder() {
orderService.processCancel();
}
@PostMapping("/finish")
public void finishOrder() {
stateMachine.sendEvent("FINISH");
}
@PostMapping("/resume")
public void resumeOrder(String orderId) throws Exception {
orderService.resumeOrder(orderId);
}
@PostMapping("/reactive")
public Mono<Void> reactiveOrder(String orderId) {
return reactiveOrderService.processReactive(orderId);
}
}

View File

@@ -0,0 +1 @@
app.states.initial=PROD_INITIAL

View File

@@ -0,0 +1 @@
app.states.initial=INIT_STATE

View File

@@ -0,0 +1,6 @@
[
{
"methodFqn": "ExternalNotificationService.sendExternalNotification",
"event": "EXTERNAL_TRIGGER"
}
]

View File

@@ -0,0 +1,33 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'click.kamil'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
bootJar {
enabled = false
}
jar {
enabled = true
}

View File

@@ -0,0 +1,11 @@
package click.kamil.examples.statemachine.inheritance.api;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/api/v2/orders")
public interface OrderApi {
@PostMapping("/submit")
void submitOrder();
}

View File

@@ -0,0 +1,20 @@
package click.kamil.examples.statemachine.inheritance.api;
import click.kamil.examples.statemachine.inheritance.service.ProcessingService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderControllerImpl implements OrderApi {
private final ProcessingService processingService;
public OrderControllerImpl(ProcessingService processingService) {
this.processingService = processingService;
}
@Override
public void submitOrder() {
processingService.doProcess();
}
}

View File

@@ -0,0 +1,28 @@
package click.kamil.examples.statemachine.inheritance.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
@Configuration
@EnableStateMachine
public class InheritanceStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("START")
.state("WORKING");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("START").target("WORKING")
.event("INHERITED_SUBMIT");
}
}

View File

@@ -0,0 +1,5 @@
package click.kamil.examples.statemachine.inheritance.service;
public interface ProcessingService {
void doProcess();
}

View File

@@ -0,0 +1,17 @@
package click.kamil.examples.statemachine.inheritance.service;
import lombok.RequiredArgsConstructor;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ProcessingServiceImpl implements ProcessingService {
private final StateMachine<String, String> stateMachine;
@Override
public void doProcess() {
stateMachine.sendEvent("INHERITED_SUBMIT");
}
}