素早さのJSON API
- JSON戻すActionの実装
- JSON受け取るActionの実装
- JSON を LastaDocで伝える
- ネイティヴ型のマッピング
- JS/APIひとまとめパターン
- JS/API別サーバーパターン (CORS)
- RESTful APIにするには?
- 忘れられないApiFailureHook
- Swaggerを使いましょう
- JSONデザイン (どんなJSONを戻す?)
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ごとつどつど制御するのではなく、アプリ全体で共通的な処理をすることが想定されるため、 ApiFailureHook の handleValidationError() にてエラー用の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 を付けると、実際にバリデーションが実行されます。 この場合のバリデーションエラーは、サーバーサイドのバグやデータの不整合が想定されますので、システムエラーとして扱われます。
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の発想と同じような感じ)
ネイティヴ型のマッピング
例えば、LocalDate は、デフォルトでは ISO の yyyy-MM-dd で解釈されます。
これらは、JsonResourceProvider の provideOption() にて調整できます。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ひとまとめパターン" なっていますので参考にしてみてください。
例えば、Swaggerを使っている場合に、SwaggerUI上でのURLに /api を付けるなど、DBFlute Introの実装を参考にしてみてください。
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 に定義すると良いでしょう。
RESTful APIにするには?
専用の機能を提供しています。
e.g. RESTful action for /products/ @Java
@RestfulAction
public class ProductsAction extends ShowbaseBaseAction {
@Execute
public JsonResponse<List<ProductsResult>> get$index(ProductsSearchForm form) {
...
@Execute
public JsonResponse<ProductsResult> get$index(Integer productId) {
...
@Execute
public JsonResponse<Void> post$index(ProductsPostBody body) {
...
忘れられないApiFailureHook
JsonResponse の Action にて バリデーションエラー、業務例外、システム例外が発生したときは、 Actionごとつどつど制御するのではなくアプリ全体で共通的な処理をすることが想定される ため、共通部分で統一的なJSONを戻します。
それを司るのが、ApiFailureHook です。これ自体はインターフェースなので、実装クラスは Maihama プロジェクトであれば、MaihamaApiFailureHook というクラスとなり、AssistantDirectorで登録されます。
Swaggerを使いましょう
気軽に叩けない JSON API であれば、Swagger を使って動作確認をしましょう。 (もちろん、UnitTestも書きますが、最終的にリクエストを飛ばして確認もしたいものです)
JSONデザイン (どんなJSONを戻す?)
大切です。