lundi 10 novembre 2025

Gérer le cycle de vie d’un plugin avec Spring Statemachine

Les machines à états sont un sujet très peu discuté, peut-être que le côté mathématique fait peur à certain. Pour faire suite à notre article sur les plugin, nous allons mettre en place une machine à état pour suivre son cycle de vie. 

Le projet State Machine de Spring sera utilisé pour la suite de cet article.

Un plugin peut avoir différent état 

  • INSTALLED, 
  • ACTIVE, 
  • INACTIVE, 
  • UPDATING, 
  • FAILED,
  • UNINSTALLED

Dans un système de gestion de plugin, différente action devrait être mis en place :

  • Téléchargement du JAR
  • Vérification des dépendances
  • Initialisation du contexte Spring
  • Enregistrement des routes ou services
  • Nettoyage des ressources

Chaque transition doit être explicitement autorisée. 

  • On ne peut pas désactiver un plugin qui n’est pas activé.
  • On ne peut pas réactiver un plugin désinstallé.
  • Une erreur pendant l’activation doit mener à l’état FAILED.

Gérer cela avec des if/else mène à un code difficile à maintenir. Une machine à états permet de modéliser ce cycle clairement.

État et évènement

Liste des états


public enum PluginState {
    INSTALLED,
    ACTIVE,
    INACTIVE,
    UPDATING,
    FAILED,
    UNINSTALLED
}

Listes des évènements


public enum PluginEvent {
    INSTALL,
    ACTIVATE,
    DEACTIVATE,
    UPDATE,
    UNINSTALL,
    ERROR
}

Le plugin

Cette classe a été allégé

public class Plugin {

    private String id;
    private PluginState state;

    public Plugin(String id) {
        this.id = id;
        this.state = PluginState.INSTALLED; // Après installation
    }

    public String getId() { return id; }
    public PluginState getState() { return state; }
    public void setState(PluginState state) { this.state = state; }
}
Dans la réalité, il y aurait fort probablement son nom, url, version.

Action du plugin

Activation du plugin

@Component
public class ActivatePluginAction implements Action<PluginState, PluginEvent> {
    private static final Logger log = LoggerFactory.getLogger(ActivatePluginAction.class);
    @Override
    public void execute(StateContext<PluginState, PluginEvent> context) {
        Plugin plugin = (Plugin) context.getMessage().getHeaders().get("plugin");
        log.info("Activation du plugin : " + plugin.getId());
    }
}

Désactivation du plugin


@Component
public class DeactivatePluginAction implements Action<PluginState, PluginEvent> {
    private static final Logger log = LoggerFactory.getLogger(DeactivatePluginAction.class);
    @Override
    public void execute(StateContext<PluginState, PluginEvent> context) {
        Plugin plugin = (Plugin) context.getMessage().getHeaders().get("plugin");
        log.info("Plugin désactivé : " + plugin.getId());
    }
}

Erreur du plugin


@Component
public class FailPluginAction implements Action<PluginState, PluginEvent> {
    private static final Logger log = LoggerFactory.getLogger(FailPluginAction.class);
    @Override
    public void execute(StateContext<PluginState, PluginEvent> context) {
        Plugin plugin = (Plugin) context.getMessage().getHeaders().get("plugin");
        log.warn("Plugin en échec : " + plugin.getId());
        plugin.setState(PluginState.FAILED);
    }
}

Configuration de la machine à états

La classe PluginStateMachineConfig est le coeur de la machine à états.
Elle configure
  • Les états possibles du plugin
  • Les transitions autorisées entre ces états
  • Les événements qui déclenchent ces transitions
  • Les actions à exécuter lors de certaines transitions

@Configuration
@EnableStateMachineFactory
public class PluginStateMachineConfig extends StateMachineConfigurerAdapter<PluginState, PluginEvent> {

    private final ActivatePluginAction activateAction;
    private final DeactivatePluginAction deactivateAction;
    private final FailPluginAction failAction;

