素早さのJSON API

JSON戻すActionの実装

JsonResponseを戻り値に

Executeメソッドの戻り値が、JsonResponse 型になり、asJson() で Bean を戻します。

e.g. JsonResponse and asJson() @Java
@Execute
public JsonResponse<SeaLandResult> index() {
    ...
    SeaLandResult result = new SeaLandResult();
    result.piari = "piary";
    result.bonvo = "yage";
    ...
    return asJson(result); 
}

JsonResponse には Generic の型として、JSON に変換する Result クラスを指定します。 (JSONを戻す実現要件としては本当は不要ですが、仕組みの中で型を判別できることで、LastaDocに載せたりバリデーションのミスをチェックしたりなどの様々なメリットを享受することができます。 なのでプログラム上の明示という意味合いも兼ねて指定するようにしています)

Jsonに変換される Bean クラスは、...Result と命名するのがLastaFluteのオススメです。 (制約ではありませんが、API の結果であることがわかるように明示しています)

HtmlResponseと混在してもOK

JSON APIというよりかはAJAX的な使い方をする場合、一つのActionクラスの中に HtmlResponse と JsonResponse のメソッドが混じってもOKです。

もともと HtmlResponse の場合は、Actionは一つの画面に付き一つ作るというのがオススメです。 JsonResponoseが必要な画面でも、API専用のActionというように分けず、同じActionにあった方が再利用もしやすく、管理しやすいかと思います。 (もちろん、ケースバイケースのさじ加減が必要ですが、少なくとも仕組み的な制約はないということです)

API用のvalidateApi()

JsonResponseの場合は、バリデーションのメソッドとして validateApi() を使います。

e.g. validateApi() for form in Action class @Java
@Execute
public JsonResponse<SeaLandResult> index(SeaLandForm form) {
    validateApi(form, messages -> {});
    ...
}

普通の validate() に比べて、バリデーションエラー時の制御をする第三引数のLambdaがありません。

JsonResponseのときは、Actionごとつどつど制御するのではなく、アプリ全体で共通的な処理をすることが想定されるため、 ApiFailureHookhandleValidationError() にてエラー用のJSONを戻します。 (ちなみに、業務例外も同様に、ApiFailureHook で制御されます)

ちなみに、もしそのアプリがJSON APIサーバーで全部 JsonResponse というときは、BaseAction が implements しているインターフェースを LaValidatableApi に変更すると、validate() 自体が JsonResponse 用のメソッドになります。(これは一番最初に決めましょう)

Resultのプロパティはpublicフィールド

Form や Body と同様に、Web周りの Bean は、publicフィールド をベースにするのが LastaFlute のスタイルです。 特に JsonResult は、仮に Getter/Setter を作ったとしても、privateフィールドが直接参照されますので、フィールドベースにするのがよいでしょう(これは Gson の特徴)

e.g. Required annotation in Result class @Java
public class SeaLandResult {

    public Integer piari;

    public String bonvo;

    ...
}

Resultのプロパティはネイティヴ型

String だけじゃなく、Integer, Long, LocalDate, Boolean, CDef.Xxx など、対応するネイティヴ型をそのまま定義することができます。

e.g. native type property in Bean class @Java
public class SeaLandResult {

    public Integer piari;

    public LocalDate bonvo;

    public Boolean dstore;

    public CDef.MemberStatus amba;

    ...
}

ネイティヴ型は基本的にWrapper型

プロパティの型に、int, long や boolean などの primitive 型も定義できますが、オススメしません。

後述される Validator Annotation との相性を考えると、全体的に プロパティの型にはWrapper型を使う というポリシーがよいでしょう。 (primitive 型だと、setし忘れても0やfalseなどのデフォルト値が入ってしまうので必須チェックできない)

LocalDate型の日付フォーマット

LocalDate などの日付型は、yyyy-MM-dd, yyyy-MM-dd'T'HH:mm:ss.SSS (ISOの日付フォーマット) で解釈されます。一応、これを調整しようと思えばできます。

ただ、日付のフォーマットをサーバーで解決するのか?クライアントで解決するのか? これはプロジェクトのポリシーとして決まっているべきことです。 ディベロッパーが勝手に判断してはいけないので、迷う場合は必ず確認をしましょう。

