Cloud native Drools mit Spring Boot native
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.
<?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.)
@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.
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.
@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.
@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.
$ 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.
$ 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.