Replace .get() heuristics with type-based access classification and extend literal dataflow resolution.

Introduce ExpressionAccessClassifier to distinguish map lookups from Supplier/bean accessors, fold Map.of and string/array literals at compile time, and add regression tests for dispatcher and dataflow paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 10:14:23 +02:00
parent 12183693aa
commit c447641800
48 changed files with 4174 additions and 184 deletions

View File

@@ -0,0 +1,26 @@
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'
}
bootJar { enabled = false }
jar { enabled = true }

View File

@@ -0,0 +1,12 @@
package click.kamil.examples.statemachine.layered;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LayeredDispatcherApplication {
public static void main(String[] args) {
SpringApplication.run(LayeredDispatcherApplication.class, args);
}
}

View File

@@ -0,0 +1,15 @@
package click.kamil.examples.statemachine.layered.command;
/**
* Intermediate command enum: endpoints map HTTP string keys to these values first,
* then {@link click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher}
* maps them to concrete state-machine transition events.
*/
public enum DomainCommand {
ORDER_PAY,
ORDER_SHIP,
ORDER_CANCEL,
DOCUMENT_SUBMIT,
DOCUMENT_APPROVE,
DOCUMENT_REJECT
}

View File

@@ -0,0 +1,83 @@
package click.kamil.examples.statemachine.layered.dispatch;
import click.kamil.examples.statemachine.layered.command.DomainCommand;
import click.kamil.examples.statemachine.layered.document.DocumentState;
import click.kamil.examples.statemachine.layered.document.DocumentTransitionEvent;
import click.kamil.examples.statemachine.layered.order.OrderState;
import click.kamil.examples.statemachine.layered.order.OrderTransitionEvent;
import click.kamil.examples.statemachine.layered.order.PayRichOrderEvent;
import click.kamil.examples.statemachine.layered.order.ShipRichOrderEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.stereotype.Service;
@Service
public class CentralEventDispatcher {
private final StateMachineFactory<OrderState, OrderTransitionEvent> orderStateMachineFactory;
private final StateMachineFactory<DocumentState, DocumentTransitionEvent> documentStateMachineFactory;
@Autowired
public CentralEventDispatcher(
StateMachineFactory<OrderState, OrderTransitionEvent> orderStateMachineFactory,
StateMachineFactory<DocumentState, DocumentTransitionEvent> documentStateMachineFactory) {
this.orderStateMachineFactory = orderStateMachineFactory;
this.documentStateMachineFactory = documentStateMachineFactory;
}
public void dispatch(DomainCommand command) {
switch (command) {
case ORDER_PAY -> orderPay();
case ORDER_SHIP -> orderShip();
case ORDER_CANCEL -> orderCancel();
case DOCUMENT_SUBMIT -> documentSubmit();
case DOCUMENT_APPROVE -> documentApprove();
case DOCUMENT_REJECT -> documentReject();
}
}
public void orderPay() {
sendOrderEvent(OrderTransitionEvent.PAY);
}
public void orderShip() {
sendOrderEvent(OrderTransitionEvent.SHIP);
}
public void orderCancel() {
sendOrderEvent(OrderTransitionEvent.CANCEL);
}
public void orderPayViaRichEvent(PayRichOrderEvent event) {
sendOrderEvent(event.getType());
}
public void orderShipViaRichEvent(ShipRichOrderEvent event) {
sendOrderEvent(event.getType());
}
public void documentSubmit() {
sendDocumentEvent(DocumentTransitionEvent.SUBMIT);
}
public void documentApprove() {
sendDocumentEvent(DocumentTransitionEvent.APPROVE);
}
public void documentReject() {
sendDocumentEvent(DocumentTransitionEvent.REJECT);
}
private void sendOrderEvent(OrderTransitionEvent event) {
StateMachine<OrderState, OrderTransitionEvent> machine = orderStateMachineFactory.getStateMachine();
machine.start();
machine.sendEvent(event);
}
private void sendDocumentEvent(DocumentTransitionEvent event) {
StateMachine<DocumentState, DocumentTransitionEvent> machine = documentStateMachineFactory.getStateMachine();
machine.start();
machine.sendEvent(event);
}
}

