3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-06-30

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を含んでいます。

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の公式ドキュメントによると、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

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

スクリーンショット_2.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

KieScannerのメリット

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

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

参考リンク

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?