    public PluginStateMachineConfig(ActivatePluginAction activateAction,
                                    DeactivatePluginAction deactivateAction,
                                    FailPluginAction failAction) {
        this.activateAction=activateAction;
        this.deactivateAction=deactivateAction;
        this.failAction=failAction;
    }

    @Override
    public void configure(StateMachineStateConfigurer<PluginState, PluginEvent> states) throws Exception {
        states
                .withStates()
                .initial(PluginState.INSTALLED)
                .states(EnumSet.allOf(PluginState.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<PluginState, PluginEvent> transitions) throws Exception {
        transitions
                .withExternal()
                .source(PluginState.INSTALLED)
                .target(PluginState.ACTIVE)
                .event(PluginEvent.ACTIVATE)
                .action(activateAction)
                .and()
                .withExternal()
                .source(PluginState.ACTIVE)
                .target(PluginState.INACTIVE)
                .event(PluginEvent.DEACTIVATE)
                .action(deactivateAction)
                .and()
                .withExternal()
                .source(PluginState.INACTIVE)
                .target(PluginState.ACTIVE)
                .event(PluginEvent.ACTIVATE)
                .action(activateAction)
                .and()
                .withExternal()
                .source(PluginState.ACTIVE)
                .target(PluginState.UPDATING)
                .event(PluginEvent.UPDATE)
                .and()
                .withExternal()
                .source(PluginState.UPDATING)
                .target(PluginState.ACTIVE)
                .event(PluginEvent.ACTIVATE)
                .action(activateAction)
                .and()
                .withExternal()
                .source(PluginState.INSTALLED)
                .target(PluginState.UNINSTALLED)
                .event(PluginEvent.UNINSTALL)
                .and()
                .withExternal()
                .source(PluginState.ACTIVE)
                .target(PluginState.FAILED)
                .event(PluginEvent.ERROR)
                .action(failAction)
                .and()
                .withExternal()
                .source(PluginState.INACTIVE)
                .target(PluginState.FAILED)
                .event(PluginEvent.ERROR)
                .action(failAction)
                .and()
                .withExternal()
                .source(PluginState.FAILED)
                .target(PluginState.UNINSTALLED)
                .event(PluginEvent.UNINSTALL);
    }
}

Diagramme du workflow

Ce diagramme affiche les différents états et les transitions possible.



Cycle de vie du plugin

Cette classe crée, stocke et orchestre les transitions d'états des plugins via Spring StateMachine. Une map sert de stockage temporaire des plugins.

Lors de l'activation d'un plugin, on 

  • Récupère le plugin via son id.
  • Crée une instance de machine à états via la factory.
  • Construit un message contenant l’événement ACTIVATE, avec le plugin comme contexte.
  • Envoie l’événement à la machine.
  • Si la transition échoue, une exception est levée (ex. : INSTALLED → DEACTIVATE n’est pas autorisé).
  • Met à jour l’état du plugin avec le nouvel état de la machine.
  • La machine à états garantit que seul ce qui est autorisé se produit.

@Service
public class PluginService {

    private final Map<String, Plugin> plugins = new ConcurrentHashMap<>();

    @Autowired
    private StateMachineFactory<PluginState, PluginEvent> stateMachineFactory;

    public Plugin createPlugin(String id) {
        Plugin plugin = new Plugin(id);
        plugins.put(id, plugin);
        return plugin;
    }

    public Plugin getPlugin(String id) {
        return plugins.get(id);
    }

    public void activate(String id) {
        Plugin plugin = getPlugin(id);
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin non trouvé : " + id);
        }

        StateMachine<PluginState, PluginEvent> stateMachine = stateMachineFactory.getStateMachine();

        Message<PluginEvent> message = MessageBuilder
                .withPayload(PluginEvent.ACTIVATE)
                .setHeader("plugin", plugin)
                .build();

        stateMachine.sendEvent(message);
        plugin.setState(stateMachine.getState().getId());
    }

    public void deactivate(String id) {
        Plugin plugin = getPlugin(id);
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin non trouvé : " + id);
        }

        StateMachine<PluginState, PluginEvent> stateMachine = stateMachineFactory.getStateMachine();

        Message<PluginEvent> message = MessageBuilder
                .withPayload(PluginEvent.DEACTIVATE)
                .setHeader("plugin", plugin)
                .build();

        stateMachine.sendEvent(message);
        plugin.setState(stateMachine.getState().getId());
    }

