DroolsのKieScannerを使って、アプリケーション起動中に実行するルールを更新する方法を検証します。
はじめに
前回の記事では、ルールを定義するDRLファイルをクラスパスに配置し、アプリケーション起動時にKieContainerを生成していました。
しかしDroolsでは、KJARとよばれるJarファイルにルールをまとめ、アプリケーションからロードして実行する方法もサポートされています。
さらにKieScannerという仕組みを使うと、アプリケーション起動中にKJARの更新を検知して、ルールを最新のものに切り替えることが可能です。
本記事では、この流れをサンプルコードで検証していきます。
KJARとKieScannerについて
簡単ですが用語の補足をしていきます。
- KJAR
ルールを定義するDRLファイルや、KieContainerの生成に必要な資材(kmodule.xmlなど)を含むJarファイルです。
Mavenリポジトリに配置して、Droolsの仕組みでロードすることでKieContainerの生成やルール実行ができます。
- KieScanner
Mavenリポジトリに保存されたKJARを定期的にスキャンして、更新があればKieContainerに反映することができます。
サンプルアプリ
検証用に作成したプログラムについて紹介していきます。
ルールの題材としては前回同様、顧客の年齢に応じて提供するドリンクを決定するルールとします。
KJARを作成するためのプロジェクトと、KieScannerでKJARを参照するプロジェクトとの2つあります。
コード全量はGithubにあげています。
drink_rule_kjar
KJARをビルドするためのプロジェクトです。
ルールが扱うデータクラスとDRLファイル、kmodule.xmlを含んでいます。
package org.example;
public class Drink {
private String name;
private int charge;
...
package org.example;
public class Person {
private String name;
private int age;
...
import org.example.Person
import org.example.Drink
rule "Child"
when
$person : Person( age < 20 )
$drink : Drink()
then
$drink.setName( "Orange Juice" );
$drink.setCharge( 100 );
end
rule "Adult"
when
$person : Person( age >= 20 )
$drink : Drink()
then
$drink.setName( "Beer" );
$drink.setCharge( 200 );
end
<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns="http://jboss.org/kie/6.0.0/kmodule">
</kmodule>
pom.xmlも一部のせておきます。ルールをテスト実行するためのdrools関連のライブラリや、kie-maven-pluginというmaven pluginが含まれています。
...
<dependencies>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-engine</artifactId>
<version>${drools.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-xml-support</artifactId>
<version>${drools.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-mvel</artifactId>
<version>${drools.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.kie</groupId>
<artifactId>kie-maven-plugin</artifactId>
<version>${drools.version}</version>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
...
(個人的にはGradleのほうが好きですが・・・)
Droolsの公式ドキュメントによると、kie-maven-pluginを用いてビルドすることが推奨されているので、Mavenプロジェクトで作成しています。
kie_scanner_spring
drink_rule_kjarで作成したKJARのルールを使用して、ドリンクを決定するアプリケーションのプロジェクトです。
Spring bootで構築されたWEB APIとなっています。
ルールの実行に関わる処理を抜粋して紹介していきます。
Controllerクラスのdrink urlでPersonオブジェクトをリクエストとして受け付けています。
@RestController
public class DrinkController {
@Autowired
DrinkDecisionService drinkDecisionService;
@GetMapping("/drink")
public Drink decideDrink(@ModelAttribute(name = "person") Person person) {
Drink drink = drinkDecisionService.decideDrink(person);
return drink;
}
}
APIのInput, Outとなるデータクラスは以下のように定義されています。
package org.example.kiescannerspring.facts;
public class Person {
private String name;
private int age;
...
package org.example.kiescannerspring.facts;
public class Drink {
private String name;
private int charge;
...
Controllerクラスに注入されるサービスクラスに、KieContainer作成用の処理と、ルールを実行する処理を書いています。
@Service
public class DrinkDecisionService {
KieServices ks = KieServices.Factory.get();
ReleaseId releaseId = ks.newReleaseId("org.example", "drink_rule_kjar", "0.0.1-SNAPSHOT");
KieContainer kieContainer;
@PostConstruct
void initKieContainer() {
this.kieContainer = ks.newKieContainer(releaseId);
KieScanner kScanner = ks.newKieScanner(this.kieContainer);
kScanner.start(10000L);
}
public Drink decideDrink(Person person) {
StatelessKieSession kieSession = kieContainer.newStatelessKieSession();
// set up
Object drinkObj = createDrinkFact(kieContainer);
Object personObj = createPersonFact(kieContainer, person);
// execute
Command insertElementsCommand = CommandFactory.newInsertElements(Arrays.asList(personObj, drinkObj));
kieSession.execute(insertElementsCommand);
return convertToDrink(drinkObj);
}
private Object createPersonFact(KieContainer kContainer, Person person) {
Object o = null;
try {
Class cl = kContainer.getClassLoader().loadClass("org.example.Person");
o = cl.newInstance();
Method setAge = cl.getMethod("setAge", int.class);
setAge.invoke(o, person.getAge());
Method setName = cl.getMethod("setName", String.class);
setName.invoke(o, person.getName());
} catch (InstantiationException | ClassNotFoundException | IllegalAccessException
| InvocationTargetException |NoSuchMethodException e) {
throw new RuntimeException(e);
}
return o;
}
private Object createDrinkFact(KieContainer kContainer) {
Object o = null;
try {
Class cl = kContainer.getClassLoader().loadClass("org.example.Drink");
o = cl.newInstance();
} catch (InstantiationException | ClassNotFoundException | IllegalAccessException e) {
throw new RuntimeException(e);
}
return o;
}
private Drink convertToDrink(Object drinkObj) {
Drink drink = new Drink();
try {
Field nameField = drinkObj.getClass().getDeclaredField("name");
nameField.setAccessible(true);
drink.setName((String) nameField.get(drinkObj));
Field chargeField = drinkObj.getClass().getDeclaredField("charge");
chargeField.setAccessible(true);
drink.setCharge((Integer) chargeField.get(drinkObj));
} catch ( NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
return drink;
}
}
ここで、
KieServices ks = KieServices.Factory.get();
ReleaseId releaseId = ks.newReleaseId("org.example", "drink_rule_kjar", "0.0.1-SNAPSHOT");
のように、Maven GAVを用いて利用するKJARを指定しています。
PostConstructを使うことで、Bean生成時にKieContainerが初期化されるようにしています。
@PostConstruct
void initKieContainer() {
this.kieContainer = ks.newKieContainer(releaseId);
KieScanner kScanner = ks.newKieScanner(this.kieContainer);
kScanner.start(10000L);
}
さらに、作成したKieContainerをもとにKieScannerを作成しています。
start(10000L)
としているため、10秒ごとにKieScannerがMavenリポジトリをスキャンして、KJARの更新を検知する設定にしています。
メソッドdecideDrinkの中では、KieSessionを生成してDroolsのルールを実行しています。
public Drink decideDrink(Person person) {
StatelessKieSession kieSession = kieContainer.newStatelessKieSession();
// set up
Object drinkObj = createDrinkFact(kieContainer);
Object personObj = createPersonFact(kieContainer, person);
// execute
Command insertElementsCommand = CommandFactory.newInsertElements(Arrays.asList(personObj, drinkObj));
kieSession.execute(insertElementsCommand);
return convertToDrink(drinkObj);
}
実装でハマった点として、kie_scanner_springプロジェクトで定義したPerson, DrinkオブジェクトをそのままKieSessionにinsertしてもルールが発火しませんでした。。
(おそらくKJARで定義されたものと同一クラスのオブジェクトでないとダメ)
そのため、(黒魔術的ではありますが)リフレクションを使ってKJARで定義されたPerson, Drinkオブジェクトを生成しています。
動作確認
アプリを起動した状態でルールが更新されることをを検証していきます。
1. KJARの作成
drink_rule_kjarのプロジェクト配下で以下のコマンドを実行します。
./mvnw clean install
KJARがビルドされ、ローカルのMavenリポジトリにインストールされます。
2.APIの動作確認
kie_scanner_springのmainメソッドを実行し、APIを起動します。
kie_scanner_springの依存関係にspringdoc-openapi-starter-webmvc-ui
を追加しているので、http://localhost:8080/swagger-ui/index.html
にアクセスするとSwagger UIが開きます。
リクエストのPersonとして、{ "name": "Taro", "age": 20}
を入力してAPI実行すると想定どおりビールが提供されます。
3.KJARの更新
続いてSpringアプリケーションを起動したまま 、drink_rule_kjarのプロジェクトでルールを編集し、再ビルドします。
お酒を提供できるのは21歳以上とします。
import org.example.Person
import org.example.Drink
rule "Child"
when
$person : Person( age < 21 )
$drink : Drink()
then
$drink.setName( "Orange Juice" );
$drink.setCharge( 100 );
end
rule "Adult"
when
$person : Person( age >= 21 )
$drink : Drink()
then
$drink.setName( "Beer" );
$drink.setCharge( 200 );
end
再ビルドのためにコマンド実行します。
./mvnw clean install
4.APIでルールが変更されていることの確認
しばらく待つと、kie_scanner_springのログにメッセージが表示されます。
org.kie.api.builder.KieScanner : The following artifacts have been updated: {org.example:drink_rule_kjar:0.0.1-SNAPSHOT=org.example:drink_rule_kjar:jar:0.0.1-SNAPSHOT}
KieScannerによってKJARの更新が検知され、KieContainerが再構築されたことがわかります。
あらためて、Person={ "name": "Taro", "age": 20}
を入力値として、APIを実行してみます。
先程とは異なり、今度はオレンジジュースが返却されます。
KieScannerのメリット
KieScannerを使ってアプリケーション起動中にルールの更新ができることを検証できました。
今回検証した構成のメリットとしては、やはりアプリを止めずにルールの切り替えができることでしょうか。
ただ、Drools公式ドキュメント(後述するリンク先)によると、KieScannerは開発環境での利用が推奨されています。
開発環境でのCICDフローの改善などに利用シーンがありそうと感じました。
リフレクションを使っている箇所だけはなんとかしたいですが、、代替案が見つけられませんでした。
引き続き調査していきたいと思います。
参考リンク
- Using a KIE scanner to monitor and update KIE containers(Drools公式ドキュメント)
- KJARについては、以下の記事がとてもわかりやすかったです