Search code examples
drools

Using CDI to inject a fact in a drools rule


I want to insert a fact in the Drools rule engine that is a CDI bean. To do that in normal java I would

@Inject
MyBean myBean;

Is it possible to @Inject in the rule engine? Do I need weld as a dependency for that?

For example, I have a rule that that has the following consequence currently (without CDI):

insert(new MyLibraryClass())

The MyLibraryClass has been updated and includes cdi injection now, so the class needs to be injected to function properly. As I want to be flexible in the rules to be able to insert this class as a fact or not and at what point in time, I want to do the insertion and creation of the object in the rule engine.


Solution

  • Just inject it into the class that calls the rules (normal Java), and pass the instance into working memory when you invoke the rules.

    So if you have a class, say, called RuleInvoker which invokes the rules ...

    public class RuleInvoker {
      @Inject
      MyBean myBean;
    
      public void invokeRules() {
    
        KieSession kieSession = ...; 
    
        kieSession.insert( myBean ); // insert into working memory like any other fact
    
        // insert other facts, etc here, then fire rules
    
        kieSession.fireAllRules();
    
        // ... etc
      }
    }
    

    I've done dependency injection like this using Spring. No extra dependencies that you're not already using is necessary.


    To clarify, here is a bit of a toy example.

    Let's say originally I had some class Tax which I was originally creating a new instance of in the rules, and then using in subsequent rules:

    rule "Insert Tax"
    when
      not (Tax())
    then
      insert(new Tax())
    end
    
    rule "Apply Tax to Alcohol"
    when
      $tax: Tax()
      $purchase: Purchase( includesAlcohol == true )
    then
      $purchase.apply(tax, TaxType.ALCOHOL);
    end
    
    // Other rules eg. apply tax to lottery tickets, apply tax to groceries, etc.
    

    Now I have to change my design to support different taxation schemes for two different regions. These taxation schemes will be injected; in this case I will use Spring's @Autowired annotation but @Inject is similar.

    For my contrived example, I would convert Tax to an interface, and then have the different tax schemes simply implement that interface. Since the rules are all written against tax, the rules thus don't need to be changed.

    public interface Tax { 
      String getRegion();
      // other methods previously on Tax class
    }
    public class RegionATax implements Tax { ... }
    public class RegionBTax implements Tax { ... }
    public class RegionCTax implements Tax { ... }
    

    Since I can no longer new up my tax instances, I need to get them into the context by putting them into working memory manually.

    public class RuleInvoker {
      @Autowired
      List<Tax> taxSchemes; // injects all tax schemes (implementors of Tax interface)
    
      public void invokeRules(Purchase purchase) {
    
        KieSession kieSession = ...; 
    
        for (Tax tax : taxSchemes) {
          kieSession.insert( tax ); // insert into working memory like any other fact
        }
    
        // insert other facts, etc here, then fire rules
        kieSesion.insert(purchase);
    
        kieSession.fireAllRules();
    
        // ... etc
      }
    }
    

    And then finally the "Insert Tax" rule would need to be replaced with a rule that would decide which tax scheme is appropriate to use. In this particularly contrived example, since I've not provided myself any utility methods, I'm just going to retract every incompatible Tax that doesn't match the purchase region.

    rule "Filter incompatible tax regions"
    salience 1
    when
      Purchase( $region: region )
      $incompatibleTaxRegion: Tax( region != $region )
    then
      retract( $incompatibleTaxRegion )
    end
    
    // Other rules remain the same since they're referencing the Tax interface
    

    I chose to use salience to ensure the filter rule invoked prior to the regular rules. A better design might involve having a separate agenda group to do this.


    At the end of the day, you can't inject directly into the rule context, because whatever CDI library you're using doesn't control the lifecycle of the working memory / rule context. The solution instead is to pass an object into the rules whose lifecycle is controlled by your CDI library.

    The easiest way to do this is usually to just pass the beans directly to working memory either as facts or globals. Other solutions I've seen are "injector" classes being passed in. I once had to manage a rule set where the Spring application context in its entirety was passed into the rules (a bit of a nightmare to deal with tbh.)

    If you don't want to pass the beans directly, you can create some sort of "injector" class as a bean holder, inject that into your rule invoker class, and then let the rules sort out what they need. I'm not familiar with Weld at all, but if it makes an application context available that you can inject, you could pass that into the rules as well. But all of these solutions will require you to adjust the rules in some way, at minimum to remove or update your original insert( new MyLibraryClass()) rule.