View File

@@ -0,0 +1,23 @@
package click.kamil.examples.statemachine.layered.dispatch;
import click.kamil.examples.statemachine.layered.command.DomainCommand;
import org.springframework.stereotype.Component;
@Component
public class StringCommandMapper {
public DomainCommand fromString(String commandKey) {
if (commandKey == null) {
throw new IllegalArgumentException("commandKey must not be null");
}
return switch (commandKey) {
case "order.pay" -> DomainCommand.ORDER_PAY;
case "order.ship" -> DomainCommand.ORDER_SHIP;
case "order.cancel" -> DomainCommand.ORDER_CANCEL;
case "document.submit" -> DomainCommand.DOCUMENT_SUBMIT;
case "document.approve" -> DomainCommand.DOCUMENT_APPROVE;
case "document.reject" -> DomainCommand.DOCUMENT_REJECT;
default -> throw new IllegalArgumentException("Unknown command: " + commandKey);
};
}
}

View File

@@ -0,0 +1,8 @@
package click.kamil.examples.statemachine.layered.document;
public enum DocumentState {
DRAFT,
SUBMITTED,
APPROVED,
REJECTED
}

View File

@@ -0,0 +1,7 @@
package click.kamil.examples.statemachine.layered.document;
public enum DocumentTransitionEvent {
SUBMIT,
APPROVE,
REJECT
}

View File

@@ -0,0 +1,46 @@
package click.kamil.examples.statemachine.layered.document.config;
import click.kamil.examples.statemachine.layered.document.DocumentState;
import click.kamil.examples.statemachine.layered.document.DocumentTransitionEvent;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
public abstract class AbstractDocumentStateMachineConfiguration
extends EnumStateMachineConfigurerAdapter<DocumentState, DocumentTransitionEvent> {
@Override
public void configure(StateMachineStateConfigurer<DocumentState, DocumentTransitionEvent> states)
throws Exception {
states
.withStates()
.initial(DocumentState.DRAFT)
.state(DocumentState.SUBMITTED)
.state(DocumentState.APPROVED);
configureAdditionalStates(states);
}
@Override
public void configure(StateMachineTransitionConfigurer<DocumentState, DocumentTransitionEvent> transitions)
throws Exception {
transitions
.withExternal()
.source(DocumentState.DRAFT)
.target(DocumentState.SUBMITTED)
.event(DocumentTransitionEvent.SUBMIT)
.and()
.withExternal()
.source(DocumentState.SUBMITTED)
.target(DocumentState.APPROVED)
.event(DocumentTransitionEvent.APPROVE);
configureAdditionalTransitions(transitions);
}
protected void configureAdditionalStates(StateMachineStateConfigurer<DocumentState, DocumentTransitionEvent> states)
throws Exception {
}
protected void configureAdditionalTransitions(
StateMachineTransitionConfigurer<DocumentState, DocumentTransitionEvent> transitions) throws Exception {
}
}

View File

@@ -0,0 +1,29 @@
package click.kamil.examples.statemachine.layered.document.config;
import click.kamil.examples.statemachine.layered.document.DocumentState;
import click.kamil.examples.statemachine.layered.document.DocumentTransitionEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
@Configuration
@EnableStateMachineFactory(name = "documentStateMachineFactory")
public class StandardDocumentStateMachineConfiguration extends AbstractDocumentStateMachineConfiguration {
@Override
protected void configureAdditionalStates(StateMachineStateConfigurer<DocumentState, DocumentTransitionEvent> states)
throws Exception {
states.state(DocumentState.REJECTED);
}
@Override
protected void configureAdditionalTransitions(
StateMachineTransitionConfigurer<DocumentState, DocumentTransitionEvent> transitions) throws Exception {
transitions
.withExternal()
.source(DocumentState.SUBMITTED)
.target(DocumentState.REJECTED)
.event(DocumentTransitionEvent.REJECT);
}
}

