Neuigkeiten von trion.
Immer gut informiert.

Cloud native Drools mit Spring Boot native

Drools Rules Engine

Drools ist eine Rule-Engine, also ein Baustein für Expertensysteme und zur Flexibilisierung von Entscheidungen. Dabei ist Drools flexibel in bestehende JavaEE, Spring Boot und andere Frameworks integrierbar.
Dieser Beitrag beschäftigt sich mit folgender Fragestellung: Wie könnte eine Cloud-native Umsetzung von Drools im Gegensatz zu dem klassischen Modell einer Einbettung aussehen?

Für eine Cloud-native Architektur wäre es sinnvoll, Drools als separaten Microservice bereitzustellen. Als Skalierungsmodell sollte der Drools Microservice horizontal skalierbar sein. Optimalerweise würde ein schnelles Scale-Up und Scale-to-zero für Function-as-a-Service Modelle unterstützt. In dem Artikel Spring native und GraalVM im produktiven Einsatz wurden die Grundlagen zum Einsatz von Spring native und GraalVM aufgezeigt.
Damit wäre eine sehr hohe Startgeschwindigkeit möglich, erforderlicher Zustand für die durch Drools zu treffenden Entscheidungen ist dabei zu externalisieren, beispielsweise in Redis.

Soweit, so gut. Doch GraalVM native Image bringt einige Einschränkungen mit sich, die speziell bei Drools zu Herausforderungen führen: Drools generiert zur Laufzeit Klassen dynamisch, unter anderem um von den Optimierungen der Java Laufzeitumgebung zu profitieren. Das passt gerade nicht zu den closed-world Annahmen von GraalVM native Image.

Drools und GraalVM native Image

Quarkus, ebenso wie Drools aus dem Hause RedHat, ist ein Full-Stack Framework das im Hinblick auf Kubernetes und OpenShift entwickelt wurde. Ziel dabei ist eine Build-Time Optimierung und guter Support von GraalVM native Image, um unterschiedliche Cloud Szenarien optimal zu unterstützen.
Drools wurde im Rahmen des Kogito Projektes so weiter entwickelt, dass Rules als POJOs umgesetzt werden, die zur Build Zeit generiert werden. Damit wird Drools GraalVM native Image kompatibel. Und das beste daran: Das Programmiermodell und die Regelsprache ändern sich nicht.
Mit Drools Version 7.45 und Spring native 0.12 funktioniert das ganze bereits ohne offensichtliche Instabilitäten, Fehler oder Probleme.

Das funktioniert nicht nur mit Quarkus, sondern ebenfalls mit Spring Boot. Damit lassen sich zusammen mit aktuellen Drools Versionen entsprechende Artefakte erstellen, die den Anforderungen an eine Cloud native Architektur genügen. In der Umsetzung ist das trotz des frühen Stadiums von Spring native relativ einfach, wie das folgende Beispiel zeigt.

Beispielprojekt: Drools Spring Boot GraalVM native Image

Das Beispielprojekt kann ganz normal über den Spring Initializer erstellt werden. Als Java Version kommt das aktuelle Java 17 LTS zum Zuge, als Buildsystem das traditionelle Maven. Da Drools als HTTP Service bereitgestellt werden soll, wird die Spring-Web Abhängigkeit benötigt.
Und natürlich Spring native. Aktuell werden dazu noch die Spring Repositories als Quelle benötigt, da die experimentellen Versionen nicht bei Maven Central bereitgestellt werden.

Für Drools kommen schließlich noch die Abhängigkeiten drools-core, drools-compiler und drools-mvel hinzu.

