Files
spring-state-machine-renderer/plan-extended-anaylis/generic_extensibility.md

3.7 KiB

Generic and Extensible Endpoint Finder

The Need for Genericity

Applications use various frameworks and patterns to trigger state machine events. While Spring MVC and Kafka are common, others like Micronuat, Jakarta EE, or custom internal event buses might be used.

Configuration-Driven Detection

We can allow users to define "Entry Point" patterns via a configuration file (e.g., analysis-config.json).

{
  "entryPoints": [
    {
      "name": "SpringMVC",
      "classAnnotations": ["RestController", "Controller"],
      "methodAnnotations": ["PostMapping", "GetMapping", "PutMapping", "DeleteMapping", "RequestMapping"],
      "metadataExtractors": {
        "path": ["value", "path"]
      }
    },
    {
      "name": "Kafka",
      "methodAnnotations": ["KafkaListener"],
      "metadataExtractors": {
        "topic": ["topics", "topicPartitions"]
      }
    }
  ]
}

Abstract EntryPoint Detector

A generic detector that uses the above configuration to identify entry points.

public class ConfigurableTriggerDetector implements TriggerDetector {
    private final AnalysisConfig config;

    @Override
    public List<TriggerPoint> detect(CompilationUnit cu, CodebaseContext context) {
        // 1. Find all classes
        // 2. If class has any of config.classAnnotations
        // 3. Scan methods for any of config.methodAnnotations
        // 4. If found, look for sendEvent calls inside the method or its call tree
    }
}

Indirect Flow Detection (Call Tree Analysis)

Oftentimes, the sendEvent call is not directly in the controller method but in a service called by the controller.

Simple Call Tree Analysis

  1. Find all methods that call sendEvent(E). Let's call them "Direct Triggers".
  2. Find all methods that call "Direct Triggers".
  3. Recursively build this "Trigger Path".
  4. If a "Trigger Path" starts at an "Entry Point" (Controller/Listener), we have a complete flow.

Example: OrderController.submitOrder() -> OrderService.processOrder() -> StateMachine.sendEvent(SUBMIT)

Analysis:

  1. OrderService.processOrder() is a Direct Trigger for SUBMIT.
  2. OrderController.submitOrder() calls OrderService.processOrder().
  3. OrderController.submitOrder() is an Entry Point (annotated with @PostMapping).
  4. Result: POST /order -> SUBMIT transition.

Handling Reactive Chains Statically

In WebFlux/Project Reactor, the logic is decoupled into a pipeline. Static analysis must "connect the dots" across operators.

Strategy:

  1. When analyzing a method, identify if it returns a reactive type (Mono, Flux).
  2. If so, inspect all lambdas within that method.
  3. If a lambda calls sendEvent, treat the method as a "Trigger Point".
  4. If the method is also an "Entry Point" (via annotations or RouterFunction), we have a direct reactive flow.

Supporting Inheritance in Listeners

Often, base classes or interfaces define generic listeners:

public interface BaseConsumer<T> {
    @RabbitListener(queues = "${app.queue}")
    void handle(T payload);
}

The CodebaseContext must resolve the ${app.queue} if possible (via application.properties scanning) or at least note that it's a parameterized listener.

Advanced Call Graph: The "Data Flow"

For a truly detailed analysis, we should track NOT just method calls, but also how the "Event" object is passed around.

  • Is the event hardcoded? sm.sendEvent(Events.CREATE)
  • Is it derived from a payload field? sm.sendEvent(payload.getEvent())
  • Is it mapped via a switch or Map?

Static Mapping Inference: If we see a switch statement on a payload field that leads to different sendEvent calls, we can report multiple possible flows from the same endpoint.