View File

@@ -0,0 +1,8 @@
package click.kamil.examples.statemachine.layered.order;
public enum OrderState {
NEW,
PAID,
SHIPPED,
CANCELLED
}

View File

@@ -0,0 +1,7 @@
package click.kamil.examples.statemachine.layered.order;
public enum OrderTransitionEvent {
PAY,
SHIP,
CANCEL
}

View File

@@ -0,0 +1,8 @@
package click.kamil.examples.statemachine.layered.order;
public class PayRichOrderEvent implements RichOrderEvent {
@Override
public OrderTransitionEvent getType() {
return OrderTransitionEvent.PAY;
}
}

View File

@@ -0,0 +1,7 @@
package click.kamil.examples.statemachine.layered.order;
import click.kamil.examples.statemachine.layered.order.OrderTransitionEvent;
public interface RichOrderEvent {
OrderTransitionEvent getType();
}

View File

@@ -0,0 +1,8 @@
package click.kamil.examples.statemachine.layered.order;
public class ShipRichOrderEvent implements RichOrderEvent {
@Override
public OrderTransitionEvent getType() {
return OrderTransitionEvent.SHIP;
}
}

View File

@@ -0,0 +1,45 @@
package click.kamil.examples.statemachine.layered.order.config;
import click.kamil.examples.statemachine.layered.order.OrderState;
import click.kamil.examples.statemachine.layered.order.OrderTransitionEvent;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
public abstract class AbstractOrderStateMachineConfiguration
extends EnumStateMachineConfigurerAdapter<OrderState, OrderTransitionEvent> {
@Override
public void configure(StateMachineStateConfigurer<OrderState, OrderTransitionEvent> states) throws Exception {
states
.withStates()
.initial(OrderState.NEW)
.state(OrderState.PAID)
.state(OrderState.SHIPPED);
configureAdditionalStates(states);
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderState, OrderTransitionEvent> transitions)
throws Exception {
transitions
.withExternal()
.source(OrderState.NEW)
.target(OrderState.PAID)
.event(OrderTransitionEvent.PAY)
.and()
.withExternal()
.source(OrderState.PAID)
.target(OrderState.SHIPPED)
.event(OrderTransitionEvent.SHIP);
configureAdditionalTransitions(transitions);
}
protected void configureAdditionalStates(StateMachineStateConfigurer<OrderState, OrderTransitionEvent> states)
throws Exception {
}
protected void configureAdditionalTransitions(
StateMachineTransitionConfigurer<OrderState, OrderTransitionEvent> transitions) throws Exception {
}
}

View File

@@ -0,0 +1,29 @@
package click.kamil.examples.statemachine.layered.order.config;
import click.kamil.examples.statemachine.layered.order.OrderState;
import click.kamil.examples.statemachine.layered.order.OrderTransitionEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
@Configuration
@EnableStateMachineFactory(name = "orderStateMachineFactory")
public class StandardOrderStateMachineConfiguration extends AbstractOrderStateMachineConfiguration {
@Override
protected void configureAdditionalStates(StateMachineStateConfigurer<OrderState, OrderTransitionEvent> states)
throws Exception {
states.state(OrderState.CANCELLED);
}
@Override
protected void configureAdditionalTransitions(
StateMachineTransitionConfigurer<OrderState, OrderTransitionEvent> transitions) throws Exception {
transitions
.withExternal()
.source(OrderState.PAID)
.target(OrderState.CANCELLED)
.event(OrderTransitionEvent.CANCEL);
}
}

View File

