Actionの作り方 (JSONスタイル)

実装の流れ

ちょっと前提

ここでは、docksidestage.org というドメイン、つまりパッケージは org.docksidestage を想定し、harbor というアプリ名であることを想定して説明していきます。

ハンズオンで Action を作ってみよう!

LastaFluteでの実装のやり方を学んでいる最中であれば、まずは、Exampleプロジェクト harbor で、実際にクラスを作りながらやっていってもよいでしょう。コードを真似て書きながら(コピーでもOK)、画面を作っていくと流れとコツがわかってくるかと思います。 (それができるようにドキュメントを作っています)

harborプロジェクトは、組み込みの H2 Database を使っているので、データベースのインストールは不要です。 ただし、clone した後、ReplaceSchema を叩くの忘れないように。(Quick Trial の欄を参考に)

まずはラフスケッチ実装

一行一行、しっかり書いていくのではなく、まずは流れを実装して、全体像を構築してから細かいところを固めていくというスタイルをオススメしています。 最初の一ターン目はラフスケッチです。わからないことがあったら保留して次に進み、全体像を把握できる状態になってからつまづいたところを解決していきましょう。

  1. URLを決める
  2. Actionクラスを作る
  3. Executeメソッドを準備
  4. Formを作る
  5. JSON Resultを作る
  6. Executeメソッドを実装
  7. ラフスケッチできた

URLを決める

まずは、URLを決めましょう。

ここでは /lido/sea/3?pay=HAN (/lido/sea/[商品ID]?pay=[支払方法]) というGETリクエストの対応する Action を作ることにしましょう。

/3 の部分はPathパラメーターで商品IDと想定し、ログイン会員(自分)とフォローしている会員の、指定された商品に対応する購入一覧今のあなたの気分 を JSON で戻すとします。もし、payが指定されていたら、指定された方法で支払された購入に絞ります。(maihamadb を参考に、よくわからなければ後で)

Actionクラスを作る

Actionの名前を決める

/3 はPathパラメーターとするならば、Actionを識別する部分が /lido/sea/ となり、

  • A. LidoAction#sea()
  • B. LidoSeaAction#index()

の、どちらかとなります。(規約)

最後の sea という要素の下にさらに分割された要素が来るのか次第です。 (とりあえず漠然と決めて、あとでやっぱりこっちだった、となればリファクタリングすればOKです。変更してもURLには影響しません)

ここでは、seaの下さらに要素があると想定して、LidoSeaAction#index() を作りましょう。 (index()は、HTMLのindex.htmlと同様に、要素がないことを表現します。実質的に、その Action のデフォルトメソッドと考えて良いでしょう)

Actionのパッケージ(配置場所)を決める

LidoSeaAction であれば...

  • A. ...app.web.LidoSeaAction
  • B. ...app.web.lido.LidoSeaAction
  • C. ...app.web.lido.sea.LidoSeaAction

のどれかになります。(規約)

LastaFlute の Action - Action Package

web直下に置くのは、よほどのメジャー感のあるものなので、基本的には B, C となるでしょう。 lidoの仲間がすごく多いことが想定される場合は C ですが、まずは B にしておいて、後から移動してもよいでしょう。 (あとでパッケージを移動しても、URLやプログラムに影響はありません)

ここでは、...app.web.lido.sea に置くことにしましょう。

実際に作る

実際に org.docksidestage.app.web.lido.sea.LidoSeaAction にクラスを作ってみましょう。

e.g. Action class location @Package
src/main/java
 |-org.docksidestage
 |  |-app
 |  |  |-logic
 |  |  |-web
 |  |  |  |-lido
 |  |  |  |  |-sea
 |  |  |  |  |  |-LidoSeaAction.java
 |  |  |  |-...
 |  |  |-...
 |  |-bizfw
 |  |-dbflute
 |  |-...

Executeメソッドを準備

Baseクラスを継承

まずは、作成したActionにて、そのアプリのBaseクラスを継承しましょう。アプリ名が harbor であれば、 HarborBaseAction を継承します。(そのアプリのBaseActionが用意されているはずです)

e.g. Action class extends Base class @Java
/**
 * @author yourname
 */
public class LidoSeaAction extends HarborBaseAction {
}

Executeメソッドを定義

ここでは、JSONスタイルの Execute メソッド index() を作ってみましょう。

Executeアノテーションを付けて戻り値は JsonResponse<LidoSeaResult>, そして、Pathパラメーター /3 を受け取る引数を最初に定義して、GETパラメーターを受け取る Form を最後の引数に定義します。(規約)

