JSweet で自作アノテーションを活用する

この投稿では、前回に引き続き、アノテーションを含む Java コードで書かれた処理やメタデータ取得を、どうしても JavaScript で再現して利用したい場合の一つの解決策を示します。

今回も JSweet というツールを利用します。
前回は簡単に使えましたが、今回のようにアノテーションを残して処理した場合に面倒な部分がありました。

サンプルプロジェクトはこちら

やること

とあるプロジェクトの構成として、以下のものがあったとします。

  • src
    • java/main/bar/Example.java
      呼び出される Java クラス Example のコード
      外部モジュールにある @foo.SpecialType@foo.SpecialField でアノテートされている
    • src/js/index.js
      Example を利用するような JavaScript コード

何も考えずアノテーションを含む .jar ファイルや Java コードを加えて JSweet でトランスパイルしても、アノテーションの情報は消えてしまいます。考えてみれば、Java のアノテーションはメタ情報を付与するよう使うのに対し、 JavaScript のデコレータは対象を変換するよう使うので、無理はないかもしれませんね。
(Java のアノテーションプロセッサを使ったり reflect-metadata を使ったりすることで、お互い似た機能にできるとは思いますが。)

今回はこの辺りの問題を解決します。 @SpecialType が付くクラスの中の @SpecialField が付くフィールドのメタ情報の一覧を取得するサンプルを示します。リフレクションをかなり意識しました。

Example.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package bar;

import foo.SpecialType;
import foo.SpecialField;

@SpecialType
public class Example {
@SpecialField
private Integer hoge;

@SpecialField("qux")
private Integer fuga;

@SpecialField(value = "baz", count = 1)
private Integer piyo;

private Integer xxx;
}

(何らかのメソッドを実行すると、 {"hoge": {value: "special", count: 0}, ... , "piyo": {value: "baz", count: 1}} のように返ってくる。)

解決法

いろいろ考えられますが、今回はこんな手段をとります。

  1. JSweet でトランスパイルする専用のアノテーション Java コードを用意する
  2. JSweet で TypeScript へトランスパイル
  3. アノテーション Java コードから生成される TypeScript を、同等の機能を持ったデコレータ TypeScript コードに置き換える
  4. TypeScript でデコレータ利用のフラグを使ってコンパイルする

直接デコレータに変換できるような Java コードを考えるのも手ですが、なかなか書き辛そうでした。

Java アノテーション to TypeScript デコレータ 変換

まずは、アノテーションがトランスパイル後にデコレータとして残せるよう、必要な準備・構成を用意しておきます。

JSweet 準備

最初に JSweet を使えるようにする必要があります。ここは前回とほとんど同じですが、再掲します。

http://repository.jsweet.org/artifactory/libs-release-local/org/jsweet/ より、 “jsweet-core” と “jsweet-transpiler with-dependencies” の最新版の .jar をダウンロードしてローカルに保存してください。(執筆時の最新版は、それぞれ “5-201726” と “2.0.1” です"。)
続いて、 NPM スクリプトを使う場合は spawn モジュールにより JSweet トランスパイラを動かします。
‘src/java/main’ と、 JSweet が解釈できる( TypeScript に変換後も残る )アノテーションの置き場 ‘dev/java-annotations’ に含まれるコードを入力とし、 commonjs 形式の TypeScript コードだけを出力するように設定します。
このときのコードは次のようになります。(“node-java” を使う場合は、ログ出力の取扱が面倒でした。)

watch.js
10
11
12
13
14
15
// This is for Windows
const jsweetProcess = spawn('java', ['-cp', 'dev/jsweet-jar/jsweet-core-5-20170726.jar;dev/jsweet-jar/jsweet-transpiler-2.0.1-jar-with-dependencies.jar',
'org.jsweet.JSweetCommandLineLauncher', '--input', './src/java/main/bar:./dev/java-annotations',
'--module', 'commonjs', '--tsOnly', '--tsout', JSWEET_OUTPUT_DIR, '--watch'], {
shell: true
});

(前回との違いは './src/java/main''./src/java/main/bar:./dev/java-annotations' に変わったことくらいです。)

JSweet Candy の準備

JSweet ではアノテーションの情報が消えると前述しました。これができないのでは( Angular 向けなどは特に)役に立たないため、 JSweet Candy という仕組みが用意されています。
言わば、アノテーションとデコレータの橋渡しをしてくれるような Java パッケージ・コードです。

