読者です 読者をやめる 読者になる 読者になる

Java と TypeScript をタイプセーフにツナグ

サーバサイドを Java、クライアントを TypeScript で作った場合、タイプセーフに通信できないかという試みです。 考えられる選択肢としましては、 Thrift とか、いっそサーバサイド Scala, クライアント Scala.js にするっていうのもあるとは思います。 今回、Java の Entity から TypeScript のクラスやインターフェースを自動生成できるようになれば、JSONの通信がタイプセーフになるのではと思い試してみました。 EntityCodeGenerator というクラスを一つだけ持つライブラリを作成しました。

サーバ (Java)

まずは、サーバ側のコード

import io.github.chibat.selva.App;
import io.github.chibat.selva.server.Server;
import io.github.chibat.util.typescript.EntityCodeGenerator;

public class ExampleApp implements App {

    @Override
    public void init() {

        resource("/").get(req -> forward("/calc.html"));

        resource("/calc").get(req -> {
            Input input = req.bean(Input.class);
            Result result = new Result();
            result.add = input.arg1 + input.arg2;
            result.subtract = input.arg1 - input.arg2;
            return json(result);
        });

    }

    public static class Input {
        public Integer arg1;
        public Integer arg2;
    }

    public static class Result {
        public Integer add;
        public Integer subtract;
    }

    public static void main(String[] args) {

        new EntityCodeGenerator()
            .readClass(Input.class)
            .asClass()
            .writeFile("src/main/typescript/entities.ts"); // (1)

        new EntityCodeGenerator()
            .readClass(Result.class)
            .asInterface()
            .writeFile("src/main/typescript/typings/entities.d.ts"); // (2)

        new Server().add(ExampleApp.class).listen(); // (3)
    }
}

フレムワークで、Selva を使っています。init メソッドでリクエストに対するレスポンスの処理を記述しています。 リクエストパラメータの引数2つを足し算、引き算してレスポンスするだけです。リクエスト用に Input, レスポンス用に Result の Entity を定義しています。 で、main クラスの(1)で Input クラスに対応する TypeScript の クラスのコードをファイル entities.ts に出力、 (2)で Result クラスに対応する TypeScript のインターフェースのコードを entities.d.ts に出力しています。 (3)でアプリケーションサーバの起動が起動され Webブラウザが起動し、画面が開きます。 実際、TypeScript のコード出力は、Javaコンパイルのタイミングでできないかと考えますが。。

クライアント (TypeScript)

生成されたコード

前述のサーバ起動で生成されるコードが以下です。

entities.ts

module ExampleApp {
    export class Input {
        arg2: number;
        arg1: number;
    }
}

entities.d.ts

declare module ExampleApp {
    export interface Result {
        add: number;
        subtract: number;
    }
}

レスポンス用の Entity は型だけ分かれば十分なので、クラスではなくインターフェースとして出力しています。

calc.ts

/// <reference path="typings/jquery/jquery.d.ts" />
/// <reference path="typings/entities.d.ts" />

$(document).ready(function(){
    $('#calcBtn').click(() => {
        var input = new ExampleApp.Input();
        input.arg1 = $('#arg1').val();
        input.arg2 = $('#arg2').val();
        $.ajax({
            url: '/calc',
            data: input,
            dataType: 'json',
            success: (result: ExampleApp.Result) => {
                $('#add').text(result.add);
                $('#subtract').text(result.subtract);
            },
        });
    });
})

calc.html

いろいろ省略してますが、HTMLのコードです。

Arg1:  <input id="arg1" type="text" value="" /><br/>
Arg2:  <input id="arg2" type="text" value="" /><br/>
<button id="calcBtn" type="button">Calc</button><br/>
Add: <span id="add"></span>
Subtract: <span id="subtract"></span>

Maven Repository

EntityCodeGenerator 利用のための build.gradle

repositories {
  jcenter()
}
dependencies {
  compile 'io.github.chibat:util-typescript:0.0.+'
}

まとめ

これで 少しではありますが、Java, TypeScript がタイプセーフに繋がったかなと。 EntityCodeGenerator は、Jackson の ObjectMapper 依存で、それを使ってるフレームワークなどに組み込めるかなと考えています。一応ネストしたプロパティーとかにも対応しています。 レスレポンスの型にジェネリクス使ったりするかもしれませんが、その場合は、extends して型に名前を付けてあげないとコードが生成できません。 メソッドシグネチャでリクエスト、レスポンスの型が明示的に宣言されているフレームワーク、Spring MVC とか Jersey だったらさらに、RPCに近いコードも吐き出せそうだなと考えます。

以上です。