Beispiel für das Maven POM der Drools Spring native Anwendung
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>de.trion.sample</groupId>
    <artifactId>spring-drools</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>spring-drools</name>
    <description>Sample project for Drools and Spring Boot</description>
    <properties>
        <java.version>17</java.version>
        <repackage.classifier/>
        <spring-native.version>0.12.1</spring-native.version>
        <drools.version>7.73.0.Final</drools.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-native</artifactId>
            <version>${spring-native.version}</version>
        </dependency>

        <!-- drools -->
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-engine-classic</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-model-compiler</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-core-dynamic</artifactId>
            <version>${drools.version}</version>
        </dependency>

        <!-- dev -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <classifier>${repackage.classifier}</classifier>
                    <image>
                        <builder>paketobuildpacks/builder:tiny</builder>
                        <env>
                            <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                        </env>
                    </image>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.experimental</groupId>
                <artifactId>spring-aot-maven-plugin</artifactId>
                <version>${spring-native.version}</version>
                <executions>
                    <execution>
                        <id>test-generate</id>
                        <goals>
                            <goal>test-generate</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>generate</id>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/release</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/release</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

    <profiles>
        <profile>
            <id>native</id>
            <properties>
                <repackage.classifier>exec</repackage.classifier>
                <native-buildtools.version>0.9.13</native-buildtools.version>
            </properties>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-launcher</artifactId>
                    <scope>test</scope>
                </dependency>
            </dependencies>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.graalvm.buildtools</groupId>
                        <artifactId>native-maven-plugin</artifactId>
                        <version>${native-buildtools.version}</version>
                        <extensions>true</extensions>
                        <executions>
                            <execution>
                                <id>test-native</id>
                                <phase>test</phase>
                                <goals>
                                    <goal>test</goal>
                                </goals>
                            </execution>
                            <execution>
                                <id>build-native</id>
                                <phase>package</phase>
                                <goals>
                                    <goal>build</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

Die Spring Boot Anwendung selbst erfordert keine besondere Konfiguration. Um in Drools die Kie Session (Knowledge-is-everything) zu instantiieren werden die Regeldateien im Beispiel aus dem Classpath geladen. Die dazu verwendete Drools Konfiguration ist im folgenden Beispiel zu sehen.
(Die Alternative wäre die Verwendung eines KieClasspathContainers mit entsprechender kmodule.xml, das Beispiel soll jedoch einfach nachvollziehbar bleiben.)

Drools Konfiguration in Spring
@Configuration
public class DroolsConfiguration
{
    private final KieServices kieServices;

    public DroolsConfiguration()
    {
        kieServices = KieServices.Factory.get();
    }

    @Bean
    public KieContainer getKieContainer()
    {
        final KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
        kieFileSystem.write(ResourceFactory.newClassPathResource("drools/risk.drl"));

        final KieBuilder kb = kieServices.newKieBuilder(kieFileSystem);
        kb.buildAll();

        final KieModule kieModule = kb.getKieModule();
        return kieServices.newKieContainer(kieModule.getReleaseId());
    }
}

Als Regelbeispiele wird ein fiktives Kraftfahrtrisiko auf sehr simple Weise modelliert. Die Drools DRL Regeln sind im folgenden zu sehen. Dabei wird auch von erweiterten Drools Features wie salience und agenda-group Gebrauch gemacht. In dem Regelnamen wird stets dokumentiert, was die Regel bewirkt. Dies hat sich als ein Baustein für eine langfristige Wartbarkeit von Drools Regelsystemen in der Praxis bewährt.

Beispielregeln im Drools DRL Format
import de.trion.sample.springdrools.risk.RiskRequest
import de.trion.sample.springdrools.risk.RiskResponse
import de.trion.sample.springdrools.risk.RiskClass


rule "Execute explicit rule groups"
salience 1
when
then
  drools.setFocus("car");
  drools.setFocus("family");
  drools.setFocus("age");
  drools.setFocus("base");
end;


rule "Base risk is MEDIUM"
agenda-group "base"
    when
     $response : RiskResponse(riskClass == null);
    then
    modify($response){
        setRiskClass(RiskClass.MEDIUM)
    }
end;


rule "Expensive car has higher risk"
agenda-group "car"
    when
     $request : RiskRequest(principalAmount >= 100000)
     $response : RiskResponse(riskClass == RiskClass.LOW);
    then
    modify($response){
        setRiskClass(RiskClass.HIGH)
    }
end;

rule "Expensive car has high risk"
agenda-group "car"
    when
     $request : RiskRequest(principalAmount >= 100000)
     $response : RiskResponse(riskClass == RiskClass.MEDIUM);
    then
    modify($response){
        setRiskClass(RiskClass.HIGH)
    }
end;