e.g. Action class extends Base class @Java
/**
 * @author yourname
 */
public class LidoSeaAction extends HarborBaseAction {

    @Execute
    public JsonResponse<LidoSeaResult> index(Integer productId, LidoSeaForm form) {
    	// まだ、Formがない、Resultがない、returnがないので三つンパイルエラーです!
    	// この後、FormとResultを作って、戻り値を指定しますので、ちょっとそのままで。
    }
}

もし、Pathパラメーターの /3 が非必須の要素であれば、productId の型を OptionalThing<Integer> にします。省略されたときは empty になります。そもそも、PathパラメーターもFormパラメーターもないのであれば、引数なしでOKです。

Formを作る

Formクラスを定義

LidoSeaForm は定義してみましたが、まだ存在しないのでコンパイルエラーです。

Formのクラス名は Form で終わる 必要があります(規約)。その前の名前は任意ですが、Actionとイメージの近い名前がオススメです。 ここでは、LidoSeaForm という名前で作りましょう。

パッケージ(置き場所)は、Actionクラスと同じ (つまり、Actionの隣に置く) がオススメです。

e.g. Form class location @Directory
src/main/java
 |-org.docksidestage
 |  |-app
 |  |  |-logic
 |  |  |-web
 |  |  |  |-lido
 |  |  |  |  |-sea
 |  |  |  |  |  |-LidoSeaAction.java
 |  |  |  |  |  |-LidoSeaForm.java
 |  |  |  |  |-...
 |  |  |  |-...
 |  |  |-...
 |  |-bizfw
 |  |-dbflute
 |  |-...

Formの実装

publicフィールドで受け取るパラメーターのプロパティを定義します。フリー入力項目でなければ、Stringではなく、Integer や LocalDate や CDef (区分値) など、ネイティヴな型で宣言してOKです。 (変換できない値だったら、それはシステム上のミス、もしくは、いたずらということで 400 になる)

ここでは、pay=HANというGETパラメーターに対応して、payプロパティを定義しましょう。 商品ステータスの区分値なので、CDef 型で宣言します。

e.g. Form class @Java
/**
 * @author yourname
 */
public class LidoSeaForm {

    public CDef.PaymentMethod pay;
}

もし、バリデーションを行うなら、Validatorアノテーションを付けます。

必須チェック
@Required (String, Integerなんでも使える)
文字列の最大長
@Length
数値の最大値
@Max
などなど
いろいろ

ここでは、payプロパティを必須にしてしまいましょう。(何も付けないと説明しづらいので...)

e.g. Form class @Java
/**
 * @author yourname
 */
public class LidoSeaForm {

    @Required
    public CDef.PaymentMethod pay;
}

JSON Bodyはまた後で

ちなみに、リクエストを (Request Bodyの) JSON で受け取る場合は、Form ではなく Body クラスを使います。JSON API を作るときは、Bodyの方がメインになることが想定されますが、いったんここでは簡易に動作確認をするために Form で作ることにしましょう。(クラス名の終わりを Body にするだけなので作ること自体は簡単です)

JSON Resultを作る

JSON Resultクラスを定義

レスポンスで戻すJSONを表現する JSON Resultクラスを作りましょう。

特にクラス名に規約はありませんが、Resultで終わる名前にするのか慣習です。ここでは、LidoSeaResult という名前で作りましょう。

パッケージ(置き場所)は、Actionクラスと同じ (つまり、Actionの隣に置く) がオススメです。

e.g. JSON Result class location @Directory
src/main/java
 |-org.docksidestage
 |  |-app
 |  |  |-logic
 |  |  |-web
 |  |  |  |-lido
 |  |  |  |  |-sea
 |  |  |  |  |  |-LidoSeaAction.java
 |  |  |  |  |  |-LidoSeaForm.java
 |  |  |  |  |  |-LidoSeaResult.java
 |  |  |  |  |-...
 |  |  |  |-...
 |  |  |-...
 |  |-bizfw
 |  |-dbflute
 |  |-...

JSON Resultの実装

プロパティの宣言の仕方は、基本的に Form と同じです。

ここでは、検索されたデータ項目と "今のあなたの気分" を表現しましょう。

e.g. properties for display in JSON Result class @Java
/**
 * @author yourname
 */
public class LidoSeaResult {

    @NotNull
    @Valid
    public List<LidoSeaProductPart> products;