@@ -0,0 +1,61 @@
package click.kamil.examples.statemachine.layered.web;
import click.kamil.examples.statemachine.layered.command.DomainCommand;
import click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher;
import click.kamil.examples.statemachine.layered.dispatch.StringCommandMapper;
import click.kamil.examples.statemachine.layered.order.PayRichOrderEvent;
import click.kamil.examples.statemachine.layered.order.ShipRichOrderEvent;
import org.springframework.stereotype.Service;
@Service
public class CommandGateway {
private final StringCommandMapper stringCommandMapper;
private final CentralEventDispatcher centralEventDispatcher;
public CommandGateway(StringCommandMapper stringCommandMapper, CentralEventDispatcher centralEventDispatcher) {
this.stringCommandMapper = stringCommandMapper;
this.centralEventDispatcher = centralEventDispatcher;
}
public void payOrder() {
centralEventDispatcher.orderPay();
}
public void shipOrder() {
centralEventDispatcher.orderShip();
}
public void cancelOrder() {
centralEventDispatcher.orderCancel();
}
public void payOrderViaRichEvent(PayRichOrderEvent event) {
centralEventDispatcher.orderPayViaRichEvent(event);
}
public void shipOrderViaRichEvent(ShipRichOrderEvent event) {
centralEventDispatcher.orderShipViaRichEvent(event);
}
public void submitDocument() {
centralEventDispatcher.documentSubmit();
}
public void approveDocument() {
centralEventDispatcher.documentApprove();
}
public void rejectDocument() {
centralEventDispatcher.documentReject();
}
/**
* Full two-hop dispatch used by {@link GenericCommandController}:
* string key → {@link DomainCommand} → transition event.
*/
public void executeViaMapper(String commandKey) {
DomainCommand command = stringCommandMapper.fromString(commandKey);
centralEventDispatcher.dispatch(command);
}
}

View File

@@ -0,0 +1,31 @@
package click.kamil.examples.statemachine.layered.web;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
private final CommandGateway commandGateway;
public DocumentController(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}
@PostMapping("/submit")
public void submit() {
commandGateway.submitDocument();
}
@PostMapping("/approve")
public void approve() {
commandGateway.approveDocument();
}
@PostMapping("/reject")
public void reject() {
commandGateway.rejectDocument();
}
}

View File

@@ -0,0 +1,22 @@
package click.kamil.examples.statemachine.layered.web;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/commands")
public class GenericCommandController {
private final CommandGateway commandGateway;
public GenericCommandController(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}
@PostMapping("/{commandKey}")
public void execute(@PathVariable String commandKey) {
commandGateway.executeViaMapper(commandKey);
}
}

View File

@@ -0,0 +1,31 @@
package click.kamil.examples.statemachine.layered.web;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final CommandGateway commandGateway;
public OrderController(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}
@PostMapping("/pay")
public void pay() {
commandGateway.payOrder();
}
@PostMapping("/ship")
public void ship() {
commandGateway.shipOrder();
}
@PostMapping("/cancel")
public void cancel() {
commandGateway.cancelOrder();
}
}

View File

@@ -0,0 +1,28 @@
package click.kamil.examples.statemachine.layered.web;
import click.kamil.examples.statemachine.layered.order.PayRichOrderEvent;
import click.kamil.examples.statemachine.layered.order.ShipRichOrderEvent;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/orders/rich")
public class RichOrderController {
private final CommandGateway commandGateway;
public RichOrderController(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}
@PostMapping("/pay")
public void payViaRichEvent() {
commandGateway.payOrderViaRichEvent(new PayRichOrderEvent());
}
@PostMapping("/ship")
public void shipViaRichEvent() {
commandGateway.shipOrderViaRichEvent(new ShipRichOrderEvent());
}
}

View File

@@ -0,0 +1,31 @@
package click.kamil.examples.statemachine.layered.web;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Demonstrates the full two-hop dispatch chain with string literals:
* string key → {@link click.kamil.examples.statemachine.layered.command.DomainCommand}
* → transition event.
*/
@RestController
@RequestMapping("/api/string-dispatch")
public class StringDispatchController {
private final CommandGateway commandGateway;
public StringDispatchController(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}
@PostMapping("/orders/pay")
public void payWithStringKey() {
commandGateway.executeViaMapper("order.pay");
}
@PostMapping("/orders/ship")
public void shipWithStringKey() {
commandGateway.executeViaMapper("order.ship");
}
}