Intégration d'Easy Rules dans une application Spring Boot

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

Dans le développement d’applications modernes, la gestion des règles métier est une tâche cruciale, surtout dans des domaines comme la finance où des conditions complexes doivent être validées dynamiquement.
Les moteurs de règles permettent de découpler la logique métier du code principal, rendant les applications plus flexibles et maintenables. Cet article explore l’intégration d’Easy Rules, un moteur de règles léger, dans une application Spring Boot.

Présentation d’Easy Rules

Easy Rules est une bibliothèque Java open-source conçue pour simplifier la définition et l’exécution de règles métier.
Contrairement à des moteurs de règles plus complexes, Easy Rules adopte une approche minimaliste, avec une API intuitive basée sur des annotations comme @Rule@Condition, et @Action.
Une règle dans Easy Rules est une classe Javaqui encapsule une condition (évaluée sur des faits) et une action (exécutée si la condition est remplie).

⚖️ Avantages et inconvénients

➕ Avantages

➖ Inconvénients

Cas pratique

Pour illustrer l’intégration d’Easy Rules, examinons une application bancaire gérant des comptes et des transactions (dépôts et retraits). L’objectif est de valider les transactions, notamment en s’assurant qu’un retrait ne dépasse pas le solde du compte. Nous détaillerons les spécificités du code liées à Easy Rules.

Installation

Pour intégrer Easy Rules à votre application, il faut ajouter la dépendance suivante dans votre fichier pom.xml

<dependency>
    <groupId>org.jeasy</groupId>
    <artifactId>easy-rules-core</artifactId>
    <version>4.1.0</version>
</dependency>

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 transcation.

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

Explication :

Définition de la règle Easy Rules

@Rule(name = "insufficient balance rule", description = "Check if account has sufficient balance")
public class InsufficientBalanceRule {
    
    @Condition
    public boolean when(@Fact("account") Account account, @Fact("transaction") Transaction transaction) {
        return transaction.getType() == TransactionType.WITHDRAWAL
                && (transaction.getAmount() == null
                || transaction.getAmount().compareTo(BigDecimal.ZERO) <= 0
                || account.getBalance() == null
                || account.getBalance().compareTo(transaction.getAmount()) < 0);
    }
    
    @Action
    public void then() throws Exception {
        throw new TransactionException("Insufficient balance");
    }
}

Explications :

Configuration du moteur Easy Rules

La classe Engine configure le moteur et gère l’exécution des règles :

@Component
public class Engine {
    private final Rules rules;
    private final DefaultRulesEngine rulesEngine;
    private final TransactionRulesListener rulesListener;
    
    public Engine() {
        rules = new Rules();
        rules.register(new InsufficientBalanceRule());
        rulesEngine = new DefaultRulesEngine();
        rulesListener = new TransactionRulesListener();
        rulesEngine.registerRuleListener(rulesListener);
    }
    
    public void executeRules(Account account, Transaction transaction) {
        Facts facts = new Facts();
        facts.put("account", account);
        facts.put("transaction", transaction);
        rulesEngine.fire(rules, facts);
        rulesListener.throwIfFailed();
    }
}

Explications :

Listener personnalisé pour gérer les erreurs

La classe TransactionRulesListener implémente l’interface RuleListener d’Easy Rules :

public class TransactionRulesListener implements RuleListener {

    private static final Logger logger = LoggerFactory.getLogger(TransactionRulesListener.class);

    private TransactionException transactionException;

    @Override
    public boolean beforeEvaluate(Rule rule, Facts facts) {
        this.transactionException = null;
        return true; // Permet l'évaluation de la règle
    }

    @Override
    public void afterEvaluate(Rule rule, Facts facts, boolean evaluationResult) {
        // Rien à faire après l'évaluation
    }

    @Override
    public void beforeExecute(Rule rule, Facts facts) {
        // Rien à faire avant l'exécution
    }

    @Override
    public void onSuccess(Rule rule, Facts facts) {
    }

    @Override
    public void onFailure(Rule rule, Facts facts, Exception exception) {
        // Capturer TransactionException si elle est la cause
        Throwable cause = exception;
        while (cause != null) {
            logger.error("Error executing rule: {}", rule.getName());
            if (cause instanceof TransactionException) {
                this.transactionException = (TransactionException) cause;
                return;
            }
            cause = cause.getCause();
        }
        // Stocker une exception générique si ce n'est pas une TransactionException
        this.transactionException = new TransactionException("Error executing rule: " + rule.getName());
        logger.error("Error executing rule: {}", rule.getName(), exception);
    }

    /**
     * Lance l'exception capturée, s'il y en a une.
     */
    public void throwIfFailed() throws TransactionException {
        if (transactionException != null) {
            throw transactionException;
        }
    }
}

Explications :

Intégration avec Spring AOP

Un aspect Spring applique les règles avant le traitement des transactions :

@Aspect
@Component
public class RulesAspect {
    
    private final Engine rulesEngine;
    private final AccountRepository accountRepository;
    
    public RulesAspect(Engine rulesEngine, AccountRepository accountRepository) {
        this.rulesEngine = rulesEngine;
        this.accountRepository = accountRepository;
    }

    @Before("@annotation(fr.eletutour.annotations.TransactionRule) && args(transaction)")
    public void applyRules(Transaction transaction) {
        Account account = accountRepository.findById(transaction.getAccountNumber())
                .orElseThrow(() -> new IllegalArgumentException("Account not found"));
        rulesEngine.executeRules(account, transaction);
    }
}

Explications :

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 d’Easy Rules dans une application Spring Boot offre une solution pour gérer des règles métier de manière flexible et maintenable.
Sa simplicité et son intégration transparente avec Spring, notamment via AOP, en font un choix idéal pour des projets nécessitant un moteur de règles léger.

Bien qu’il ne soit pas adapté aux systèmes complexes nécessitant des fonctionnalités avancées comme l’inférence ou la gestion externe des règles, Easy Rules excelle dans des cas d’utilisation comme la validation de transactions bancaires.

Comparé à Drools et RuleBook, il se distingue par sa facilité d’utilisation et sa légèreté, ce qui en fait un excellent compromis pour de nombreuses applications.