Révolutionner la gestion des règles métier - intégrer RuleBook dans Spring Boot

Publié le 06/06/2025 Source : sfeir.dev

Dans le développement d’applications modernes, la gestion des règles métier est une problématique clé pour garantir la robustesse et la flexibilité des systèmes.
Les règles métier, telles que la validation des transactions bancaires ou les restrictions sur la création de comptes, doivent être bien structurées, maintenables et évolutives.
RuleBook est une bibliothèque Java légère et flexible qui permet de définir et d’exécuter des règles métier de manière déclarative. Lorsqu’elle est intégrée à Il était une fois… Spring Boot, RuleBook offre une solution puissante pour externaliser la logique de validation tout en s’appuyant sur l’écosystème Spring, notamment Spring AOP pour une application modulaire des règles.

Qu’est ce que RuleBook ?

RuleBook est une bibliothèque open-source Java conçue pour implémenter des règles métier de manière simple et intuitive.
Contrairement à des moteurs de règles complexes comme Drools, RuleBook adopte une approche légère basée sur des annotations et une API fluide. Les règles sont définies dans des classes Java annotées avec des métadonnées telles que @Rule@Given@When@Then, et @Result, ce qui les rend lisibles et faciles à maintenir.

Dans une application Spring Boot, RuleBook peut être intégré via le module rulebook-spring, qui fournit des beans comme SpringAwareRuleBookRunner pour charger automatiquement les règles d’un package donné. Les règles peuvent être appliquées à des méthodes spécifiques grâce à Spring AOP, ce qui permet de séparer la logique de validation de la logique métier principale.

⚖️ Quels sont les avantages et inconvénients de RuleBook ?

➕ Avantages

➖ Inconvénients

Comparaison entre RuleBook et Drools

Drools est un moteur de règles open-source plus mature et puissant, largement utilisé dans les systèmes de gestion de règles métier (BRMS). Voici une comparaison entre RuleBook et Drools :

Critère RuleBook Drools
Complexité Simple, basé sur des annotations Java Plus complexe, avec un langage dédié (DRL) et des outils comme Workbench
Performance Convient aux applications légères, moins optimisé pour les règles complexes Optimisé pour les règles complexes et les grandes bases de connaissances
Intégration Spring Intégration native via rulebook-spring et Spring AOP Intégration possible, mais nécessite plus de configuration
Fonctionnalités Validation simple, règles statiques Supporte l’inférence, les événements, et les règles dynamiques
Courbe d’apprentissage Facile à apprendre pour les développeurs Java Courbe d’apprentissage plus longue, surtout pour le langage DRL
Communauté Petite communauté, moins de ressources Grande communauté, nombreux tutoriels et support commercial (Red Hat)

Quand choisir RuleBook ?

Quand choisir Drools ?

Cas pratique

Pour illustrer l’intégration de RuleBook, considérons une application Spring Boot de gestion de compte bancaire. L’application permet de créer des comptes et d’effectuer des transactions (dépôts et retraits), avec des règles de validation définies via RuleBook et appliquées via Spring AOP.

Installation

Pour intégrer RuleBook à votre application, il faut ajouter les dépendances suivantes dans votre fichier pom.xml

<dependencies>
    <dependency>
        <groupId>com.deliveredtechnologies</groupId>
        <artifactId>rulebook-core</artifactId>
        <version>${rulebook.version}</version>
    </dependency>
    <dependency>
        <groupId>com.deliveredtechnologies</groupId>
        <artifactId>rulebook-spring</artifactId>
        <version>${rulebook.version}</version>
    </dependency>
</dependencies>

Modèles métier

Les classes Account et Transaction représentent nos objets métier

@Entity
public class Account {
    @Id
    private String accountNumber;
    private String ownerName;
    private BigDecimal balance;

    //constructeur + getter et setter
}

@Entity
public class Transaction {
    @Id
    @GeneratedValue
    private Long id;
    private String accountNumber;
    @Enumerated(EnumType.STRING)
    private TransactionType type;
    private BigDecimal amount;
    private LocalDateTime timestamp = LocalDateTime.now();

