Asakusa Framework0.7.6のDirect I/Oおよび拡張機能を使ってWordCountを作ってみた。
|
|
AsakusaFWの標準の機能だけを使ったWordCountに対し、標準以外の機能も使ったWordCountを作ってみた。
標準以外の機能とは、以下のものである。
前提:EclipseにShafuおよび拙作DMDL EditorXが入っていること。
〜
repositories {
maven { url 'http://hishidama.github.io/mvnrepository' }
}
dependencies {
compile group: 'com.asakusafw.sdk', name: 'asakusa-sdk-core', version: asakusafw.asakusafwVersion
compile group: 'com.asakusafw.sdk', name: 'asakusa-sdk-directio', version: asakusafw.asakusafwVersion
compile group: 'com.asakusafw.sdk', name: 'asakusa-sdk-windgate', version: asakusafw.asakusafwVersion
compile group: 'com.asakusafw.sandbox', name: 'asakusa-directio-dmdl-ext', version: asakusafw.asakusafwVersion
compile group: 'jp.hishidama.asakusafw', name: 'asakusafw-spi', version: '0.+'
testRuntime group: 'com.asakusafw.sdk', name: 'asakusa-sdk-test-emulation', version: asakusafw.asakusafwVersion
testRuntime group: 'jp.hishidama.asakusafw', name: 'asakusafw-tester', version: '0.+'
〜
}
※asakusa-directio-dmdl-extは、AsakusaFW 0.7.5〜0.8.0でdirectio.lineを使う為に必要。0.8.1以降では不要。[2016-07-31]最初に、Importer/Exporterクラスのテンプレートを作っておく。
(このテンプレートのファイル名をDMDL上に記述する為)
テンプレートファイルは(dmdlの場所に合わせて)src/main/templateの下に置くこととする。
テンプレートにはFreeMarkerというテンプレートエンジンが使われているので、拡張子はftlとする。
今回は、Importerはdirectio.line(テキストファイル読み込み用)、Exporterはdirectio.csv(CSVファイル出力用)のテンプレートを作成する。
package ${packageName};
/**
* ${model.description!""}
*/
public class ${className} extends Abstract${model.camelName}LineInputDescription {
@Override
public String getBasePath() {
return "master/${model.name}";
}
@Override
public String getResourcePattern() {
return "*.csv";
}
@Override
public DataSize getDataSize() {
return DataSize.${arg("dataSize")!"UNKOWN"};
}
}
package ${packageName};
/**
* ${model.description!""}
*/
public class ${className} extends Abstract${model.camelName}CsvOutputDescription {
@Override
public String getBasePath() {
return "result/${model.name}";
}
@Override
public String getResourcePattern() {
return "${model.name}.csv";
}
}
見ればなんとなく分かる通り、「${変数名}」で変数が使用できる。
「${変数名!値}」は、変数がnullだった場合に「!」以降で指定された値が出力される。
また、「<#タグ>」でFreeMarker用の構文も使える。
→テンプレートを使って実際に生成されたクラス(javaソース)
入力データ・出力データを表すクラス(モデル)を作成する。
今回は入力ファイルにdirectio.lineを使うので、入力行を表す1項目だけがあるデータモデルを作成する。
出力は単語と個数というCSVファイルとする。
中間のデータモデルとして、1行のテキストを分割した単語のみを表すデータモデルも用意する。
ここではmodels.dmdlというファイルを作成し、データモデルを記述する。
WordCountであれば、入力は単なる文字列(text)、出力は単語(word)と出現数(count)。
"テキストファイルの一行に対応するエンティティ"
@directio.line
@template(
id = "fromLine",
template = "src/main/template/FromLine.ftl",
category = "line",
type_name_pattern = "{0}FromLine",
args = { "dataSize=LARGE" }
)
line_model = {
"テキスト"
text : TEXT;
};
word_model = {
"単語"
word : TEXT;
};
"単語と発生回数の対を表現するエンティティ"
@directio.csv
@template(
id = "toCsv",
template = "src/main/template/ToCsv.ftl",
category = "csv",
type_name_pattern = "{0}ToCsv"
)
summarized word_count_model = word_model => {
"単語"
any word -> word;
"発生回数"
count word -> count;
} % word;
「@template」でテンプレートファイルの場所やファイル名・引数を指定する。
| 属性名 | 説明 |
|---|---|
| id | ひとつのデータモデルに複数の@templateを付ける場合、それぞれを区別するためのもの。 区別がつきさえすれば、適当な文字列でよい。 |
| template | テンプレートファイルのパス。プロジェクトディレクトリーからの相対パス。 |
| category | 生成されるソースのパッケージ名の一部に使われる。 |
| type_name_pattern | 生成されるクラス名。「{0}」の部分はデータモデルクラス名に置換される。 |
| args | テンプレート内で使う値。 「 args = { "引数名=値" }」と書くと、テンプレート側で「${arg("引数名")}」で値が取れる。 |
作成したDMDLファイルからデータモデルクラス(Javaソース)を生成する。
Shafuを使う場合は以下のようにして生成する。
あるいは、DMDL EditorXを使う場合は以下のようにする。
これにより、@directio.lineや@directio.csv属性が付けられたデータモデルのImporter/Exporterの抽象クラスが生成される。
また、@template属性に従ったクラスも生成される。
ちなみに、テンプレートによって作られたクラスは以下のようになっている。
package com.example.modelgen.dmdl.line;
/**
* テキストファイルの一行に対応するエンティティ
*/
public class LineModelFromLine extends AbstractLineModelLineInputDescription {
@Override
public String getBasePath() {
return "master/line_model";
}
@Override
public String getResourcePattern() {
return "*.csv";
}
@Override
public DataSize getDataSize() {
return DataSize.LARGE;
}
}
package com.example.modelgen.dmdl.csv;
/**
* 単語と発生回数の対を表現するエンティティ
*/
public class WordCountModelToCsv extends AbstractWordCountModelCsvOutputDescription {
@Override
public String getBasePath() {
return "result/word_count_model";
}
@Override
public String getResourcePattern() {
return "word_count_model.csv";
}
}
@directio.line・@directio.csvによって作られる抽象クラスを継承しているが、その抽象クラスと同じパッケージに属すようにcategoryを付けたので、import文は無くても大丈夫。
次に、オペレーター(演算子)を作成する。
オペレーターはJavaの抽象クラス内に定義する決まりになっているので、最初に空のクラスを作る。
package com.example.operator;
public abstract class WordCountOperator {
}
テキストを単語に分割するには、Extract(抽出演算子)を使う。
import java.util.StringTokenizer; import com.asakusafw.runtime.core.Result; import com.asakusafw.vocabulary.operator.Extract; import com.example.modelgen.dmdl.model.LineModel; import com.example.modelgen.dmdl.model.WordModel;
private final WordModel wordModel = new WordModel();
@Extract
public void split(LineModel line, Result<WordModel> out) {
String text = line.getTextAsString();
StringTokenizer tokenizer = new StringTokenizer(text);
while (tokenizer.hasMoreTokens()) {
String word = tokenizer.nextToken();
wordModel.reset();
wordModel.setWordAsString(word);
out.add(wordModel);
}
}
Operatorクラスでは、メソッド名がジョブフローで使えるメソッド名(AsakusaDSL的には“語彙”と呼ばれる)になる。
Resultの変数名は、ジョブフローでの出力フィールド名になる。
(デフォルトで用意されている演算子(core演算子)では、出力が1つの場合はoutという名前になっている事が多いので、それを踏襲した)
DMDL上でTEXTとして宣言した項目は、Javaのクラス上はHadoopのTextクラスになっている。
そしてセッター・ゲッターには「asString」という接尾辞が付いたメソッドが用意されているので、Stringクラスで読み書きできる。
DMDLでsummarizedを使った場合、ジョブフローで使えるメソッド名(語彙)を決める為に(だと思う)、OperatorクラスにSummarize演算子(メソッド名と入出力のデータモデルクラス)を記述する。
import com.asakusafw.vocabulary.flow.processor.PartialAggregation; import com.asakusafw.vocabulary.operator.Summarize; import com.example.modelgen.dmdl.model.WordCountModel; import com.example.modelgen.dmdl.model.WordModel;
@Summarize(partialAggregation = PartialAggregation.PARTIAL) public abstract WordCountModel count(WordModel in);
集計方法自体はDMDLの方に書かれているので、Operatorのメソッドとしては特に記述するものは無い。
したがって抽象メソッドになっている。
SummarizeにPARTIALを指定すると、部分集約(中間集計)が行われる(つまりCombinerが動く)ようになる。
Operatorのソースを記述して保存すると、EclipseのAsakusaFWプラグイン(Ashigelコンパイラー)が自動的に起動して、FactoryクラスやOperatorImplクラスを生成する。
(DMDLと違って、何らかのコマンドを実行する必要は無い)
もしエラーがあったら、通常のEclipseでのコンパイルエラーと同様にソース上に赤い印が出る。
オペレーター(演算子)は実行時に普通に呼び出されるクラスなので、単体テストは普通にJUnit4で直接メソッドを呼び出す形で行う。
が、今回はメソッド呼び出し部分に拙作のテストツールを使ってみる。
このテストツールでは、Operatorの演算子の種類に応じたTesterクラスを使用する。
package dio.wordcount.operator; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.List; import jp.hishidama.asakusafw.tester.ExtractTester; import jp.hishidama.asakusafw.tester.OperatorTester.OperatorResults; import org.junit.Test; import com.example.modelgen.dmdl.model.LineModel; import com.example.modelgen.dmdl.model.WordModel;
public class WordCountOperatorTest {
@Test
public void testSplit() {
ExtractTester tester = new ExtractTester(WordCountOperator.class, "split");
// 入力データ
List<LineModel> list = new ArrayList<>();
list.add(createText("word1"));
list.add(createText(""));
list.add(createText("word3 blank"));
list.add(createText("word4 blank"));
list.add(createText("word5\ttab"));
list.add(createText("word6\t tab-blank"));
list.add(createText("word7\ttab blank"));
// テスト実行
OperatorResults result = tester.execute(list);
List<WordModel> out = result.get(0);
// 結果確認
String[] expected = { "word1", "word3", "blank", "word4", "blank", "word5", "tab", "word6", "tab-blank", "word7", "tab", "blank" };
assertThat(out.size(), is(expected.length));
int i = 0;
for (WordModel word : out) {
assertThat(word.getWordAsString(), is(expected[i++]));
}
}
LineModel createText(String text) {
LineModel model = new LineModel();
model.setTextAsString(text);
return model;
}
}
testerのインスタンス生成時に、テスト対象のOperatorクラス名とメソッド名を渡す。
(内部でOperatorImplのインスタンスが生成される)
通常のOperatorのテストの場合、OperatorImplクラスのメソッドを直接呼ぶ形になるが、
拙作のテストツールの場合、入力データのリストを渡し、出力結果のリストを受け取る形になる。
(Extract演算子だと直接メソッドを呼ぶ形式の方がテストしやすいかもしれないが、元々このテストツールは、入力データのソートやグルーピングが必要となる演算子(GroupSortやMasterJoin等)のテストをする目的で作った)
countメソッドは抽象メソッドであり、実体はAsakusaFWが生成するので、通常は単体テストの対象外。
OperatorImplでのcountメソッドの実装は、例外が発生するようになっている(使えないようになっている)。
が、集計モデルのキーが正しいのかどうかテストしたければ、拙作のテストツールで@Summarizeのメソッドをテストすることも出来る。[2015-12-23]
import jp.hishidama.asakusafw.tester.SummarizeTester; import com.example.modelgen.dmdl.model.WordCountModel; import com.example.modelgen.dmdl.model.WordModel;
@Test
public void testCount() {
SummarizeTester tester = new SummarizeTester(WordCountOperator.class, "count");
// 入力データ
List<WordModel> list = new ArrayList<>();
list.add(createWord("Hello"));
list.add(createWord("World"));
list.add(createWord("Hello"));
list.add(createWord("Asakusa"));
// テストの実行
List<WordCountModel> out = tester.execute(list);
// 結果確認
assertThat(out.size(), is(3));
Map<String, Long> map = new HashMap<>();
for (WordCountModel model : out) {
map.put(model.getWordAsString(), model.getCount());
}
assertThat(map.get("Hello"), is(2L));
assertThat(map.get("World"), is(1L));
assertThat(map.get("Asakusa"), is(1L));
}
WordModel createWord(String word) {
WordModel model = new WordModel();
model.setWordAsString(word);
return model;
}
次に、ジョブフローを定義する。
ここで「どのファイルを入力とし、どのオペレーターを呼んで、どのファイルへ出力するか」を記述する。
参考: Asakusa FrameworkのAsakusa DSLスタートガイド#データフローを記述する
通常であれば、まずImporter/Exporterクラスを作成するのだが、今回はテンプレートによって生成されているので、何もしなくてよい。
今回は、DMDL EditorXの機能を使ってジョブフロークラスを作ってみる。
| 項目 | 説明 | 例 |
|---|---|---|
| Source folder | ソースのディレクトリー。 | wordcount2/src/main/java |
| Package | パッケージ名。 | com.example.jobflow |
| Name | クラス名。 | WordCountJob |
| 項目 | 説明 | 例 |
|---|---|---|
| comment | ジョブフローの日本語名。 クラスやコンストラクターのJavadocに使われる。 |
ワードカウントジョブ |
| JobFlow name | ジョブフロー名。 「 @JobFlow(name="ジョブフロー名")」として使われる。個人的には、クラス名と同じ名前にしてしまっている。 |
WordCountJob |
package com.example.jobflow; import com.asakusafw.vocabulary.flow.Export; import com.asakusafw.vocabulary.flow.FlowDescription; import com.asakusafw.vocabulary.flow.Import; import com.asakusafw.vocabulary.flow.In; import com.asakusafw.vocabulary.flow.JobFlow; import com.asakusafw.vocabulary.flow.Out; import com.example.modelgen.dmdl.csv.WordCountModelToCsv; import com.example.modelgen.dmdl.line.LineModelFromLine; import com.example.modelgen.dmdl.model.LineModel; import com.example.modelgen.dmdl.model.WordCountModel;
/**
* ワードカウントジョブ
*/
@JobFlow(name = "WordCountJob")
public class WordCountJob extends FlowDescription {
/** テキストファイルの一行に対応するエンティティ */
private final In<LineModel> lineModel;
/** 単語と発生回数の対を表現するエンティティ */
private final Out<WordCountModel> wordCountModel;
/**
* ワードカウントジョブ
*
* @param lineModel テキストファイルの一行に対応するエンティティ
* @param wordCountModel 単語と発生回数の対を表現するエンティティ
*/
public WordCountJob(
@Import(name = "lineModel", description = LineModelFromLine.class) In<LineModel> lineModel,
@Export(name = "wordCountModel", description = WordCountModelToCsv.class) Out<WordCountModel> wordCountModel
) {
this.lineModel = lineModel;
this.wordCountModel = wordCountModel;
}
@Override
public void describe() {
// TODO WordCountJob.describe()
}
}
describeメソッドの中は自分で書く必要がある。
import com.example.operator.WordCountOperatorFactory; import com.example.operator.WordCountOperatorFactory.Count; import com.example.operator.WordCountOperatorFactory.Split;
@Override
protected void describe() {
WordCountOperatorFactory operators = new WordCountOperatorFactory();
Split split = operators.split(lineModel);
Count count = operators.count(split.out);
wordCountModel.add(count.out);
}
}
operators.split()やoperators.count()が、自分で作ったオペレーター(演算子)を表している。
split.outの「out」は、Extractを使って自分で記述したメソッドの引数のResultの変数名。
ジョブフロークラス上で右クリックしてコンテキストメニューを開き、「DMDL EditorX」→「Modify JofFlow/FlowPart
Field」を実行すると、
ジョブフロークラスを作ったときと同様のウィザードで、コンストラクターの引数およびフィールドを編集(追加・削除)することが出来る。
ただし、編集されたフィールドやコンストラクターはインデントが無茶苦茶になってしまうので、Ctrl+Shift+Fでソース整形することを推奨する。
AsakusaFWでは、ジョブフロー(とFlowPartとバッチ)のテスト用クラスが用意されている。
それらを使ってJUnit4として実行できる。(今回はスモールジョブ実行エンジンでテストを実行するよう最初に設定してある)
今回は、DMDL EditorXの機能を使ってジョブフローのテストクラスおよびテストデータ用Excelファイルを生成してみる。
package com.example.jobflow; import com.asakusafw.testdriver.JobFlowTester; import com.asakusafw.testdriver.core.PropertyName; import com.example.jobflow.WordCountJob; import com.example.modelgen.dmdl.model.LineModel; import com.example.modelgen.dmdl.model.WordCountModel; import org.junit.Test;
/**
* {@link WordCountJob}のテスト。
*/
public class WordCountJobTest {
static {
System.setProperty(PropertyName.KEY_SEGMENT_SEPARATOR, "_");
}
@Test
public void describe() {
JobFlowTester tester = new JobFlowTester(getClass());
// TODO tester.setBatchArg("arg", "value");
tester.input("lineModel", LineModel.class).prepare("WordCountJobTest.xls#lineModel");
tester.output("wordCountModel", WordCountModel.class).verify("WordCountJobTest.xls#wordCountModel", "WordCountJobTest.xls#wordCountModel_rule");
tester.runTest(WordCountJob.class);
}
}
この状態で、Ctrlキーを押しながらExcelファイル名の部分をマウスでホバーするとメニューが出る。
そこで「Open File」を選択すると、そのExcelファイルを開くことが出来る。
(src/test/resource下の同一パッケージ内にそのExcelファイルがあるときだけ使える技(笑))
このExcelファイルに対し、入力データおよび検証データを記載する。
参考: Asakusa FrameworkのExcelによるテストデータ定義
| A | B | C | |
| 1 | text | ||
| 2 | Hello Hadoop World | ||
| 3 | Hello Asakusa | ||
| 4 | |||
| 5 |
| A | B | C | |
| 1 | word | count | |
| 2 | Asakusa | 1 | |
| 3 | Hadoop | 1 | |
| 4 | Hello | 2 | |
| 5 | World | 1 | |
| 6 |
| A | B | C | D | E | F | |
| 1 | Format | EVR-2.0.0 | ||||
| 2 | 全体の比較 | 全てのデータを検査 [Strict] | ||||
| 3 | プロパティ | 値の比較 | NULLの比較 | コメント | オプション | |
| 4 | word | 検査キー [Key] | 通常比較 [-] | 単語 | ||
| 5 | count | 完全一致 [=] | 通常比較 [-] | 発生回数 | ||
| 6 |
検証データシートに書かれているデータと実際のデータをどう比較してどういう状態ならテストOK(あるいはNG)とするかをruleシートに書く。
どのセルも初期値は自動で入っている。
今回のケースでは出力項目がwordとcountの二項目なので、それぞれどういう比較をするかを書く。(書くというか、実際はプルダウンになっているので、選択する)
※ExcelファイルをEclipse外のエディターで編集したら、Eclipse上でF5を押して反映させる(srcからclassesへコピーさせる)必要がある。
→Excelファイルを使用せず、独自のデータを入力データにする方法
最後にBatch DSLで「どのジョブフローを実行するか」を記述する。
今回は、DMDL EditorXの機能を使ってバッチクラスを作ってみる。
| 項目 | 説明 | 例 |
|---|---|---|
| Source folder | ソースのディレクトリー。 | wordcount2/src/main/java |
| Package | パッケージ名。 | com.example.batch |
| Name | クラス名。 | WordCountBatch |
| 項目 | 説明 | 例 |
|---|---|---|
| comment | バッチの日本語名。 クラスのJavadocや@Batchのコメントに使われる。 |
ワードカウントバッチ |
| Batch name | バッチ名。 「 @Batch(name="バッチ名")」として使われる。つまり、YAESSの引数として使用するもの。 個人的には、クラス名と同じ名前にしてしまっている。 |
WordCountBatch |
| parameter | バッチ引数がある場合、その定義。 ここを書かなくても実行は出来る。 |
package com.example.jobflow; import com.asakusafw.vocabulary.batch.Batch; import com.asakusafw.vocabulary.batch.BatchDescription; import com.example.jobflow.WordCountJob;
/**
* ワードカウントバッチ
*/
@Batch(
name = "WordCountBatch",
comment = "ワードカウントバッチ",
strict = false)
public class WordCountBatch extends BatchDescription {
@Override
public void describe() {
run(WordCountJob.class).soon();
}
}