有名な OSS の JS ライブラリ用には Candy がすでに用意されており、これは簡単に利用できます。ただし、自作アノテーションを使う場合には、当然ながら Candy も自作する必要があります。
どうにも、 JSweet は JS ライブラリを使うコードを Java で書くのが目的なようで、今回のような Java プログラムを使う Java コードをトランスパイルするケースについて説明がありません。
JS ライブラリ向けには、特別な “def” から始まる名前空間を使って定義するようですが、今回はそうでない名前空間下のアノテーションを使わねばなりません。

JS ライブラリ用の JSweet Candy は、

  • “def” から始まる空間に定義
    JSweet が特殊扱いする
  • デコレータの実装は不要 JS ライブラリに含まれるため

という状況を想定しているのに対して、今回のケースでは

  • 名前空間は任意
    今回の例では “foo” 以下のアノテーション
  • デコレータの「実装」が Globals クラスの static メソッドとして必要
    後述する @Decorator が付与されているアノテーションには必要となります。(JSweet のエラーメッセージより。 “def” 以下では不要と思われる。)

となっているため、少し頭を捻らなければなりません。
この状況で採れる手段としては、

  1. TypeScript へ変換後に残るデコレータまですべて Java コードにより書き上げる
  2. デコレータの定義だけ Java コードで作って実装は TypeScript にする

がありそうです。解決法に書いたように、今回は 2 の方法をとります。
なお、どちらもデコレータに変換したいアノテーションが別ファイルか .jar に存在するなら、トランスパイル時には未定義にも重複定義にもならないよう設定できるため、JSweet 用に別のアノテーションを再定義しても問題ありません。

前置きが長くなりましたが、例として ‘foo/SpecialType.java’‘foo/SpecialField.java’ を用意しています。
後者のほうが複雑なため、そちらを紹介します。

foo/SpecialField.java
1
2
3
4
5
6
7
8
9
10
11
12
13
package foo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

import jsweet.lang.Decorator;

@Decorator
@Target(ElementType.FIELD)
public @interface SpecialField {
public String value() default "special";
public int count() default 0;
}

通常のアノテーションの定義に加え、 jsweet.lang.Decorator が加えられています。
JSweet でアノテーションに新たに加えるのはこれだけですが、 Globals 以下の static メソッドも作らなければなりません。
実装例として ‘foo/Globals.java’ を作りました。

foo/Globals.java
1
2
3
4
5
6
package foo;

public final class Globals {
public static void SpecialType(def.js.Object args) {}
public static void SpecialField(def.js.Object args) {}
}

アノテーションがデコレータに変換されたときの引数は Json として渡されます。引数が無い場合は第一引数が空か、関数でないデコレータに変換されます。(仕様が謎)
とりあえず、アノテーションと同名で関数が定義されていれば、内容や引数リストは問わないようです。

TypeScript デコレータの準備

ここまでの工程でアノテーションがデコレータとして残るような変換は設定できたので、次にデコレータ本体を記述します。

デコレータの実装法は 公式ドキュメント に譲ります。

デコレータの記述

デコレータには引数付きの呼び出しとそうでないものがあります。
JSweet で生成されるコードは、どちらも利用する可能性があり、両方のパターンを受け付けるよう準備しなければなりません。

この例では @SpecialField デコレータが “reflect-metadata” を使ってメタデータを定義し、 @SpecialType デコレータが対象クラスに新しい get プロパティを生やしてそれを取得できるようにしています。
メタデータを使ったロジックの詳しい実装は ‘foo/Core.ts’ を参考にしてみてください。

デコレータとしてどのような関数定義が必要かはデコレータの公式ドキュメントを見ればわかりますが、それを発展させて、アノテーションの初期値をデコレータでも再現しています。
実装例として ‘foo/SpecialType.ts’‘foo/SpecialField.ts’ がありますが、例のごとく後者のほうが複雑なのでそちらを紹介します。
@SpecialType が付与されたクラスでは、このデコレータの処理を経ることで、 __specialFields という、メタデータの配列を取得するプロパティが生え、 @SpecialField の一覧が取得できます。(これは ‘foo/Core.ts’ に記述)

foo/SpecialField.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {SpecialFiledProcess as process} from "./Core"

const defaultValues = {value: "special", count: 0};