    public static class LidoSeaProductPart {

        @Required
        public Long purchaseId;
        @Required
        public String memberName;
        @Required
        public String productName;
        @Required
        public String productHandleCode;
        @Required
        public LocalDate purchaseDate;
        @Required
        public Integer purchasePrice;
    }

    @Required
    public String yourMood;
}

ネストした要素を表現するクラスは、staticのインナークラスで表現するのが慣習です。 特に再利用するわけでもないので、わざわざ独立したクラスにする必要はないでしょうし、一緒に宣言されていたほうが JSON の形が見えやすいという考えです。 また、内部の一部分を表現するクラスということで、Part で終わるクラス名にするのも慣習です。 (慣習なので従わなくてもいいのですが、クラス検索したときとかわかりやすいので、LastaFluteのデフォルト慣習として)

JSON Result でも Validation

JSON Result でも Validator Annotation を付けましょう(@Required や @NotNull など基本的なものだけ)。 JSONの戻りに対する自分自身のチェックと解釈することができます。 クライアント側でエラーがあるとデバッグしづらいので事前に防ぐとともに、ドキュメント的な意味合いも兼ねます。

リストは、空っぽ(0件)もあり得るのであれば @Required ではなく、@NotNull (nullだけのチェック) にしておきましょう。 また、ネストした要素のクラスのバリデーションも有効にするために @Valid も忘れずに。

Executeメソッドを実装

ひとまずreturnを書いておく

returnは後であれこあカスタマイズするかもしれませんが、コンパイルエラーのまま実装するのはつらいので、とりあえず解決しておきます。

asJson([JSON Resultのインスタンス]) を return します。

e.g. return JsonResponse with JSON Result @Java
@Execute
public JsonResponse<LidoSeaResult> index(int productId, LidoSeaForm form) {
    LidoSeaResult result = new LidoSeaResult();
    return asJson(result); 
}

(jflute備忘録: デモンストレーションのとき、ここで Boot で画面アクセス)

validateApi()を呼ぶ

FormにValidatorアノテーションを一つでも付けているなら、validateApi() を呼ぶ必要があります。

e.g. validateApi() in Action class @Java
@Execute
public JsonResponse<LidoSeaResult> index(int productId, LidoSeaForm form) {
    validateApi(form, messages -> {});
    LidoSeaResult result = new LidoSeaResult();
    return asJson(result); 
}

Eclipseであれば、秘伝の EditorTemplate が設定されていれば、魔法のように補完できます。

相関バリデーションやDBを使ったバリデーションなど、アノテーションでは実現できないものは、第二引数の Lambda の中で実装します。 (if文でチェックしてメッセージを追加)

"Api" の付かない単なる validate() メソッドは、HTMLを戻すときのためのメソッドなので、今回は関係ありません。 ただ、JSON APIサーバーを作るときは、validateApi() しか使わないのに毎回呼び分けることになってしまうので、[App]BaseAction が implements しているインターフェースを差し替えて、validate() を validateApi() と同じにすると良いでしょう。

DBFluteで検索しよう

DBFluteを使って検索・更新などを行うのであれば...基点テーブルの Behavior を DI しましょう。

ここでは、購入の一覧でしたから、基点テーブルは PURCHASE です。

e.g. DBFlute Behavior's DI @Java
@Resource
private PurchaseBhv purchaseBhv;

@Execute
public HtmlResponse index(int productId, LidoSeaForm form) {
    ...
}

そして、ConditionBean で検索しましょう。

e.g. select by ConditionBean @Java
@Execute
public JsonResponse<LidoSeaResult> index(int productId, LidoSeaForm form) {
    validateApi(form, messages -> {});
    Integer userId = getUserBean().get().getUserId();
    ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
        cb.setupSelect_Member();
        cb.setupSelect_Product();
        cb.query().setProductId_Equal(productId);
        cb.orScopeQuery(orCB -> {
            orCB.query().setMemberId_Equal(userId);
            orCB.query().queryMember().existsMemberFollowingByYourMemberId(followingCB -> {
                followingCB.query().setMyMemberId_Equal(userId);
            });
        });
        cb.query().existsPurchasePayment(paymentCB -> {
            paymentCB.query().setPaymentMethodCode_Equal_AsPaymentMethod(form.pay);
        });
        cb.query().addOrderBy_PurchaseDatetime_Desc();
    });
    LidoSeaResult result = new LidoSeaResult();
    return asJson(result); 
}