    public void uninstall(String id) {
        Plugin plugin = getPlugin(id);
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin non trouvé : " + id);
        }

        StateMachine<PluginState, PluginEvent> stateMachine = stateMachineFactory.getStateMachine();

        Message<PluginEvent> message = MessageBuilder
                .withPayload(PluginEvent.UNINSTALL)
                .setHeader("plugin", plugin)
                .build();

        stateMachine.sendEvent(message);
        plugin.setState(stateMachine.getState().getId());
        plugins.remove(id); // Supprimer du stockage
    }

}

La machine à états valide les transitions. Si un plugin est dans l’état INSTALLED, une tentative de DEACTIVATE échouera.

Il est facile d'ajouter des états / transitions

Test

Un controlleur a été mis en place pour pouvoir tester plus aisément les états.


@RestController
@RequestMapping("/plugins")
public class PluginController {

    private final PluginService pluginService;

    public PluginController(PluginService pluginService) {
        this.pluginService=pluginService;
    }

    @PostMapping("/create/{id}")
    public String create(@PathVariable String id) {
        Plugin plugin = pluginService.createPlugin(id);
        return "Plugin créé : " + id + ", état initial : " + plugin.getState();
    }

    @PostMapping("/{id}/activate")
    public String activate(@PathVariable String id) {
        pluginService.activate(id);
        Plugin plugin = pluginService.getPlugin(id);
        return "Plugin activé : " + id + ", état : " + plugin.getState();
    }

    @PostMapping("/{id}/deactivate")
    public String deactivate(@PathVariable String id) {
        pluginService.deactivate(id);
        Plugin plugin = pluginService.getPlugin(id);
        return "Plugin désactivé : " + id + ", état : " + plugin.getState();
    }

    @PostMapping("/{id}/uninstall")
    public String uninstall(@PathVariable String id) {
        pluginService.uninstall(id);
        return "Plugin désinstallé : " + id;
    }

    @GetMapping("/{id}")
    public String getStatus(@PathVariable String id) {
        Plugin plugin = pluginService.getPlugin(id);
        if (plugin == null) {
            return "Plugin non trouvé";
        }
        return "Plugin " + id + ", état : " + plugin.getState();
    }
}

Par la suite, vous pouvez utiliser ce controller pour tester différents scénarios

Créer un plugin

curl -X POST http://localhost:8080/plugins/create/demo

Tenter de le désactiver (état INSTALLED)

curl -X POST http://localhost:8080/plugins/demo/deactivate

Activer le plugin

curl -X POST http://localhost:8080/plugins/demo/activate

Désactiver 

curl -X POST http://localhost:8080/plugins/demo/deactivate

Tenter de réactiver un plugin désinstallé

curl -X POST http://localhost:8080/plugins/demo/uninstall
curl -X POST http://localhost:8080/plugins/demo/activate

Conclusion

PluginStateMachineConfig est le plan de votre machine à états.
Il définit les règles du jeu : ce qui est autorisé, ce qui n’est pas, et ce qui se passe à chaque étape.
C’est une alternative claire et robuste aux if/else imbriqués, surtout dans des systèmes modulaires comme des plugins ou des workflows métier. 

Les sources sont disponible sur github

Aucun commentaire:

Enregistrer un commentaire

Gérer le cycle de vie d’un plugin avec Spring Statemachine

Les machines à états sont un sujet très peu discuté, peut-être que le côté mathématique fait peur à certain. Pour faire suite à notre articl...