rule "Young driver has higher risk"
agenda-group "age"
    when
     $request : RiskRequest(applicant.age < 20)
     $response : RiskResponse(riskClass == RiskClass.MEDIUM);
    then
    modify($response){
        setRiskClass(RiskClass.HIGH)
    }
end;

rule "Driver with many kids is low risk"
agenda-group "family"
    when
     $request : RiskRequest(applicant.numberOfKids > 1)
     $response : RiskResponse(riskClass == RiskClass.HIGH);
    then
    modify($response){
        setRiskClass(RiskClass.LOW)
    }
end;

rule "Married driver lowers risk"
agenda-group "family"
    when
     $request : RiskRequest(applicant.married == true)
     $response : RiskResponse(riskClass == RiskClass.MEDIUM);
    then
    modify($response){
        setRiskClass(RiskClass.LOW)
    }
end;

Damit die Regelengine per HTTP angesprochen werden kann, wird noch ein Spring Controller und, zur Trennung der Verantwortlichkeiten, ein zugehöriger Service benötigt.

Spring Controller zur Bereitstellung des Drools Microservice
@RestController
@RequestMapping("/api")
public class RiskController
{
    private final RiskService riskService;

    public RiskController(RiskService riskService)
    {
        this.riskService = riskService;
    }

    @PostMapping
    RiskResponse calc(@RequestBody RiskRequest request)
    {
        return riskService.calculateRisk(request);
    }
}

Die Controller-Schicht ist relativ dünn. Im Service findet dann der tatsächliche Drools Aufruf statt.

RiskService zum Aufruf von Drools
@Service
public class RiskService
{
    private final KieContainer kieContainer;

    public RiskService(KieContainer kieContainer)
    {
        this.kieContainer = kieContainer;
    }

    public RiskResponse calculateRisk(RiskRequest riskRequest)
    {
        final KieSession kieSession = kieContainer.newKieSession();
        kieSession.insert(riskRequest);

        var res = new RiskResponse();
        var handle = kieSession.insert(res);

        kieSession.fireAllRules();
        kieSession.dispose();

        res.setApplicant(riskRequest.getApplicant());

        return res;
    }
}

Der Drools Service lässt sich ganz klassich als normale Spring Boot Java Anwendung starten. Zum Testen kann als HTTP Client z.B. httpie verwendet werden.

Start der Drools Spring Boot App
$ mvn spring-boot:run
...
Started SpringDroolsApplication in 1.854 seconds (JVM running for 2.072)

Ein Beispielaufruf sieht dann z.B. so aus:

$ http :8080/api/ type=CAR principalAmount=50000 \
  applicant:='{"age": "52", "married": false, "numberOfKids": 0}'
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/json
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
    "applicant": {
        "age": 52,
        "married": false,
        "name": null,
        "numberOfKids": 0
    },
    "calculationTime": null,
    "riskClass": "MEDIUM"
}

Um daraus ein native Image zu bauen, wird Docker und ein GraalVM native Image kompatibles Buildpack verwendet. Mit GraalVM native Image dauert der Build deutlich länger, da die Ahead-of-time (AOT) Compilation entsprechend aufwendig ist.

Build der Drools Anwendung als GraalVM native image mit Maven
$ mvn spring-boot:build-image
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:22 min

Im Ergebnis entsteht ein neues Docker Image mit einem Umfang von ca. 100 MB. Das Image lässt sich als Container starten und analog aufrufen.

$  docker run --rm -p 8080:8080 spring-drools:1.0.0-SNAPSHOT

Der schnelle Start ermöglicht in vielen Fällen sogar einen On-Demand Start des Dienstes.

Fazit

Mit Drools und den neuen Möglichkeiten von GraalVM und dem native Image lassen sich performante Cloud Native Applikationen entwickeln. Dabei müssen sowohl Frameworkentwickler, als auch Architekten und Anwendungsentwickler in neue Konzepte und Implementierungen investieren. Im Ergebnis können durch die Cloud native Unterstützung leichter zu entwickelnde, zu wartende und zu skalierende Systeme konzipiert und umgesetzt werden.




Zu den Themen Kubernetes, Docker und Drools bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.