※実際、商品IDが固定なので、商品は単独データ取得してリストでは要らないはずですが、簡略化しています

ログインしているユーザーの情報は、getUserBean() で取得できます。 ここでは、ログインしていることが前提なので、戻り値の OptionalThing は問答無用で get() してしまいます。

まだ、ラフスケッチ中なので、完璧な実装じゃなくてもOKです。とりあえずなんか検索できれば。

JSON Resultにマッピング

検索したデータ (Entity) を、JSON Result にマッピングします。(いわゆる詰替え)

e.g. mapping to JSON Result in Action class @Java

@Resource
private PurchaseBhv purchaseBhv;

@Execute
public JsonResponse<LidoSeaResult> index(int productId, LidoSeaForm form) {
    validateApi(form, messages -> {});
    Integer userId = getUserBean().get().getUserId();
    ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
        ...
    });
    LidoSeaResult result = new LidoSeaResult();
    result.products = purchaseList.stream().map(purchase -> {
        LidoSeaProductPart part = new LidoSeaProductPart();
        part.purchaseId = purchase.getPurchaseId();
        purchase.getMember().alwaysPresent(member -> {
            part.memberName = member.getMemberName();
        });
        purchase.getProduct().alwaysPresent(product -> {
            part.productName = product.getProductName();
            part.productHandleCode = product.getProductHandleCode();
        });
        part.purchaseDate = purchase.getPurchaseDatetime().toLocalDate();
        part.purchasePrice = purchase.getPurchasePrice();
        return part;
    }).collect(Collectors.toList());
    result.yourMood = "オープンリーチ一発ツモされて満貫の親っかぶりした気分";
    return asJson(result);
}

ちなみに、リフレクションで詰め替えるとかは強烈に非推奨です。

ラフスケッチできた

さて、これでラフスケッチ実装は終了です。全体像が、見えてきたでしょうか?

とりあえずActionはこんな感じ

この時点では、このような感じで Action ができあがっているはずです。

e.g. rough sketch finished @Java
/**
 * @author yourname
 */
public class LidoSeaAction extends HarborBaseAction {

    @Resource
    private PurchaseBhv purchaseBhv;

    @Execute
    public JsonResponse<LidoSeaResult> index(int productId, LidoSeaForm form) {
        validateApi(form, messages -> {});
        Integer userId = getUserBean().get().getUserId();
        ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
            cb.setupSelect_Member();
            cb.setupSelect_Product();
            cb.query().setProductId_Equal(productId);
            cb.orScopeQuery(orCB -> {
                orCB.query().setMemberId_Equal(userId);
                orCB.query().queryMember().existsMemberFollowingByYourMemberId(followingCB -> {
                    followingCB.query().setMyMemberId_Equal(userId);
                });
            });
            cb.query().existsPurchasePayment(paymentCB -> {
                paymentCB.query().setPaymentMethodCode_Equal_AsPaymentMethod(form.pay);
            });
            cb.query().addOrderBy_PurchaseDatetime_Desc();
        });
        LidoSeaResult result = new LidoSeaResult();
        result.products = purchaseList.stream().map(purchase -> {
            LidoSeaProductPart part = new LidoSeaProductPart();
            part.purchaseId = purchase.getPurchaseId();
            purchase.getMember().alwaysPresent(member -> {
                part.memberName = member.getMemberName();
            });
            purchase.getProduct().alwaysPresent(product -> {
                part.productName = product.getProductName();
                part.productHandleCode = product.getProductHandleCode();
            });
            part.purchaseDate = purchase.getPurchaseDatetime().toLocalDate();
            part.purchasePrice = purchase.getPurchasePrice();
            return part;
        }).collect(Collectors.toList());
        result.yourMood = "オープンリーチ一発ツモされて満貫の親っかぶりした気分";
        return asJson(result);
    }
}

Bootしてアクセスしてみましょう

Bootクラスを起動して、実際にそのURLでアクセスしてみましょう。 恐らく非常にそっけない画面しか表示されないと思いますが、ログをみてちゃんと動いていればこの先に進めます。

ここでは、HarborBoot クラスの main() を実行しましょう。

e.g. HarborBoot @Java
/**
 * @author jflute
 */
public class HarborBoot { // #change_it_first

    public static void main(String[] args) { // e.g. java -Dlasta.env=production -jar harbor.war
        new JettyBoot(8090, "/harbor").asDevelopment(isDevelopment()).bootAwait();
    }