export function SpecialField(targetPrototype: any, propertyKey: string): void;
export function SpecialField(args_: { value?: string; count?: number }): (targetPrototype: any, propertyKey: string) => void;
export function SpecialField(arg1: any, arg2?: string): any {
if (typeof arg2 === 'string') {
const args = defaultValues;
const targetPrototype: any = arg1;
const propertyKey = arg2;
process(args, targetPrototype, propertyKey);
} else {
{
const args = {...defaultValues, ...arg1};
return (targetPrototype: any, propertyKey: string) => process(args, targetPrototype, propertyKey)
}
}
}

ここでは関数のオーバーロードを行っています。
5 行目は @SpecialField とデコレートする場合、6 行目は @SpecialField({}?) とデコレートする場合で、 7行目はどちらも許容するような関数の実装です。

第 2 引数が文字列として存在すれば @SpecialField という形式で呼ばれていることがわかり、このときは Java 側で定義しているデフォルト値を詰めてからメタデータ付与処理を行います。
そうでなければ @SpecialField({}?) と呼ばれているので、足りない引数をデフォルト値で埋め、メタデータ付与処理を包んで関数化し、その関数を返します。
(デコレータの処理に従っているだけですが、タイプセーフに書くのは結構複雑です。)

同期

生成されるファイルには、トランスパイル対象だった ‘bar/Example.ts’ に加えて、デコレータを定義だけした ‘foo/SpecialType.ts’ と ‘foo/SpecialField.ts’ (どちらも前述でない)が含まれています。これらが ‘tmp/jsweet’ 以下に生成されますが、 ‘bar/Example.ts’ が相対パスで各デコレータを参照しているため、実装を入れ替えるのは少々面倒です。いくら本物の実装で上書きしても、 JSweet の watch モードでは遠慮なく上書きされてしまうからです。

そこで、 ‘watch.js’ L34-67 では “chokidar” により、 ‘bar’ 以下のファイルだけを ‘target/generated/ts’ 以下へ移動してそこにデコレータの実装を加え、ディレクトリの整合性を持たせています。

JSweet も “chokidar” も watch モードで動かすようにすれば、 Java コードが正しい限りは常に正しい TypeScript コードとして使える状態になっています。

TypeScript ビルド設定

TypeScript をビルドするために、今回は通常の “typescript” npm パッケージだけを使います。 Webpack でもできますが、テーマから外れるので取り上げません。
どちらにしても、デコレータは実験的機能なため、 ‘tsconfig.json’ の設定には気をつけなければなりません。

tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"compilerOptions": {
"noEmitOnError": true,
"noImplicitAny": true,
"target": "es6",
"module": "commonjs",
"experimentalDecorators": true,
"suppressImplicitAnyIndexErrors": true
},
"exclude": [
"node_modules",
"tmp/jsweet",
"dev"
]
}

デコレータ利用設定に加え、 JSweet の出力先やデコレータの実装の同期元フォルダを “exclude” するのも忘れないでください。
こうすると、 ‘target/generated/ts’ 以下の .ts ファイルはすべて .js ファイルにコンパイルされ、 JavaScript コードから直接呼び出せるようになります。

今回は TypeScript のコンパイルも watch モード化することにします。公式ドキュメント から少し削って、 ‘watch.js’ L69-109 のように書いています。
ただし、多くの場合は Webpack も利用しているでしょうから、 “ts-loader” などを使うのも良いでしょう。

結果表示

サンプルプロジェクトで yarn run build-watch を実行して watch を開始した後で、 node src/js/index.js を実行してみてください。

src/js/index.js
1
2
3
const {Example} = require("../../target/generated/ts/bar/Example.js");

console.log(JSON.stringify(Example.__specialFields, null, 2));

何もいじっていないときの結果はこんな感じです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"hoge": {
"value": "special",
"count": 0
},
"fuga": {
"value": "qux",
"count": 0
},
"piyo": {
"value": "baz",
"count": 1
}
}

最初の方に掲載した ‘bar/Example.java’ でアノテーションと共に指定した値と同じになっていると思います。
この状態でアノテーションの引数を変更するとトランスパイルが走り、 node src/js/index.js の実行結果も変わるのが確認できると思います。

まとめ

  • JSweet と自作 JSweet Candy を使い、自作 Java アノテーションで書かれたメタデータを TypeScript のデコレータに変換する例を紹介しました。
  • TypeScript デコレータのタイプセーフな実装例を示しました。
  • Java アノテーションによるメタ情報を、デコレータと “reflect-metadata” により JavaScript コードから取得するコードを書きました。
  • JSweet の watch モードと “chokidar” 、 TypeScript のインクリメントコンパイルを利用して、 Java コードがすぐに JavaScript コードに変換されるよう構成しました。