【Drools】KieScannerでアプリケーション起動中にルールを更新する

DroolsのKieScannerを使って、アプリケーション起動中に実行するルールを更新する方法を検証します。

- [はじめに](#はじめに)
- [KJARとKieScannerについて](#kjarとkiescannerについて)
- [サンプルアプリ](#サンプルアプリ)
- [動作確認](#動作確認)
- [KieScannerのメリット](#kiescannerのメリット)
- [参考リンク](#参考リンク)


# はじめに

[前回の記事](https://qiita.com/yagii/items/5c1ee8781dc16252d2a3)では、ルールを定義する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を参照するプロジェクトとの２つあります。

コード全量はGithubにあげています。
- [drink_rule_kjar](https://github.com/Hami1ton/study_brms/tree/main/drools/kie_scanner_inspection/drink_rule_kjar)
- [kie_scanner_spring](https://github.com/Hami1ton/study_brms/tree/main/drools/kie_scanner_inspection/kie_scanner_spring)

## drink_rule_kjar

KJARをビルドするためのプロジェクトです。
ルールが扱うデータクラスとDRLファイル、kmodule.xmlを含んでいます。

```Drink.java
package org.example;

public class Drink {

    private String name;

    private int charge;

...
```

```Person.java
package org.example;

public class Person {

    private String name;

    private int age;
...
```

```Drink.drl
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

```

```kmodule.xml
<?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が含まれています。

```pom.xml
...
	<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の[公式ドキュメント](https://docs.drools.org/8.39.0.Final/drools-docs/docs-website/drools/KIE/index.html#_kiemoduleintroductionbuildingintroductionsection)によると、kie-maven-pluginを用いてビルドすることが推奨されているので、Mavenプロジェクトで作成しています。

## kie_scanner_spring

drink_rule_kjarで作成したKJARのルールを使用して、ドリンクを決定するアプリケーションのプロジェクトです。
Spring bootで構築されたWEB APIとなっています。

ルールの実行に関わる処理を抜粋して紹介していきます。
Controllerクラスのdrink urlでPersonオブジェクトをリクエストとして受け付けています。

```DrinkController.java
@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となるデータクラスは以下のように定義されています。

```Person.java
package org.example.kiescannerspring.facts;

public class Person {

    private String name;

    private int age;
...
```

```Drink.java
package org.example.kiescannerspring.facts;

public class Drink {

    private String name;

    private int charge;
...
```

Controllerクラスに注入されるサービスクラスに、KieContainer作成用の処理と、ルールを実行する処理を書いています。
```DrinkDecisionService.java
@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が開きます。

![スクリーンショット_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481175/1908e7c3-1c53-04a8-d50e-9965c5063722.png)

リクエストのPersonとして、`{ "name": "Taro",　"age": 20}`を入力してAPI実行すると想定どおりビールが提供されます。

![スクリーンショット_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481175/509576fb-4b9b-3d99-1364-f51ede17d24b.png)

### 3.KJARの更新

続いて**Springアプリケーションを起動したまま** 、drink_rule_kjarのプロジェクトでルールを編集し、再ビルドします。
お酒を提供できるのは21歳以上とします。

```Drink.drl
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を実行してみます。
先程とは異なり、今度はオレンジジュースが返却されます。

![スクリーンショット_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481175/821b50fb-d784-f4c4-4dcc-1bdfcc9581f1.png)


## KieScannerのメリット

KieScannerを使ってアプリケーション起動中にルールの更新ができることを検証できました。
今回検証した構成のメリットとしては、やはりアプリを止めずにルールの切り替えができることでしょうか。
ただ、Drools公式ドキュメント(後述するリンク先)によると、KieScannerは開発環境での利用が推奨されています。
開発環境でのCICDフローの改善などに利用シーンがありそうと感じました。

リフレクションを使っている箇所だけはなんとかしたいですが、、代替案が見つけられませんでした。
引き続き調査していきたいと思います。

# 参考リンク

- [Using a KIE scanner to monitor and update KIE containers(Drools公式ドキュメント)](https://docs.drools.org/8.39.0.Final/drools-docs/docs-website/drools/KIE/index.html#kie-scanner-proc_packaging-deploying)
- KJARについては、以下の記事がとてもわかりやすかったです
    - [Droolsブログ : 05 KJAR](https://tokobayashi.hatenablog.com/entry/2019/06/27/144116)