    private static boolean isDevelopment() {
        return System.getProperty("lasta.env") == null;
    }
}
e.g. boot log @Console
INFO  (...@showBoot():105) - _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
INFO  (...@showBoot():106) -  the system has been initialized:
INFO  (...@showBoot():107) - 
INFO  (...@showBoot():108) -   -> Harbor (Local Development)
INFO  (...@showBoot():109) - _/_/_/_/_/_/_/_/_/_/
Boot successful as development: url -> http://localhost:8090/harbor

ここで、http://localhost:8090/harbor/lido/sea/3?pay=HAN にアクセスしてみましょう。 さて、ブラウザで何が表示されたでしょうか?

この時点で単純にアクセスするとログイン必須エラーになります。 harborプロジェクトはログイン機能を使っているのでActionクラスはデフォルトでログイン必須になっていますし、処理の中でログインユーザーの情報を利用しているのでログインが前提です。

そして、一度ログイン画面 http://localhost:8090/harbor/signin にアクセスして、ログインしてから lido/sea にアクセスしてみてください。

サーバーのログでも確認してみましょう

サーバーのログ (IDEのコンソールなど) にも着目してみましょう。 想定通りのSQLが実行されて、想定通しのJSONレスポンスが戻っていることを確認してみてください。

LastaFluteでこのようにログが出る、ということを知っておくことも大切です。うまく活用してスムーズなデバッグライフを送ってください。

ちょいっとリファクタリング

ちょっとメソッド長いですね。実際に業務だと、もっと大きくなる可能性があります。

IDEのショートカットを使って、稲妻のようにリファクタリングして、確認のためもう一度アクセスしてみましょう。 (jflute備忘録: デモンストレーションのとき稲妻のように)

e.g. after refactoring @Java
/**
 * @author yourname
 */
public class LidoSeaAction extends HarborBaseAction {

    // ===================================================================================
    //                                                                           Attribute
    //                                                                           =========
    @Resource
    private PurchaseBhv purchaseBhv;

    // ===================================================================================
    //                                                                             Execute
    //                                                                             =======
    @Execute
    public JsonResponse<LidoSeaResult> index(int productId, LidoSeaForm form) {
        validateApi(form, messages -> {});
        ListResultBean<Purchase> purchaseList = selectPurchaseList(productId, form);
        LidoSeaResult result = mappingToResult(purchaseList);
        return asJson(result);
    }

    // ===================================================================================
    //                                                                              Select
    //                                                                              ======
    private ListResultBean<Purchase> selectPurchaseList(int productId, LidoSeaForm form) {
        Integer userId = getUserBean().get().getUserId();
        ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
            cb.setupSelect_Member();
            cb.setupSelect_Product();
            cb.query().setProductId_Equal(productId);
            cb.orScopeQuery(orCB -> {
                orCB.query().setMemberId_Equal(userId);
                orCB.query().queryMember().existsMemberFollowingByYourMemberId(followingCB -> {
                    followingCB.query().setMyMemberId_Equal(userId);
                });
            });
            cb.query().existsPurchasePayment(paymentCB -> {
                paymentCB.query().setPaymentMethodCode_Equal_AsPaymentMethod(form.pay);
            });
            cb.query().addOrderBy_PurchaseDatetime_Desc();
        });
        return purchaseList;
    }

    // ===================================================================================
    //                                                                             Mapping
    //                                                                             =======
    private LidoSeaResult mappingToResult(ListResultBean<Purchase> purchaseList) {
        LidoSeaResult result = new LidoSeaResult();
        result.products = purchaseList.stream().map(purchase -> {
            LidoSeaProductPart part = new LidoSeaProductPart();
            part.purchaseId = purchase.getPurchaseId();
            purchase.getMember().alwaysPresent(member -> {
                part.memberName = member.getMemberName();
            });
            purchase.getProduct().alwaysPresent(product -> {
                part.productName = product.getProductName();
                part.productHandleCode = product.getProductHandleCode();
            });
            part.purchaseDate = purchase.getPurchaseDatetime().toLocalDate();
            part.purchasePrice = purchase.getPurchasePrice();
            return part;
        }).collect(Collectors.toList());
        result.yourMood = "オープンリーチ一発ツモされて満貫の親っかぶりした気分";
        return result;
    }
}

DefTestでポリシーチェック

"Action定義のテスト" のドキュメントをよく読み...

HarborActionDefTest を実行してみましょう。