    //constructeur + getter et setter
}

L’énumération TransactionType définit les types de transactions :

public enum TransactionType {
    DEPOSIT, WITHDRAWAL
}

Annotation personnalisée

Une annotation@TransactionRule marque les méthodes où les règles RuleBook doivent s’appliquer pour valider les transactions.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TransactionRule {
}

Explication :

Configuration RuleBook

La configuration de RuleBook dans Spring Boot repose sur la classe RuleBookConfiguration, qui définit des beans SpringAwareRuleBookRunner pour charger les règles à partir de packages spécifiques. Chaque RuleBookRunner est configuré pour cibler un sous-package contenant les règles pertinentes, garantissant une séparation claire des contextes (création de compte vs. transactions).

@Configuration
public class RuleBookConfiguration {

    @Bean
    public SpringAwareRuleBookRunner accountCreationRuleBook() {
        return new SpringAwareRuleBookRunner("fr.eletutour.rules.account.creation");
    }

    @Bean
    public SpringAwareRuleBookRunner transactionRuleBook() {
        return new SpringAwareRuleBookRunner("fr.eletutour.rules.account.operation");
    }
}

Explications :

Cette configuration garantit que chaque opération utilise uniquement les règles pertinentes, améliorant la performance et la clarté. Les développeurs peuvent facilement ajouter de nouvelles règles en les plaçant dans le bon sous-package, sans modifier la configuration.

Définir les règles RuleBook

Les règles RuleBook sont définies dans des classes Java annotées avec @RuleBean et @Rule. Chaque règle utilise les annotations @Given pour recevoir des faits (données d’entrée), @When pour définir une condition de validation, et @Then pour spécifier l’action à exécuter si la condition est remplie.

Règle de création de compte

@RuleBean
@Rule(order = 1)
public class AccountCreationRule {

    private final Logger logger = org.slf4j.LoggerFactory.getLogger(AccountCreationRule.class);

    @Given("account")
    private Account account;

    @Result
    private String result;

    @When
    public boolean when() {
        logger.info("Validating account creation for account: {}", account);
        return account.getAccountNumber() == null || account.getAccountNumber().isEmpty()
                || account.getBalance() == null || account.getBalance().compareTo(BigDecimal.ZERO) < 0;
    }

    @Then
    public void then() {
        logger.error("Account creation failed: Invalid account number or negative initial balance");
        result = "Account creation failed: Invalid account number or negative initial balance";
    }
}

Explications :

Règles de validation de transaction

@RuleBean
@Rule(order = 1)
public class DepositRule {

    private final Logger logger = org.slf4j.LoggerFactory.getLogger(DepositRule.class);

    @Given("transaction")
    private Transaction transaction;

    @Given("errors")
    private List<String> errors;

    @When
    public boolean when() {
        logger.info("Validating deposit transaction: {}", transaction);
        return transaction.getType() == TransactionType.DEPOSIT
                && (transaction.getAmount() == null || transaction.getAmount().compareTo(BigDecimal.ZERO) <= 0);
    }

    @Then
    public void then() {
        logger.error("Deposit failed: Amount must be positive");
        errors.add("Deposit failed: Amount must be positive");
    }
}

@RuleBean
@Rule(order = 2)
public class WithdrawalRule {

    private final Logger logger = org.slf4j.LoggerFactory.getLogger(WithdrawalRule.class);

    @Given("transaction")
    private Transaction transaction;

    @Given("balance")
    private BigDecimal balance;

    @Given("errors")
    private List<String> errors;

    @When
    public boolean when() {
        logger.info("Validating withdrawal transaction: {}", transaction);
        return transaction.getType() == TransactionType.WITHDRAWAL
                && (transaction.getAmount() == null
                || transaction.getAmount().compareTo(BigDecimal.ZERO) <= 0
                || balance == null
                || balance.compareTo(transaction.getAmount()) < 0);
    }

    @Then
    public void then() {
        logger.error("Withdrawal failed: Insufficient balance or invalid amount");
        errors.add("Withdrawal failed: Insufficient balance or invalid amount");
    }
}