サーバーで決定
何かしらの方法で、LocalDateのフォーマットを変更
クライアントで決定
そのまま転送用の日付フォーマットで通信(ISOのデフォルトフォーマット)

もし、サーバーで決定するのであれば、どのレベルで調整するのか次第でやり方が変えましょう。

ぜんぶ調整
option でネイティヴ型のフォーマットを変更
そこだけ調整
@JsonDatePatternアノテーションで変更

いずれにせよ、国際化対応などで独自の変換ロジックを必要としない限り、日付を String で定義する必要はないでしょう。LocalDate, LocalDateTime を積極的に使っていきましょう。

JsonResultもバリデーション

Form や Body と同様に、asJson() に入れる JsonResult もバリデーションすることができます。 プロパティに @Required などの Validator Annotation を付けると、実際にバリデーションが実行されます。 この場合のバリデーションエラーは、サーバーサイドのバグやデータの不整合が想定されますので、システムエラーとして扱われます。

JsonResultのバリデーション
e.g. Required annotation in Result class @Java
public class SeaLandResult {

    @Required
    public Integer piari;

    @Required
    public String bonvo;

    ...
}

LastaDocにもアノテーションが載るので、フロントサイドのディベロッパーに項目の特徴を自然と伝えることができます。 ドキュメント的な意味合いとアサート的な意味合いの両方のメリットを享受します。

ただ、やり過ぎるとキリが無いので、とりあえず @Required や @NotNull だけは付けておく、というのがオススメです。 (それだけでも、サーバーサイドの "設定し忘れバグ" を防ぐことができますので)

一方で、実装上の三大チェックポイントは抑えておきましょう。

primitive型プロパティ
気をつけて!int や boolean の @Required
ネストBeanプロパティ
忘れないで!ネストBean には @Valid
Listプロパティ
思い出して!List の @Required は一件以上

JSON受け取るActionの実装

JsonBodyクラスを引数に

JSON を RequestBody で直接受け取る場合は、Formクラスではなく、Bodyクラスを使います。

e.g. JsonBody instead of form @Java
@Execute
public JsonResponse<SeaLandResult> index(SeaLandBody body) {
    ...
}

Bodyクラスは、Formと同様にpublicフィールドベースで、ネイティヴ型で定義します。 Validator Annotation も付けることができます。この辺の要領は、JsonResult と全く同じです。

e.g. validator annotation in form @Java
public class SeaLandBody {

    @Required
    public Integer piari;

    @Required
    public LocalDate bonvo;
}

Bodyのバリデーション

Bodyクラスでも、バリデーションは Form と全く要領は同じです。

その Action の戻りが HTML なら validate() ですし、JSON なら validateApi() です。 (JSON Bodyを受け取りながらHTMLを戻すというのはあまり考えられないので、Bodyならほとんど validateApi() だと想定しています)

Bodyの一律Filter

Bodyで受け取るJSONの項目にフィルター処理をかけたい場合は、JsonResourceProvider の provideOption() にて、JsonMappingOption の filterSimpleTextReading() で設定します。

厳密には、JsonManagerの読み込み処理のすべてに適用されるので、Bodyだけじゃなく JsonManager の fromJson() でもFilterされます。

JSON を LastaDocで伝える

JSON APIでアプリを作成するときは、フロントサイドのディベロッパーに "どんなJSONが戻るのか?" をスムーズに伝えるのに苦労します。 手動でドキュメントを作っても、開発の荒波の中でどんどん実装とズレていったりなど、なかなかうまく運用していくのは大変です。

LastaFluteでは JsonResult から LastaDoc というように、実際に作成したJsonResultクラスをパースして、ドキュメントを自動生成します。 ("DBからSchemaHTML" というDBFluteの発想と同じような感じ)

JSONスタイルのExecuteメソッド詳細

ネイティヴ型のマッピング

例えば、LocalDate は、デフォルトでは ISO の yyyy-MM-dd で解釈されます。

これらは、JsonResourceProviderprovideOption() にて調整できます。option にある様々なメソッドで指定します。 これ自体はインターフェースなので、実装クラスは Maihama プロジェクトであれば、MaihamaJsonResourceProvider というクラスになり、AssistantDirectorで登録されます。

e.g. format date delimited by slash at provideOption() @Java
public class MaihamaJsonResourceProvider implements JsonResourceProvider {

