前置き
Red Hat Decision Managerをとりあえず触って動かしてみたい人向け。
注:2021/4追記 ----
Developer Studio(CodeReady Studio)を前提に以下書かれていますが、現在はCodeReady Studioのプラグインは新しいものがリリースされなくなっており、IDEを使う方は、VSCodeを利用されることをおすすめします。ちなみに、VSCodeには、以下のサンプルはありません。
環境構築は以下を参照してください。
Red Hat Decision Managerをとりあえず触ってみたい場合 -開発者向け環境構築編-
ちなみに、開発者向け記事です。
- Javaソースコードが読める
- Maven基礎知識がある
という人を前提としています。
さて、Red Hat Developer Studioをインストールして、プラグインを追加したら、用意されているサンプルプロジェクトをDeveloper Studioにインポートしてみます。
ちなみに、以下のサンプルプロジェクトは、製品版のDecision Managerのではなく、Droolsのプロジェクトです。
ルールの書き方、ルールを実行するためのJavaコードの書き方、ルールに渡すデータオブジェクトの定義方法など、ルール実装に最低限必要なものが揃っています。
Decision Manager(Drools)を触るのが初めてという方は、とりあえずこれをインポートしてみると良いです。
注)記事内容はすべて個人の見解に基づくものであり、RedHat社の公式見解ではありません。
サンプルプロジェクトのインポート
Developer Studioを起動します。
プロジェクトを新規作成します。「File」 → 「New」 → 「Other」
以下の画面で「Drools Project」を選択して、「Next」
※ここで「Drools Project」が表示されない、という場合は、プラグインの追加をしていない可能性があるので、今一度環境構築編を読んで、ルール開発用プラグインの追加をしてください。
以下の画面で、真ん中のアイコンを選択(クリックする)して、「Next」
Project nameは任意の名前で。
Locationは、特に問題なければデフォルトの場所で。
Build the Project usingは、Mavenを選択。(http://repository.jboss.org/nexus/content/groups/public/ にアクセス可能である必要がある)
「Finish」をクリックすると、DroolsのサンプルプロジェクトがDeveloper Studioにインポートされる。
インポートされたプロジェクトは、こんな感じ。↓
src/main/java 配下に、実行用Javaファイル、src/main/resources 配下に、ルール定義などが入っている。
ここで、最初にpom.xmlの中身を見ておこう。
・・・
<name>Drools :: Sample Maven Project</name>
<description>A sample Drools Maven project</description>
<properties>
<runtime.version>7.0.0.Final</runtime.version>
</properties>
<repositories>
<repository>
<id>jboss-public-repository-group</id>
<name>JBoss Public Repository Group</name>
<url>http://repository.jboss.org/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</snapshots>
</repository>
</repositories>
・・・
上記の通り、製品版ではなく、Droolsのリポジトリを参照している。
ちなみに、製品版の場合は、<runtime.version>は、7.7.0.Final-redhat-8というように、***-redhat-***というバージョン名がつきます。ただし、このバージョン番号が、実際の製品バージョンと一致した数値になるわけではないので、少々ややこしいのでご注意ください。
ルールを実行してみよう
インポートしたサンプルプロジェクトには、3つのJavaファイルがあり、それぞれがmainメソッドを持つJavaクラスになっています。
- DecisionTableTest.java
- dtables/Sample.xlsに定義されたデシジョンテーブル形式のルールを実行するJavaクラス。
- DroolsTest.java
- rules/Sample.drlに定義されたDRL形式のルールを実行するJavaクラス。
- ProcessTest.java
- process/sample.bpmnに定義されたプロセスを実行するJavaクラス。
まず、DroolesTest.javaを実行してみましょう。
DroolsTest.javaを開き、右クリックで、「Run As」 → 「Java Application」
数秒後に、コンソールに以下のように表示されるはずです。
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Hello World
Goodbye cruel world
※SLF4Jから始まる行は、今は無視して問題ありません。単なる警告です。
下2行の
Hello World
Goodbye cruel world
が、ルール実行により出力された行になります。
一応これで、ルールをコンパイルして、データを渡して実行するところまでやったことになります。
でも、何が起きたのか、よくわかりませんね?
中身を解説します。
サンプルルール実行解説
DroolsTest.javaの中身を見てみましょう。
まずは、mainメソッド部分です。
(抜粋)
public static final void main(String[] args) {
try {
// load up the knowledge base
KieServices ks = KieServices.Factory.get();
KieContainer kContainer = ks.getKieClasspathContainer();
KieSession kSession = kContainer.newKieSession("ksession-rules");
// go !
Message message = new Message();
message.setMessage("Hello World");
message.setStatus(Message.HELLO);
kSession.insert(message);
kSession.fireAllRules();
} catch (Throwable t) {
t.printStackTrace();
}
}
mainメソッドの最初の3行を見ます。
KieServices ks = KieServices.Factory.get();
KieContainer kContainer = ks.getKieClasspathContainer();
KieSession kSession = kContainer.newKieSession("ksession-rules");
頭にKieがつく各種クラスは、ルールのコンパイル・実行に使用するAPIを提供するクラスです。
Javaアプリケーションにルールエンジンを組み込んで使用する場合は、このようにKieAPIを直接利用します。
KIE API
- KieServices
- スレッドセーフなSingleton。すべてのKIEにアクセス可能。まずこのインスタンスを取得するところから始まる。
- KieContainer
- kmodule.xmlを読み込む。ここではクラスパス上のsrc/main/resources/META-INF/kmodule.xmlを読み込んでいる。kmodule.xmlにて、KieBaseやKieSessionの定義をする。詳しくは後ほど。
- KieBase
- 上記サンプルでは直接出てきていないが、KieBaseがまず作られ、そこからKieSessionが生成される。ルールアセット(ルール定義やファクト定義等)を保持する。ランタイムデータは持たない。
- KieSession
- ルール実行ランタイム。データを保持。KieSession単位でルールは実行される。
ちなみに、オープンソースなので、ソースコードを見ることが出来ます。Maven使ってjarを取得した場合は、Developer Studio上で、F3(Open Declaration)でソースを追っていけます。
ファクト
ファクト、とは、ルール内で扱うデータのことです。
基本的に、JavaのPOJOで定義します。
ルールを書く前に、まずファクト定義をする必要があります。
ここでは、DroolsTest.javaのインナークラスとして、以下のようにMessageファクトが定義されていますが、インナークラスである必要はありません。
通常、ファクトは複数種類定義しますので、それぞれを独立したクラスで定義します。
ネストすることも可です。
(抜粋)
public static class Message {
public static final int HELLO = 0;
public static final int GOODBYE = 1;
private String message;
private int status;
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
public int getStatus() {
return this.status;
}
public void setStatus(int status) {
this.status = status;
}
}
上記のMessageファクトは、String型のmessageとint型のstatusという2つのフィールドを持っています。
なお、ルール条件で使用するフィールドには、必ずgetterメソッドが必要です。
というのは、ルール条件で、
Message (status > 1)
と書かれていたら、MessageファクトのgetStatus()メソッドを探しに行くからです。statusフィールドが定義されていても、getStatus()メソッドが定義されていない(またはアクセスできない)場合は、ルールコンパイル時にエラーになります。
(ファクトのフィールドのgetter,setterは常に実装しておけば問題ないです。Lombok等を使っても良いです。)
さて、いまいちど、DroolsTest.javaに戻ります。
(抜粋)
// go !
Message message = new Message();
message.setMessage("Hello World");
message.setStatus(Message.HELLO);
kSession.insert(message);
kSession.fireAllRules();
最初の3行で、Messageファクトをnewして、各フィールドに値をセットしていますね。
message に "Hello World" をセットし、
status に Message.HELLO をセットしています。
そして、
kSession.insert(message);
ここで、KieSessionにファクトを挿入しています。
ここでは、Messageファクトを1つだけ挿入していますが、ファクトはいくつでも何種類でも渡すことが出来ます。
そして最後に、
kSession.fireAllRules();
このメソッド実行で、ルールが実行されます。fireAllRulesとあるように、KieBase内の全ルールが一斉に動きます。
ルールを実行することを、fireするという言い方をするんですね。日本語だと発火といったりします。
kmodule.xmlについて
さて、では実行されたルールについて見てみましょう。
DroolsTest.javaが実行したルールはどれなのか?
ソースコードの以下の行に注目してください。
KieSession kSession = kContainer.newKieSession("ksession-rules");
KieContainerがクラスパス上のkmodule.xmlを参照しているはずなので、kmodule.xmlの中身を見てみます。
<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns="http://jboss.org/kie/6.0.0/kmodule">
<kbase name="rules" packages="rules">
<ksession name="ksession-rules"/>
</kbase>
<kbase name="dtables" packages="dtables">
<ksession name="ksession-dtables"/>
</kbase>
<kbase name="process" packages="process">
<ksession name="ksession-process"/>
</kbase>
</kmodule>
<kbase></kbase>で囲まれたところが、KieBaseの定義です。ここでは3つ定義があります。また、それぞれのKieBaseの定義内に<ksession></ksession>で囲まれたkieSessionの定義があります。
今、DroolsTest.javaでは、"ksession-rules"という名前のKieSessionを生成していましたので、そこを見てみましょう。
<kbase name="rules" packages="rules">
<ksession name="ksession-rules"/>
</kbase>
kbaseのpackages属性をみると"rules"とあります。これは、src/main/resources/rulesディレクトリ配下のルールを対象にKieBaseを作成するよ、ということです。
src/main/resources/rulesディレクトリ下を見ると、Sample.drlというルールファイルが1つあります。
よって、DroolsTest.javaでは、このSample.drlに定義されたルールが実行されているということです。
ルール定義(DRL)
さて、Sample.drlについて解説します。
package com.sample
import com.sample.DroolsTest.Message;
rule "Hello World"
when
m : Message( status == Message.HELLO, myMessage : message )
then
System.out.println( myMessage );
m.setMessage( "Goodbye cruel world" );
m.setStatus( Message.GOODBYE );
update( m );
end
rule "GoodBye"
when
Message( status == Message.GOODBYE, myMessage : message )
then
System.out.println( myMessage );
end
Javaに少し似たような感じですが、これは、Drools Rule Language(DRL)という独自の言語です。
Decision Manager(Drools)のルールは、このDRLで記述するのが基本です。
パッケージ文、インポート文については、Javaの構造と同じようなものと思えばよいです。
ファクトやJavaで定義した関数などは、インポートして使用します。
rule〜endまでが、1つのルールになります。つまり、ここでは2つのルールがあります。
ruleには名前をつける必要があります。ルールの名前は、KieBase内で一意でなければなりません。
ここでは、"Hello World"というルールと、"GoodBye"というルールがあります。ルール名は日本語にすることも可能です。
DRLの記法はシンプルなので、新たな言語を習得するというほどハードルは高くないです。
基本記法は以下の通り。
一言で言うと、whenの後に条件、thenの後にその条件に一致した場合のアクションを書きます。
1つ目の"Hello World"ルールを見てみましょう。
rule "Hello World"
when
m : Message( status == Message.HELLO, myMessage : message )
then
System.out.println( myMessage );
m.setMessage( "Goodbye cruel world" );
m.setStatus( Message.GOODBYE );
update( m );
end
ここでは、条件(when)は1行だけですね。
when
m : Message( status == Message.HELLO, myMessage : message )
解説図
コロンはバインドを表します。左側がバインド変数名です。任意の名前をつけられます。
カッコ内が条件文になります。ここでは、status == Message.HELLO
だけが条件ですが、条件が複数ある場合は、カンマ区切りで続けて書きます。
つまり、この条件文は、
statusの値がMessage.HELLO(=0)の、Messageファクトがある時
という意味になります。
2つ目のルール"GoodBye"の条件文も見てみます。
when
Message( status == Message.GOODBYE, myMessage : message )
頭のバインドがないだけで、ほぼ同じですね。
statusの値がMessage.GOODBYE(=1)の、Messageファクトがある時
という条件です。
続いて、それぞれのルールのthenの方も見ていきましょう。
rule "Hello World"のthen節。
then
System.out.println( myMessage );
m.setMessage( "Goodbye cruel world" );
m.setStatus( Message.GOODBYE );
update( m );
最初の3行は、意味わかりますね。
標準出力に myMessage にバインドされたmessageを出力し、
m にバインドされたMessageファクトのmessageに、"Goodbye cruel world"という文字列をセットし、
同じくstatusに、Message.GOODBYEをセットしています。
rule "GoodBye"のthen節は、これだけなので問題ないですね。
then
System.out.println( myMessage );
さて、ではどのようにルールが動いたかを説明します。
ルールエンジンの内部動作
ルールエンジンの内部構造
ルール定義はコンパイルされて、プロダクションメモリに配置されます。
KieSessionにインサートしたファクトは、ワーキングメモリに配置されます。
ファクトがインサートされると、ルール条件に一致するものがないかどうかを探します。これをパターンマッチングといいます。
そして、ワーキングメモリ上のファクトとすべての条件が一致したルールは、アジェンダにその組み合わせが登録されます。
- ワーキングメモリ :
- Fact を保持するランタイム領域。保持するファクトのサイズに伴い使用するメモリ領域のサイズが増減
- プロダクションメモリ :
- ルールリソース (.drl, .xls, etc.) が管理される静的な領域
- アジェンダ:
- 推論エンジンによって作成された、ルールの条件にマッチするファクトとルールの組合わせ(アクティベーション)を格納する領域
サンプルルールの動作
DroolsTest.javaの場合は、以下のようになります。
KieServices ks = KieServices.Factory.get();
KieContainer kContainer = ks.getKieClasspathContainer();
KieSession kSession = kContainer.newKieSession("ksession-rules");
ルールコンパイルして、KieSessionが生成された時点。ルールが2つあります。
Message message = new Message();
message.setMessage("Hello World");
message.setStatus(Message.HELLO);
kSession.insert(message);
次に、MessageファクトをKieSessionにインサートした時点。
Messageファクトが1つインサートされました。と同時に、このMessageファクトは、status=Message.HELLOなので、Hello Worldルールの条件と一致するため、アジェンダにHello Worldルールとファクトの組み合わせが登録されました。
ここで注意が必要なのは、インサートした時に、ルール条件と照らし合わせるということです。
ファクトとルール条件を照らし合わせることを、「評価」と言いますが、ルール実行(kSession.fireAllRules())時ではなく、ファクトインサート時に評価が行われます。
kSession.fireAllRules();
さて、次にkSession.fireAllRules()が実行され、アジェンダ内の全組み合わせが実行されます。
つまり、ここではアジェンダ内のHello Worldルールのthen節が実行されます。
then
System.out.println( myMessage );
m.setMessage( "Goodbye cruel world" );
m.setStatus( Message.GOODBYE );
update( m );
この1行目で、Messageファクトのmessageにセットされた文字列を、標準出力しています。
そのため、DroolsTest.javaを実行した際に、コンソール画面の最初に、
Hello World
が出力されたわけです。
さて、System.out.println( myMessage );
の後は、Messageファクトの値を更新していますね。
つまり、ワーキングメモリ上のMessageファクトのフィールドの値が書き換わりました。
そして、最後にupdate( m );
とあります。
これは、m つまり、Messageファクトの値を更新したから、ルールに一致するかどうか再評価して、という命令です。
このupdate文により、現在のMessageファクトと2つのルール条件とを再評価します。
すると、今度は、status=Message.GOODBYEになっているため、GoodByeルールと一致します。
最初に作られたMessageファクトとHello Worldルールの組み合わせは、実行されたためアジェンダから削除されましたが、今度はMessageファクトとGooodByeルールの組み合わせがアジェンダに登録されました。
そして、アジェンダに登録された組み合わせは、順次実行されるため、GoodByeルールのthen節が実行され、
then
System.out.println( myMessage );
コンソールの2行目に、
Goodbye cruel world
が出力されたわけです。
そして、実行されたファクトとルールの組み合わせ(アクティベーション)は、アジェンダから削除され、アジェンダが空になったら、ルール実行は自動で終了します。
ルール演習
ルールの動きを理解するために、3つ演習をしてみましょう。
演習1
まず、Sample.drlのHello Worldルールのupdate( m );
をコメントアウトします。
単行コメントアウトは、//をつけます。
rule "Hello World"
when
m : Message( status == Message.HELLO, myMessage : message )
then
System.out.println( myMessage );
m.setMessage( "Goodbye cruel world" );
m.setStatus( Message.GOODBYE );
//update( m ); ← ここを変更
end
次に、DroolsTest.javaを実行し、コンソールログの内容を確認します。
Hello World
2行目のログが出力されないはずです。
なぜだかわかりますか?
update文をコメントアウトしたことで、再評価が行われなかったからです。
Messageファクトの中身は変わっていますが、そのことを明示的にルールエンジンに通知しない限り、ルールエンジンは再度ルールとファクトの評価を行ってくれません。
そのため、GoodByeルールが実行されずに終わったのです。
演習2
演習1でコメントアウトした箇所は、元に戻します。
そして、今度はGoodByeルールを以下のように修正します。
rule "Hello World"
when
m : Message( status == Message.HELLO, myMessage : message )
then
System.out.println( myMessage );
m.setMessage( "Goodbye cruel world" );
m.setStatus( Message.GOODBYE );
update( m ); ← コメントアウトを元に戻す
end
rule "GoodBye"
when
m : Message( status == Message.GOODBYE, myMessage : message ) ← 頭に "m : "を追加
then
System.out.println( myMessage );
update( m ); ← この行追加
end
DroolTest.javaを再実行します。
さて、どうなりましたか?
Goodbye cruel world
Goodbye cruel world
Goodbye cruel world
が延々と出力され続けていますね?
終わらないので、すぐに赤いボタンを押して止めましょう。
何が起きたかわかりましたか?
無限ループですね。
Goodbyeルールを見てみると、
rule "GoodBye"
when
m : Message( status == Message.GOODBYE, myMessage : message )
then
System.out.println( myMessage );
update( m );
end
ここでも、update文があるので、Goodbye cruel world
を出力した後、再度ファクトとルールが一致しないかどうかの再評価を行います。
すると、この時点ではMessageファクトは、status=Message.GOODBYEなので、再度GoodByeルールと一致します。そこで、アジェンダには、MessageファクトとGoodByeルールの組み合わせが登録され、実行され、Goodbye cruel world
が出力され、まだ再評価でアジェンダに登録され・・・・・・と延々続くことになります。
このように、ルールの実装ミスにより、無限ループが発生する可能性がありますので、注意が必要です。
演習3
演習2で行った、無限ループするルールのままで、今度はDroolsTest.java側に一箇所修正を入れます。
Message message = new Message();
message.setMessage("Hello World");
message.setStatus(Message.HELLO);
kSession.insert(message);
kSession.fireAllRules(5); ← カッコの中に(5)を入れる
保存して、DroolsTest.javaを再実行します。
さて、どうなりましたか?
ループせずに終わったはずです。
Hello World
Goodbye cruel world
Goodbye cruel world
Goodbye cruel world
Goodbye cruel world
これは、ルール実行回数を制限する方法で、fireAllRules(5)
とした場合は、ルールが5回実行されたら、そこで強制終了します。
ここに大きな数値を入れておくことで、万一無限ループした際にも途中で終わるようにすることが出来ます。
最後に
以上、Droolsのサンプルプロジェクトをインポートして、DroolsTest.javaを実行したところまでの解説になります。
サンプルプロジェクトの、DecisionTableTest.javaは、Sample.drlと全く同じルールを、Excel形式で書いた、Sample.xlsを実行するJavaクラスです。
Sample.xlsは、Excelファイルです。開いて中身を確認してみてください。
ルールは、.drl形式だけでなく、このようにExcelなどのスプレッドシート形式でも記述が可能です。
DecisionTableTest.javaを実行すると、DroolsTest.javaと全く同じ結果がログ出力されます。
ルールのスプレッドシートの記述方法については、また別途解説したいと思います。