現時点で特に何か落ちるような不備はないはずなので、結果はgreenになるはずです。 何か一つでも、わざと落ちるような修正をしてみて、結果がredになるようにして例外メッセージを読んでみましょう。 (読み終わったら元に戻して、greenになることを確認しましょう)

LastaFluteには、最初からこのような横断的なプログラムのポリシーチェックをする UnitTest が用意されています。UnitTestは気軽に実行できるものですから、"Actionを作りながら" や "Actionを作り終わったとき" などに、実行してみてポリシー崩れがないかどうか確認するようにしましょう。

LastaDocの自動生成

"Actionのドキュメント自動生成" のドキュメントをよく読み...

LastaDocを自動生成し直して、新しく自分で作った SeaLandAction が LastaDoc に反映されることを確認してみましょう。

LastaFluteには、このようにActionに関するドキュメントを自動生成する機能が備わっています。 DBFluteで言えば、SchemaHTML のようなものです。ぜひ、有効活用していってください。

ホットデプロイ体験

例えば、cb.query().setProductId_Equal(productId) を一時的にコメントアウトして、(再起動せずに)アクセスしてみましょう。 検索結果が変わるはずです。(終わったら戻しておきましょう)

appパッケージ配下のクラスは、ホットデプロイ (HotDeploy) が効きますので、修正したらすぐに反映されます。 それの特徴をうまく活用して、開発効率を上げていってください。 (ただし、FreeGenなどDBFluteの自動生成を挟んだときは再起動が必要です)

JSON Bodyに変更してみよう

JSON APIを作るときは、やはりリクエストも JSON にすることが多いでしょう。

Form を Body という名前に

Formクラスの Form を、Body に名前変更するだけです。IDEのリファクタリング機能を使って rename してみましょう。 変数名も form から body にするのを忘れずに。

e.g. change Form to Body JQuery @Java
    @Execute
    public JsonResponse<LidoSeaResult> index(int productId, LidoSeaBody body) {
        
    }

動作確認のためにブラウザからリクエストを飛ばすのがちょっと工夫が必要なので、一例を示しておきます。 (Thanks, orito)

Chrome開発者ツールを開いてJQuery

Chrome開発者ツール (別のブラウザでもそれに相当するもの) を起動して、まず JQuery を使えるようにします。 (JQueryが必ず必要ってわけじゃなく、単純に簡易にリクエストを飛ばすために)

e.g. prepare JQuery @JavaScript
!function () {
  var script = document.createElement("script");
  script.setAttribute("src", "//code.jquery.com/jquery-2.0.0.min.js");
  document.body.appendChild(script);
}();

JSONのリクエストを送信するJavaScript

そして、pay=HAN部分がJSONになったリクエストを送信します。

e.g. send POST as JSON Body @JavaScript
$.ajax({
	url: 'http://localhost:8090/harbor/lido/sea/3',
	data: JSON.stringify({
		pay: 'HAN'
    }),
	type: 'POST',
	contentType: 'application/json',
	dataType: "json"
}).done(function(data){
    console.log(data);
});

でもってサーバーのログで確認

サーバーのログ (IDEのコンソールなど) で、想定通りのJSONを受け取って、想定通りのJSONを戻していることを確認しましょう。

リクエストされた JSON もログに表示されるようになっています。

実際にはSwaggerを使うと良いでしょう

もし、JSON APIの開発で JSON Body のリクエストばかり受け付ける API を作るのであれば、Swagger を使うと良いでしょう。

LastaFluteで Swagger を使うために最適化された lasta-doc.jar というライブラリがあります。 harborには入ってないので、ここでは気軽に試せませんが、もしスタートアップした実際のプロジェクトで Swagger を使った動作確認をするのであれば、環境構築して使える状態にしましょう。

※harborは、あくまで "サーバーサイドHTML + Ajax" を想定したプロジェクトなので、Swaggerはあまり必要ないと想定して入っていません。(逆に、不要なものが入らないように)

ちょこっとTips

スーパークラスのメソッド、何が使える?

Actionクラスの中で、this.docu...() と document メソッドを補完して、JavaDocを表示してみてください。すると...

e.g. document methods @Java
@Execute
public HtmlResponse index(int productId, LidoSeaForm form) {
    ...
    this.docu // 補完して、IDE上でJavaDoc表示
    ...
}

実装しながら、ふとActionの規約を忘れちゃったときとか、ブラウザで検索してドキュメントを探す...ではなく、 ササッと this.docu... ってやってみるといいかもですね。