    @Override
    public JsonMappingOption provideOption() {
        JsonMappingOption option = new JsonMappingOption();
        option.formatLocalDateBy(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        option.formatLocalDateTimeBy(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
        return option;
    }
}

JS/APIひとまとめパターン

HTML/JavaScriptのリソースと JSON API を ひとつのサーバーにひとまとめに するパターン、つまり、一つのwarファイルに両方のリソースを入れて同じドメインで運用する場合のコツについて。

JSON API の URL には固定の /api を

HTML/JavaScriptのリソースと JSON API へのリクエストを区分けするために、JSON API の方の URL には固定の /api を付与するようにします。 (別に /api でなくてもいいです。例えばということで)

HTML/JavaScript
/api ではないURL e.g /index.js
JSON API
/api で始まるURL e.g. /api/product/list

ApiProductListAction にならないように

そうすると、Actionクラス名には必ず Api という prefix をつけることになります。パッケージも app.api.xxx というように、api パッケージを挟むことになります。固定的にすべての Action に Api が付くことになるので、さすがに除去したいです。

そこで、ActionAdjustmentProvider の customizeActionMappingRequestPath() をオーバーライドして、ActionのマッピングURLから /api を除去します。すると、パッケージもクラス名も、/api に対応する名前をつける必要はありません。 (/api/product/list でも、app.product.ProductListAction でOK )

もし、Maihamaプロジェクトであれば、mylasta.direction.sponsor.MaihamaActionAdjustmentProvider になります。

e.g. remove /api prefix from action mapping URL @Java
public class MaihamaActionAdjustmentProvider implements ActionAdjustmentProvider {

    @Override
    public String customizeActionMappingRequestPath(String requestPath) {
        // action class name does not need 'Api' prefix
        return DfStringUtil.substringFirstRear(requestPath, "/api");
    }
}

/api でないURLでAction探さないように

必須ではありませんが、パフォーマンス考慮のために、/api でないURL で Action クラスを探しにいかないようにするとよいでしょう。 もともと .js, .css, .html などの拡張子の付いた URL なら Action クラスを探しにいかないですが、/operate/maihamadb/ というような URL だと探しに行ってしまいます。 (探しに行って見つからなければ JavaScript 側でつかまえて動作はしますが、無駄な処理が走ります)

ActionAdjustmentProvider の isForcedRoutingExcept() をオーバーライドして、/api ではないものを除外するようにします。

e.g. except non /api request as action mapping @Java
public class MaihamaActionAdjustmentProvider implements ActionAdjustmentProvider {

    @Override
    public boolean isForcedRoutingExcept(HttpServletRequest request, String requestPath) {
        // of course, request to angular resources does not need routing
        // (requestPath might contain /dbflute-intro/ so use contains())
        return !requestPath.contains("/api");
    }
}

二つ足し合わせると...

mylasta.direction.sponsor.MaihamaActionAdjustmentProvider は、それら二つの実装を足し合わせるとこのような実装になります。(メソッドの定義順序は、インターフェースに合わせています)

e.g. adjustment for JS/API together pattern @Java
public class MaihamaActionAdjustmentProvider implements ActionAdjustmentProvider {

    protected static final String API_URL_PREFIX = "/api"; // to be separated from angular request

    @Override
    public boolean isForcedRoutingExcept(HttpServletRequest request, String requestPath) {
        // of course, request to angular resources does not need routing
        // (requestPath might contain /dbflute-intro/ so use contains())
        return !requestPath.contains(API_URL_PREFIX);
    }

    @Override
    public String customizeActionMappingRequestPath(String requestPath) {
        // action class name does not need 'Api' prefix
        return DfStringUtil.substringFirstRear(requestPath, API_URL_PREFIX);
    }
}

DBFlute Introが参考実装に

DBFlute Intro がまさしく "JS/APIひとまとめパターン" なっていますので参考にしてみてください。

JS/API別サーバーパターン (CORS)

互いに独立しているので、クラスやURLなどで調整することは基本的にありません。

CORS対応

ただ、CORS (Cross-Origin Resource Sharing) 対応をする必要があるでしょう。

prepareWebDirection() にて、directCors() で CorsHook を指定します。

Exampleプロジェクト Maihama の Hangar にてExample実装されています。 マルチプロジェクトなので、prepareWebDirection() をオーバーライドして、superを呼びつつ CORS の設定をしています。

e.g. CORS for JS/API separated pattern @Java
/**
 * @author jflute
 */
public class HangarFwAssistantDirector extends MaihamaFwAssistantDirector {

    @Override
    protected void setupAppConfig(List nameList) {
        nameList.add("hangar_config.properties"); // base point
        nameList.add("hangar_env.properties");
    }

    @Override
    protected void setupAppMessage(List nameList) {
        nameList.add("hangar_message"); // base point
        nameList.add("hangar_label");
    }

    @Override
    protected ListedClassificationProvider createListedClassificationProvider() {
        return new HangarListedClassificationProvider();
    }

    @Override
    protected void prepareWebDirection(FwWebDirection direction) {
        super.prepareWebDirection(direction);
        final String allowOrigin = "http://localhost:3000"; // #simple_for_example should be environment configuration
        direction.directCors(new CorsHook(allowOrigin)); // #change_it
    }
}

allowOrigin は、環境ごとに変わるのであれば [app]_env.properties に定義すると良いでしょう。

REST APIにするには?

HTTPメソッドごとにマッピングする方法

例えば、/sea/land/ というURL で、GET のときと POST のときで処理を切り替えるためには、(SeaActionを前提として)Executeメソッド名を以下のようにします。

GET
get$land()
POST
post$land()

indexでも使えます。例えば、/sea/ の GET であれば get$index() となります。

Pathパラメーターの場所を入れ替える方法

例えば、/sea/3/land/ というように、Pathパラメーターを固定のキーワードの途中に挟むためには、urlPattern で @word を使います。メソッド名に相当するキーワードの位置を調整できます。

e.g. word in urlPattern for REST API @Java
public class SeaAction extends DocksideBaseAction {

    @Execute(urlPattern="{}/@word") // /sea/3/land/
    public JsonResponse<...> get$land(Integer seaId) {
    }
}

例えば、/sea/3/land/4/piari というように、メソッド名に相当するキーワードが二つある場合は、メソッド名の中でキャメルケースで含めて、@word を二つ使います。

e.g. double words in urlPattern for REST API @Java
public class SeaAction extends DocksideBaseAction {

    @Execute(urlPattern="{}/@word/{}/@word") // /sea/3/land/4/piary
    public JsonResponse<...> get$landPiari(Integer seaId, Integer landId) {
    }
}

一つのURLの要素にキャメルケース、例えば seaLand を含めることはできません。/sea/land/ と分解するか、/sealand/ と小文字にするかの対応が必要です。そもそも "RESTっぽいURL" を目指しているのであれば、そもそもキャメルケースで要素を表現すること自体があまり馴染まないと思うので、サポートする予定も今のところありません。

(一方で、REST自体、単純な CRUD の JSON API であればフィットしやすいですが、業務ロジックがゴリゴリ入った JSON API だとけっこう無理が出てきてしまうと考えているので、サポートの優先度は下げています)

TODO jflute この機能はもっと使いやすさを追求していかなければならない (Actionクラスのwordを...)

バージョンをURLに埋め込む方法

例えば、/v1/sea/ という URL で、v1 はバージョンだとしたときは、ActionAdjustmentProvider の customizeActionMappingRequestPath() にて調整します。

このメソッドでバージョンを抜き出して、マッピングのためのURLは filter し、抜き出したバージョンはリクエスト属性に入れるなりして Action に引き継いで利用するとよいでしょう。

忘れられないApiFailureHook

JsonResponse の Action にて バリデーションエラー業務例外システム例外が発生したときは、 Actionごとつどつど制御するのではなくアプリ全体で共通的な処理をすることが想定される ため、共通部分で統一的なJSONを戻します。

それを司るのが、ApiFailureHook です。これ自体はインターフェースなので、実装クラスは Maihama プロジェクトであれば、MaihamaApiFailureHook というクラスとなり、AssistantDirectorで登録されます。

Swaggerを使いましょう

気軽に叩けない JSON API であれば、Swagger を使って動作確認をしましょう。 (もちろん、UnitTestも書きますが、最終的にリクエストを飛ばして確認もしたいものです)

JSONデザイン (どんなJSONを戻す?)

大切です。