Explications :

Les aspects

Les aspects Spring AOP interceptent les appels aux méthodes pour appliquer les règles RuleBook.

Aspect de création de compte

@Aspect
@Component
public class AccountCreationAspect {

    @Autowired
    private RuleBookRunner accountCreationRuleBook;

    @Before("execution(* fr.eletutour.service.AccountService.createAccount(..)) && args(account)")
    public void validateAccountCreation(Account account) {
        NameValueReferableMap<Object> facts = new FactMap<>();
        facts.setValue("account", account);

        accountCreationRuleBook.run(facts);

        if (accountCreationRuleBook.getResult().isPresent()) {
            throw new IllegalArgumentException(accountCreationRuleBook.getResult().get().toString());
        }
    }
}

Aspect de gestion de transaction

@Aspect
@Component
public class TransactionAspect {

    @Autowired
    private RuleBookRunner transactionRuleBook;

    @Autowired
    private AccountRepository accountRepository;

    @Before("@annotation(fr.eletutour.annotations.TransactionRule) && args(transaction)")
    public void validateTransaction(Transaction transaction) {
        Account account = accountRepository.findById(transaction.getAccountNumber())
                .orElseThrow(() -> new IllegalArgumentException("Account not found"));
        List<String> errors = new ArrayList<>();
        NameValueReferableMap<Object> facts = new FactMap<>();
        facts.setValue("transaction", transaction);
        facts.setValue("balance", account.getBalance());
        facts.setValue("errors", errors);

        transactionRuleBook.run(facts);

        if (!errors.isEmpty()) {
            throw new TransactionException(errors);
        }
    }
}

Service et controller

Notre logique de service reste simple car nous avons “décentralisé” la gestion des règles métier dans nos aspects et notre règle RuleBook.

@Service
public class AccountService {

    private final AccountRepository accountRepository;
    private final TransactionRepository transactionRepository;

    public AccountService(AccountRepository accountRepository, TransactionRepository transactionRepository) {
        this.accountRepository = accountRepository;
        this.transactionRepository = transactionRepository;
    }

    public Account createAccount(Account account) {
        return accountRepository.save(account);
    }

    @TransactionRule
    public Transaction processTransaction(Transaction transaction) {
        Account account = accountRepository.findById(transaction.getAccountNumber())
                .orElseThrow(() -> new IllegalArgumentException("Account not found"));

        if (transaction.getType() == TransactionType.DEPOSIT) {
            account.setBalance(account.getBalance().add(transaction.getAmount()));
        } else if (transaction.getType() == TransactionType.WITHDRAWAL) {
            account.setBalance(account.getBalance().subtract(transaction.getAmount()));
        }

        accountRepository.save(account);
        return transactionRepository.save(transaction);
    }
}

Le controller expose nos endpoints à notre API REST

@RestController
@RequestMapping("/api/accounts")
public class AccountController {

    private final AccountService accountService;

    public AccountController(AccountService accountService) {
        this.accountService = accountService;
    }

    @PostMapping
    public Account createAccount(@RequestBody Account account) {
        return accountService.createAccount(account);
    }

    @PostMapping("/transaction")
    public Transaction processTransaction(@RequestBody Transaction transaction) {
        return accountService.processTransaction(transaction);
    }
}

Conclusion

L’intégration de RuleBook dans Spring Boot, comme illustré dans cet exemple, offre une approche simple pour gérer les règles métier. La configuration via SpringAwareRuleBookRunner permet de charger les règles de manière modulaire, tandis que les annotations RuleBook rendent les règles lisibles et maintenables. L’utilisation de Spring AOP et d’une exception personnalisée comme TransactionException garantit une séparation claire entre la validation et la logique métier, avec une gestion robuste des erreurs via une réponse JSON structurée.

RuleBook est idéal pour des projets nécessitant des validations simples et une intégration fluide avec Spring Boot. Pour des besoins plus complexes, comme l’inférence ou les règles dynamiques, Drools reste une alternative. En fonction de vos besoins, RuleBook peut transformer la gestion des règles métier en une tâche claire et efficace.