- 投稿日:2020-09-13T20:21:11+09:00
Webアプリのメニューの表示条件の設計・コード例
業務でのシステムの話。
ログイン後、メニューが会社や人によって変わる条件を調査をしたときに、「クラス使った方が良くない?」と思ったことがあったので、考えをメモしておきます。現状のコード
以下のような会社クラス、ユーザクラスが存在し、それらの情報を使うビューが実装されています。
ここでは、getter、setterは@Data
により自動的に実装されるものとします。
また、マジックナンバーは本来使うべきではありませんが、定数化を割愛しています。会社クラス
Company.java/** * 会社の情報を格納するクラス */ @Data public class Company { private String companyId; // 会社ID private int option1Flg; // オプション1のフラグ(0 or 1) private int option2Flg; // オプション2のフラグ(0 or 1) }ユーザクラス
User.java/** * ユーザの情報を格納するクラス */ @Data public class User { private String userId; // ユーザID private int userType; // ユーザ種別(0:一般ユーザ、1:管理者) }メニュービュー
menu.jsp<% if (company.getOption1Flg() == 1) { %> // メニュー1を表示 <% } %> <% if (company.getOption2Flg() == 1) { %> // メニュー2を表示 <% } %> <% if (user.getUserType() == 1 && (company.getOption1Flg() == 1 || company.getOption2Flg() == 1)) { %> // 管理者メニューを表示 <% } %>companyはCompanyクラスのオブジェクト、userはUserクラスのオブジェクトと考えてください。
基本的に、各オプションが1であれば、それぞれのメニューを表示するだけです。
管理者メニューだけ特別で、ユーザが管理者で、いずれかのオプションが1であれば表示されるものとなります。割と見たようなことがあるコードではないでしょうか。
現状では一見問題ありません。仕様変更後のコード(悪い例)
以下のように仕様を変更しないといけなくなりました。
- ユーザ種別にリーダーを追加する
- オプション3~5を追加する
- オプション3~5に対応するメニュー3~5は、リーダーのみが表示できる
- 管理者メニューは、ユーザが管理者で、オプション1~4のいずれかが1であれば表示(5は無関係)
これを再度コードにしますが、まずは元の形式を踏襲してみます。
会社クラス
Company.java/** * 会社の情報を格納するクラス */ @Data public class Company { private String companyId; // 会社ID private int option1Flg; // オプション1のフラグ(0 or 1) private int option2Flg; // オプション2のフラグ(0 or 1) private int option3Flg; // オプション3のフラグ(0 or 1) private int option4Flg; // オプション4のフラグ(0 or 1) private int option5Flg; // オプション5のフラグ(0 or 1) }ユーザクラス
User.java/** * ユーザの情報を格納するクラス */ @Data public class User { private String userId; // ユーザID private int userType; // ユーザ種別(0:一般ユーザ、1:管理者、2:リーダー) }メニュービュー
menu.jsp<% if (company.getOption1Flg() == 1) { %> // メニュー1を表示 <% } %> <% if (company.getOption2Flg() == 1) { %> // メニュー2を表示 <% } %> <% if (user.getUserType() == 2 && company.getOption3Flg() == 3) { %> // メニュー3を表示 <% } %> <% if (user.getUserType() == 2 && company.getOption4Flg() == 4) { %> // メニュー4を表示 <% } %> <% if (user.getUserType() == 2 && company.getOption5Flg() == 5) { %> // メニュー5を表示 <% } %> <% if (user.getUserType() == 1 && (company.getOption1Flg() == 1 || company.getOption2Flg() == 1) || company.getOption3Flg() == 1) || company.getOption4Flg() == 1)) { %> // 管理者メニューを表示 <% } %>メニュービューはこれでも一応判別はできますが、だいぶ面倒なコードになってきたのは分かりますよね。
今後、仕様変更が行われると、まずいなというのは感じると思います。解決策:メニュークラスを追加する
解決策はいろいろとあると思いますが、メニュークラスを追加する設計にします。
併せて、既存クラスも状態判定メソッドを追加します。会社クラス
Company.java/** * 会社の情報を格納するクラス */ @Data public class Company { private String companyId; // 会社ID private int option1Flg; // オプション1のフラグ(0 or 1) private int option2Flg; // オプション2のフラグ(0 or 1) private int option3Flg; // オプション3のフラグ(0 or 1) private int option4Flg; // オプション4のフラグ(0 or 1) private int option5Flg; // オプション5のフラグ(0 or 1) public boolean isEnabledOption1() { return option1Flg == 1; } public boolean isEnabledOption2() { return option2Flg == 1; } public boolean isEnabledOption3() { return option3Flg == 1; } public boolean isEnabledOption4() { return option4Flg == 1; } public boolean isEnabledOption5() { return option5Flg == 1; } public boolean isEnabledAdminOption() { return isEnabledOption1() || isEnabledOption2() || isEnabledOption3() || isEnabledOption4(); } }ユーザクラス
User.java/** * ユーザの情報を格納するクラス */ @Data public class User { private String userId; // ユーザID private int userType; // ユーザ種別(0:一般ユーザ、1:管理者、2:リーダー) public boolean isNormalUser() { return userType == 0; } public boolean isAdmin() { return userType == 1; } public boolean isLeader() { return userType == 2; } }メニュークラス
User.java/** * メニュークラス */ @RequiredArgsConstructor @Getter public class Menu { private final Company company; private final User user; public boolean isEnabledMenu1() { return company.isEnabledOption1(); } public boolean isEnabledMenu2() { return company.isEnabledOption2(); } public boolean isEnabledMenu3() { return user.isLeader() && company.isEnabledOption3(); } public boolean isEnabledMenu4() { return user.isLeader() && company.isEnabledOption4(); } public boolean isEnabledMenu5() { return user.isLeader() && company.isEnabledOption5(); } public boolean isEnabledAdminMenu() { return user.isAdmin() && company.isEnabledAdminOption(); } }メニュービュー
menu.jsp<% if (menu.isEnabledMenu1()) { %> // メニュー1を表示 <% } %> <% if (menu.isEnabledMenu2()) { %> // メニュー2を表示 <% } %> <% if (menu.isEnabledMenu3()) { %> // メニュー3を表示 <% } %> <% if (menu.isEnabledMenu4()) { %> // メニュー4を表示 <% } %> <% if (menu.isEnabledMenu5()) { %> // メニュー5を表示 <% } %> <% if (menu.isEnabledAdminMenu()) { %> // 管理者メニューを表示 <% } %>※英語が怪しいのは大目に見てください
![]()
改善後は、メニューの表示条件や、会社やユーザの状態がとても分かり易くなったと思います。
実際のアプリケーションでは、MenuItemのような一つのメニューに該当するクラスを作り、そのクラスで判断させることも考えられますが、この記事ではここまでに留めておきます。最後に:使用側で値を判断し始めたら要注意
今回の例では、メニュービューで会社やユーザの状態を直に判断し始めたことに問題がありました。
しかしながら、オブジェクト指向では、求めるな、命じよ
が基本思想になります。
クラスの責務にもよりますが、値を持つ側に処理を任せましょう。レガシープロジェクトでは、こういったコードをよく見かけることになります。
もはや手に負えないのがほとんどですが、大体はこういった小さな綻びから始まり、それをリファクタリングせずに残した結果です。
可能であれば、リファクタリングは積極的に行いましょう。
- 投稿日:2020-09-13T14:48:37+09:00
Micronaut 2.x 入門 ~AWS Lambdaにネイティブビルドしてデプロイ~
前回: Micronaut 2.x 入門に続いて今回はネイティブビルドしたMicronautアプリをAWS Lambdaにアップロードしていきます。
versionが2.0以降初期で作成されるファンクションアプリが大きく変わったので少し混乱しましたが、公式のブログに細かく作り方が記載されていたので、それを元に作成していきます。
環境
- macOS Catalina: 10.15.4
- メモリ: 16GB
- Micronaut: 2.0.1
- Java: 11.0.8.j9-adpt
- Docker for Mac: 2.3.0.4
- AWSアカウント
MicronautでLambda Function用アプリケーション作成
作業用ディレクトリを作成
$ mkdir 02-native-function && cd 02-native-functionMicronaut CLIを利用して作成していきます。今回は言語にJavaを使います。
# CLIでファンクションアプリを作成 $ mn create-function-app example.micronaut.complete --features=aws-lambda,graalvm # 確認 $ tree complete complete/ ├── Dockerfile ├── README.md ├── bootstrap ├── build.gradle ├── deploy.sh ├── docker-build.sh ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── micronaut-cli.yml ├── settings.gradle └── src ├── main │ ├── java │ │ └── example │ │ └── micronaut │ │ ├── Book.java │ │ ├── BookLambdaRuntime.java │ │ ├── BookRequestHandler.java │ │ └── BookSaved.java │ └── resources │ ├── META-INF │ │ └── native-image │ │ └── example.micronaut │ │ └── complete-application │ │ └── native-image.properties │ ├── application.yml │ └── logback.xml └── test └── java └── example └── micronaut └── BookRequestHandlerTest.java 16 directories, 21 files # アプリケーションフォルダに移動 $ cd complete作成されたファイルを見ていこう
src/main/java/example/micronaut/
に下記4ファイルが作成されます。
- Book.java
- BookLambdaRuntime.java
- BookSaved.java
- BookRequestHandler.java
それでは、1つずつファイル内容を見ていきます。
[Book.java]
入力を保存するためのモデルクラスです。Book.javapackage example.micronaut; import edu.umd.cs.findbugs.annotations.NonNull; import io.micronaut.core.annotation.Introspected; import javax.validation.constraints.NotBlank; @Introspected public class Book { @NonNull @NotBlank private String name; public Book() { } @NonNull public String getName() { return name; } public void setName(@NonNull String name) { this.name = name; } }
java:BookLambdaRuntime.java
カスタムランタイムを実装するためのクラスです。BookLambdaRuntime.javapackage example.micronaut; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import io.micronaut.function.aws.runtime.AbstractMicronautLambdaRuntime; import java.net.MalformedURLException; import com.amazonaws.services.lambda.runtime.RequestHandler; import edu.umd.cs.findbugs.annotations.Nullable; public class BookLambdaRuntime extends AbstractMicronautLambdaRuntime<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent, Book, BookSaved> { public static void main(String[] args) { try { new BookLambdaRuntime().run(args); } catch (MalformedURLException e) { e.printStackTrace(); } } @Override @Nullable protected RequestHandler<Book, BookSaved> createRequestHandler(String... args) { return new BookRequestHandler(); } }いまいち筆者も良くわかっておりませんが、ソースコードの中身を見るにAPIGatewayからのLambda呼び出しをするための受付用のクラスのようなものでしょうか。
S3フックイベントのLambdaを作るときはAbstractMicronautLambdaRuntime
のジェネリクスがS3用になるのではないでしょうか。
[java:BookSaved.java]
戻り値を保存するためのモデルクラスです。BookSaved.javapackage native.lambda import io.micronaut.core.annotation.Introspected @Introspected class BookSaved { var name: String? = null var isbn: String? = null }
[java:BookRequestHandler.java]
メインの処理を行うクラスです。BookRequestHandler.javapackage example.micronaut; import io.micronaut.core.annotation.Introspected; import io.micronaut.function.aws.MicronautRequestHandler; import java.util.UUID; @Introspected public class BookRequestHandler extends MicronautRequestHandler<Book, BookSaved> { @Override public BookSaved execute(Book input) { BookSaved bookSaved = new BookSaved(); bookSaved.setName(input.getName()); bookSaved.setIsbn(UUID.randomUUID().toString()); return bookSaved; } }APIの入力値を
Book.java
で値を受け取り、戻り値をBookSaved.java
に詰めて返却します。Lambda関数の作成
Micronautで作成したアプリケーションを上げるための準備を進めていきます。
1. AWSコンソールにログイン
AWSコンソールにログインしましょう。
もしまだ未作成の場合は、AWSアカウントの作成を行なってから再度こちらに戻ってきてください。2. Lambda作成
ログイン後に
サービス > Lambda
を開きましょう。
画面右上の[関数の作成]を押します。以下のように選択と入力をしましょう。
MicronautはJavaのアプリですが、ネイティブビルドしたアプリケーションをあげるため、ランタイムはJavaではなく「独自のブートストラップ」を選択しましょう。基本設定の修正
右上の編集ボタンから以下のように変更します。
- ハンドラ: example.micronaut.BookRequestHandler
- メモリ: 512MB
ネイティブイメージの作成
CLIで作成する際に
--features=graalvm
をつけたことで、ネイティブビルド用のファイルが作成されています。DockerfileFROM gradle:6.3.0-jdk11 as builder COPY --chown=gradle:gradle . /home/application WORKDIR /home/application RUN ./gradlew build --no-daemon FROM amazonlinux:2018.03.0.20191014.0 as graalvm ENV LANG=en_US.UTF-8 RUN yum install -y gcc gcc-c++ libc6-dev zlib1g-dev curl bash zlib zlib-devel zip ENV GRAAL_VERSION 20.1.0 ENV JDK_VERSION java11 ENV GRAAL_FILENAME graalvm-ce-${JDK_VERSION}-linux-amd64-${GRAAL_VERSION}.tar.gz RUN curl -4 -L https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-${GRAAL_VERSION}/${GRAAL_FILENAME} -o /tmp/${GRAAL_FILENAME} RUN tar -zxvf /tmp/${GRAAL_FILENAME} -C /tmp \ && mv /tmp/graalvm-ce-${JDK_VERSION}-${GRAAL_VERSION} /usr/lib/graalvm RUN rm -rf /tmp/* CMD ["/usr/lib/graalvm/bin/native-image"] FROM graalvm COPY --from=builder /home/application/ /home/application/ WORKDIR /home/application RUN /usr/lib/graalvm/bin/gu install native-image RUN /usr/lib/graalvm/bin/native-image --no-server -cp build/libs/complete-*-all.jar RUN chmod 777 bootstrap RUN chmod 777 complete RUN zip -j function.zip bootstrap complete EXPOSE 8080 ENTRYPOINT ["/home/application/complete"]deploy.sh#!/bin/bash docker build . -t complete mkdir -p build docker run --rm --entrypoint cat complete /home/application/function.zip > build/function.zipデプロイ用のファイルを作成
Dockerfile
を動かすdeploy.sh
を使ってネイティブビルド用の成果物を作成していきます。
(筆者のネットワークの問題なのかもしれませんが、非常に時間がかかりました...。)$ sh ./deploy.sh Sending build context to Docker daemon 17.63MB Step 1/24 : FROM gradle:6.3.0-jdk11 as builder ---> 0290cb9c9a7b Step 2/24 : COPY --chown=gradle:gradle . /home/application ---> 287bbae39066 ... # ファイルはbuild配下に作成されます $ ls build function.zipLambdaにアップロード
デプロイされたファイルをLambdaにアップロードしていきます。
上で作成したLambda関数を開きましょう。[関数コード] のアクションを押して、「.zipファイルをアップロード」を押して、先ほど作成した
function.zip
を選択してアップロードしてください。
Lambdaを動かしてみよう
まずは動かすためのテストイベントを作成します。
上の「テストイベントの設定」を押して以下のように設定します。bookTest{ "body": "{\"name\":\"Book Test!\"}" }
設定が完了したら「テスト」を押します。
一番下のログ出力の結果を見てみましょう。
Init Duration: 450.70 ms
Duration: 189.76 msInit Durationは起動時間で、Durationは処理時間です。
起動時間が 0.45秒なので非常に速いことがわかります。コールドスタートであるJavaを元に作成したアプリケーションでこの時間になるのは感動的ですね!
おまけ
ネイティブビルドではなく、通常のJarファイルでデプロイしたアプリケーションでの実行速度を見てみましょう。
作成してアップロードするところは省略します。
Init Duration: 2909.91 ms
初期起動時間は約3秒です。ネイティブビルドとの差は約2.5秒ほどあります。
まとめ
簡単にAWS Lambdaまであげれることが確認できました!
CLI経由でアプリケーションを作成するだけでDockerfile
やアップロード用のdeploy.sh
が生成されるのでデプロイファイルもお手軽に作成できますね。
起動速度も通常のJavaで動かすよりはるかに速いことが体験できました。次回はMicronautでDBへ接続して処理するようなWebアプリケーションを作成していきたいと思います。
参考文献
この記事は以下の情報を参考にして執筆しました。
- 投稿日:2020-09-13T14:28:29+09:00
Java HashMap,entrySet 【個人メモ】
【Java HashMap,entrySet】
業務でよく使用する、HashMap,entrySetの備忘録です。
HashMap<key, value>
HashMapはジェネリクス型(総称型)なため、keyとvalueの型を決めて宣言する。
総称型は、利用する型を限定するもの。Mapのkeyとvalueのペアは、Map.Entry インターフェイスで表す。
entrySetメソッドは、Mapのクラスメソッド
連想配列キーとバリューの組み合わせをエントリーといい、Mapのコレクションを返す。
Mapの配列ループのようなもので、動作が速い。Map.Entryは、getKeyメソッドで、key。getValueメソッドで、valueの値を取得する。
.entrySet( ); を使うことで全要素を取得することが可能
・実際に使用している例
①HashMap<String, Object> 1対1で対応
Objectには、様々な要素が入る。
②ArrayList<String> 文字列の値の羅列を格納
①+②= ArrayList<HashMap<String, Object>>
→ キーの多次元性を表現している。
余談
個人的にとても難しいと感じているところです。
まだまだ知識不足なところも多々あるので頑張っていきます。Javaを通じて、プログラムの基礎を身につけて多言語へと派生させていこうと思います。
- 投稿日:2020-09-13T13:45:22+09:00
Java 部分文字列
【Java 部分文字列】
業務でよく使用する Javaの部分文字列のメモです。
部分文字列 substring
substringメソッドの第一引数に抜き出し開始の位置(beginIndex)、第二引数に抜き出し終了の位置(endIndex)を指定する。
部分文字列は、beginIndexから始まり、endIndex-1 までを表す。
・substringの例
"サンプル文字".substring(beginindex, endindex-1);beginindexは、0番目から数え始める。
配列の添え字の数え方と同じ。・文字の長さは、endIndex - beginIndex
"サンプル文字".substring(0, 4); //出力結果:サンプルこの場合だと、文字の長さ 4-0=4
なので、4文字が部分文字列として表示される。
- 投稿日:2020-09-13T13:21:09+09:00
Javaでデータオブジェクトの内容をJSONに変換したい でも循環参照が…
この記事で「データオブジェクト」とは何?
データを格納するために定義したJavaのクラスの事を指します。「privateメンバ変数 + getter&setter」か、publicメンバ変数を持つだけのクラスです。
web APIや、JPAでデータベースからデータを取ってくる時なんかによく作りますね。「DTO」って呼ぶ場合もあります。環境
- JDK : Amazon Corretto 1.8.0.265
- Apache commons-lang3 : 3.11
やりたいこと
こういうデータオブジェクトがあったとして、ここに格納されたデータをログとかに出力したい時ってありますよね。
public class TestDto { public String stringvar; public Integer integervar; public int intvar; public TestDto2 testDto2; public BigDecimal decimal; public java.util.Date date; } public class TestDto2 { public String string2; public Integer integer2; public int int2; public TestDto testDto; }そんな時、↓みたいにJSONで出力できればすごく見やすい。
{ "stringvar": "aaa", "integervar": 1, "intvar": 2, "testDto2": { "string2": "CCC", "integer2": 2, "int2": 0, "testDto": null }, "decimal": null, "date": "2020-08-12T00:00:00.000+0900" }reflectionToStringで変換する
Apache Commonsの
ToStringBuilder#reflectionToString
は、昔からおなじみですね。reflectionを使って、データオブジェクトの内容を文字列化してくれる便利メソッドです。
実はコレ、第2引数に出力フォーマットが指定でき、JSONもちゃんと用意されています。ToStringBuilder.reflectionToString(dto, ToStringStyle.JSON_STYLE)↓出力結果(別途pretty-print済)
{ "stringvar": "aaa", "integervar": 1, "intvar": 2, "testDto2": "jp.example.common.dto.TestDto2@ed17bee", "decimal": null, "date": "Wed Aug 12 00:00:00 JST 2020" }うー、惜しい…
入れ子になったデータオブジェクトは、"testDto2": "jp.example.common.dto.TestDto2@ed17bee",
といった、クラス名とハッシュ値だけが表示され、データを表示する事ができません。
ToStringBuilder#reflectionToString
は、各メンバの内容をtoString()
を使って文字列化しているので、今回のように親クラスを指定していない場合、Object#toString
が使われ、↑のような出力結果になるのですね。DTOの基底クラスを作ってみる
それならば、↓のようなデータオブジェクトの基底クラスを作ってみよう。
import org.apache.commons.lang3.builder.ToStringBuilder; public class CommonDto implements Serializable { @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); }そして、データオブジェクトは必ず↑のクラスを継承するようにします。
public class TestDto extends CommonDto { public String stringvar; public Integer integervar; public int intvar; public TestDto2 testDto2; public BigDecimal decimal; public java.util.Date date; } public class TestDto2 extends CommonDto { public String string2; public Integer integer2; public int int2; public TestDto testDto; }これなら、
toString()
するだけでJSONで出力する事ができます。dto.toString();↓出力結果(別途pretty-print済)
{ "stringvar": "aaa", "integervar": 1, "intvar": 2, "testDto2": { "string2": "CCC", "integer2": 2, "int2": 0, "testDto": null }, "decimal": null, "date": "Wed Aug 12 00:00:00 JST 2020" }循環参照に注意!
これで完璧にできた感じがするけど、1つ落とし穴があります。
データオブジェクトの中のメンバが、自分の親を参照している場合。素直に参照をたどっていくと、ぐるぐると永遠に参照できてしまうのですね。実際、↑の例ではnullになっている
testDto2.testDto
の参照先を親にしてみると…{ "stringvar": "aaa", "integervar": 1, "intvar": 2, "testDto2": { "string2": "CCC", "integer2": 2, "int2": 0, "testDto": { "stringvar": "aaa", "integervar": 1, "intvar": 2, "testDto2": { "string2": "CCC", "integer2": 2, "int2": 0, "testDto": { "stringvar": "aaa", "integervar": 1, "intvar": 2, "testDto2": { "string2": "CCC", "integer2": 2, "int2": 0, "testDto": { :こんなふうに無限ループしてしまう。
こんな参照のしかたは、通常しないと思いますが、JPAでこういうEntityを作って相互に関連させると、まさにこういうオブジェクトを返してきます。
Stack Overflowなんか見ても、この問題で困っている人がちらほら見られます。意外に遭遇しやすい落とし穴みたい。循環参照にも対応する
今回の目的はログ出力なので、循環参照してようがなんだろうが、気にせず使えるようにしたいと思います。
(例えば、目的がREST APIのレスポンスだったら、JPAから取得したオブジェクトをそのままレスポンスする事はないと思うので、循環参照問題は気にしなくて良いと思います。)
ToStringStyle.JSON_STYLE
を使っている限り、循環参照には対応しきれないように思ったので、今回はToStringStyle.JSON_STYLE
によく似たオリジナルのクラスを作る事で対応しようと思います。
ToStringStyle.JSON_STYLE
の実体は、ToStringStyle
のインナークラスとして定義されているJsonToStringStyle
なので、まずはこれを丸々コピーして自分のプロジェクトに保存します。
↓こんな感じ(クラス名以外はJsonToStringStyle
と同じです)public class OriginalJsonToStringStyle extends ToStringStyle { private static final long serialVersionUID = 1L; private static final String FIELD_NAME_QUOTE = "\""; /** * <p> * Constructor. * </p> * * <p> * Use the static constant rather than instantiating. * </p> */ OriginalJsonToStringStyle() { super(); this.setUseClassName(false); this.setUseIdentityHashCode(false); :そして、↓のメソッドだけ中身を書き換えます。
実は、元々ToStringStyle
には循環参照を適切に処理する仕組みが入っているのですが、↓のメソッドでの処理がちょっとマズくて、その処理をスルーしてしまうのです。そのせいで無限ループしていたのですね。@Override protected void appendDetail(final StringBuffer buffer, final String fieldName, final Object value) { if (value == null) { appendNullText(buffer, fieldName); return; } if (value instanceof String || value instanceof Character) { appendValueAsString(buffer, value.toString()); return; } if (value instanceof Number || value instanceof Boolean) { buffer.append(value); return; } final String valueAsString = value.toString(); if (isJsonObject(valueAsString) || isJsonArray(valueAsString)) { buffer.append(value); return; } appendDetail(buffer, fieldName, valueAsString); }↓こんな感じに変えました
@Override protected void appendDetail(final StringBuffer buffer, final String fieldName, final Object value) { if (value == null) { appendNullText(buffer, fieldName); return; } if (value instanceof String || value instanceof Character) { appendValueAsString(buffer, value.toString()); return; } if(value instanceof java.util.Date) { appendValueAsDate(buffer, (java.util.Date)value); return; } buffer.append(value); } //ついでにjava.util.DateをISO8601拡張形式で出力するように protected void appendValueAsDate(StringBuffer buffer, java.util.Date value) { DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); buffer.append("\"" + df.format(value) + "\""); }これで循環参照を適切に察知できるようになったので、さらに下記のメソッドを追加します。
循環参照しているオブジェクトの場合、↑で書き換えたような、通常の文字列化メソッドが使われず、このメソッドで処理されるのです。
出力内容は何でも良いので、オーバーライド元のメソッドと同じくObjectUtils#identityToString
の結果。ただし、JSON形式を崩さないように、前後にダブルクォーテーションを付与するようにしました。@Override protected void appendCyclicObject(final StringBuffer buffer, final String fieldName, final Object value) { buffer.append(FIELD_NAME_QUOTE); ObjectUtils.identityToString(buffer, value); buffer.append(FIELD_NAME_QUOTE); }↓出力結果(別途pretty-print済)
{ "stringvar": "aaa", "integervar": 1, "intvar": 2, "testDto2": { "int2": 0, "integer2": 2, "string2": "CCC", "testDto": { "stringvar": "aaa", "integervar": 1, "intvar": 2, "date": "2020-08-12T00:00:00.000+0900", "decimal": null, "testDto2": "jp.example.common.dto.TestDto2@5577140b" } } "date": "2020-08-12T00:00:00.000+0900", "decimal": null }いい感じ!
循環参照している部分は、一部重複して出力されていますが、"jp.example.common.dto.TestDto2@5577140b"
という表記に置き換わって、無限ループに陥らずに済みました!まとめ
データオブジェクトをJSONに変換するのは、
ToStringBuilder#reflectionToString
で可能です。
ただし、JPAを使っている場合等でオブジェクトが循環参照している場合は、特別な処理クラスを作ってあげないとダメでした。なお、JSON変換といえば、ToStringBuilder以外にもGSONやJackson(ObjectMapper)もありますが、やはり循環参照しているオブジェクトはダメっぽいです。
- 投稿日:2020-09-13T05:34:27+09:00
【Kotlin】Javaのリフレクションを前提としたマッピングライブラリは基本的にKotlinで動かない
ModelMapper
など、「Java
向けのリフレクションを利用したマッピングライブラリがKotlin
のdata class
に対して機能しない」という話をしばしば見かけるので、何故そうなるのかと、それに対応するためにどのような手段が有るかを書きます。TL;DR
Java
向けのマッピングライブラリは「引数無しコンストラクタでインスタンス生成 -> 個々のフィールドを初期化」という手順を前提としている- 一方、
Kotlin
のdata class
は基本的に引数無しコンストラクタを持たない- このため、
Java
向けのリフレクションを利用したマッピングライブラリはKotlin
のdata class
に対して機能しない補足
この記事では、マッピングライブラリという言葉を以下のような意味で使います。
- マッピングライブラリ: 何らかのソースからフィールドを初期化したオブジェクトインスタンスを生成するような機能を提供するライブラリ
- e.g.
ModelMapper
,BeanPropertyRowMapper
また、
Kotlin
上のマップ先はclass
でもdata class
でも関係有りませんが、data class
になる場合が多いだろうということで、この記事内ではdata class
を前提として扱います。本文
- デコンパイルしてみる
- 無理やりでも動かす
Kotlin
に対応したツールについてデコンパイルしてみる
「
data class
は引数無しコンストラクタを持たない」を確認するため、以下のクラスをidea
の機能でデコンパイルしてみます。data class Sample(val foo: Int, val bar: Int?)結果は以下のようになります。
引数無しコンストラクタの定義が無いことが分かります。デコンパイル結果(※記事内容に関係の無い部分は省略済み)/* 略 */ public final class Sample { private final int foo; @Nullable private final Integer bar; public final int getFoo() { return this.foo; } @Nullable public final Integer getBar() { return this.bar; } public Sample(int foo, @Nullable Integer bar) { this.foo = foo; this.bar = bar; } /* 略 */無理やりでも動かす
ここまでで説明した通り、
Java
向けのリフレクションを利用したマッピングライブラリはKotlin
のdata class
に対して機能しません。
ただ、無理やりにでも動かす方法は幾つか有ります。No-arg compiler pluginを使う
Kotlin
公式で、実行時にのみ呼び出せる引数無しコンストラクタを追加するプラグインが提供されています。
これを用いることで、null
安全は壊れるものの、見かけ上は従来のライブラリが機能するようになります。引数無しコンストラクタをKotlin上に書く
マップ先クラスを
Java
のPOJO
っぽく機能するように書けば従来のライブラリが機能するようになります。
また、外部に公開するインターフェースと実際のデータを入れるクラスの実装を分離すれば、「外から見る限り普通のdata class
っぽく見える」状況は実現できます。完全な
Kotlin
化を諦めるなら、POJO
で実装した上でゲッターにNullability
を表すアノテーションを付けるというのも手です。Kotlinに対応したライブラリについて
最後に、自分が把握している限りで
Kotlin
に対応したObject to Object
マッピングライブラリに関して書きます。KMapper
筆者が作成した、
Kotlin
のリフレクションによる関数呼び出しベースのマッピングライブラリです。
Object
以外にも、Map
などを引数に取ったり、複数引数からマッピングを行うことができます。自作する
Kotlin
のリフレクションによる関数呼び出しは割と簡単に実装できるため、機能性を求めなければサクッと自作してしまうのが簡単という気がしています。MapStruct
記事執筆時点ではベータ版ですが、
MapStruct
は1.4
からコンストラクタ呼び出しによるマッピングをサポートするそうです。補足として、詳しい話は省きますが、
annotation-processor
ベースのライブラリであればJava
向けのものでも原理的にKotlin
対応がしやすいのかなと思っています。Jackson
Jackson
にはKotlin
サポートが有るため、これによって「1度JSON
化する -> デシリアライズする」という手順でマッピングが実現できます。
機能は非常に充実していますし、シリアライズさえできれば多様なソースにも対応できることから、実行速度を除けばJackson
を用いるのが案外よいかもしれません。
- 投稿日:2020-09-13T05:34:27+09:00
【Kotlin】Javaを前提としたリフレクションによるマッピングライブラリは基本的にKotlinで動かない
ModelMapper
など、「Java
向けのリフレクションを利用したマッピングライブラリがKotlin
のdata class
に対して機能しない」という話をしばしば見かけるので、何故そうなるのかと、それに対応するためにどのような手段が有るかを書きます。TL;DR
Java
向けのマッピングライブラリは「引数無しコンストラクタでインスタンス生成 -> 個々のフィールドを初期化」という手順を前提としている- 一方、
Kotlin
のdata class
は基本的に引数無しコンストラクタを持たない- このため、
Java
向けのリフレクションを利用したマッピングライブラリはKotlin
のdata class
に対して機能しない補足
この記事では、マッピングライブラリという言葉を以下のような意味で使います。
- マッピングライブラリ: 何らかのソースからフィールドを初期化したオブジェクトインスタンスを生成するような機能を提供するライブラリ
- e.g.
ModelMapper
,BeanPropertyRowMapper
また、
Kotlin
上のマップ先はclass
でもdata class
でも関係有りませんが、data class
になる場合が多いだろうということで、この記事内ではdata class
を前提として扱います。本文
- デコンパイルしてみる
- 無理やりでも動かす
Kotlin
に対応したツールについてデコンパイルしてみる
「
data class
は引数無しコンストラクタを持たない」を確認するため、以下のクラスをidea
の機能でデコンパイルしてみます。data class Sample(val foo: Int, val bar: Int?)結果は以下のようになります。
引数無しコンストラクタの定義が無いことが分かります。デコンパイル結果(※記事内容に関係の無い部分は省略済み)/* 略 */ public final class Sample { private final int foo; @Nullable private final Integer bar; public final int getFoo() { return this.foo; } @Nullable public final Integer getBar() { return this.bar; } public Sample(int foo, @Nullable Integer bar) { this.foo = foo; this.bar = bar; } /* 略 */無理やりでも動かす
ここまでで説明した通り、
Java
向けのリフレクションを利用したマッピングライブラリはKotlin
のdata class
に対して機能しません。
ただ、無理やりにでも動かす方法は幾つか有ります。No-arg compiler pluginを使う
Kotlin
公式で、実行時にのみ呼び出せる引数無しコンストラクタを追加するプラグインが提供されています。
これを用いることで、null
安全は壊れるものの、見かけ上は従来のライブラリが機能するようになります。引数無しコンストラクタをKotlin上に書く
マップ先クラスを
Java
のPOJO
っぽく機能するように書けば従来のライブラリが機能するようになります。
また、外部に公開するインターフェースと実際のデータを入れるクラスの実装を分離すれば、「外から見る限り普通のdata class
っぽく見える」状況は実現できます。完全な
Kotlin
化を諦めるなら、POJO
で実装した上でゲッターにNullability
を表すアノテーションを付けるというのも手です。Kotlinに対応したライブラリについて
最後に、自分が把握している限りで
Kotlin
に対応したObject to Object
マッピングライブラリに関して書きます。KMapper
筆者が作成した、
Kotlin
のリフレクションによる関数呼び出しベースのマッピングライブラリです。
Object
以外にも、Map
などを引数に取ったり、複数引数からマッピングを行うことができます。自作する
Kotlin
のリフレクションによる関数呼び出しは割と簡単に実装できるため、機能性を求めなければサクッと自作してしまうのが簡単という気がしています。MapStruct
記事執筆時点ではベータ版ですが、
MapStruct
は1.4
からコンストラクタ呼び出しによるマッピングをサポートするそうです。補足として、詳しい話は省きますが、
annotation-processor
ベースのライブラリであればJava
向けのものでも原理的にKotlin
対応がしやすいのかなと思っています。Jackson
Jackson
にはKotlin
サポートが有るため、これによって「1度JSON
化する -> デシリアライズする」という手順でマッピングが実現できます。
機能は非常に充実していますし、シリアライズさえできれば多様なソースにも対応できることから、実行速度を除けばJackson
を用いるのが案外よいかもしれません。
- 投稿日:2020-09-13T05:34:27+09:00
【Kotlin】Java向けのリフレクションを利用したマッピングライブラリは基本的にKotlinで動かない
ModelMapper
など、「Java
向けのリフレクションを利用したマッピングライブラリがKotlin
のdata class
に対して機能しない」という話をしばしば見かけるので、何故そうなるのかと、それに対応するためにどのような手段が有るかを書きます。TL;DR
Java
向けのマッピングライブラリは「引数無しコンストラクタでインスタンス生成 -> 個々のフィールドを初期化」という手順を前提としている- 一方、
Kotlin
のdata class
は基本的に引数無しコンストラクタを持たない- このため、
Java
向けのリフレクションを利用したマッピングライブラリはKotlin
のdata class
に対して機能しない補足
この記事では、マッピングライブラリという言葉を以下のような意味で使います。
- マッピングライブラリ: 何らかのソースからフィールドを初期化したオブジェクトインスタンスを生成するような機能を提供するライブラリ
- e.g.
ModelMapper
,BeanPropertyRowMapper
また、
Kotlin
上のマップ先はclass
でもdata class
でも関係有りませんが、data class
になる場合が多いだろうということで、この記事内ではdata class
を前提として扱います。本文
- デコンパイルしてみる
- 無理やりでも動かす
Kotlin
に対応したツールについてデコンパイルしてみる
「
data class
は引数無しコンストラクタを持たない」を確認するため、以下のクラスをidea
の機能でデコンパイルしてみます。data class Sample(val foo: Int, val bar: Int?)結果は以下のようになります。
引数無しコンストラクタの定義が無いことが分かります。デコンパイル結果(※記事内容に関係の無い部分は省略済み)/* 略 */ public final class Sample { private final int foo; @Nullable private final Integer bar; public final int getFoo() { return this.foo; } @Nullable public final Integer getBar() { return this.bar; } public Sample(int foo, @Nullable Integer bar) { this.foo = foo; this.bar = bar; } /* 略 */無理やりでも動かす
ここまでで説明した通り、
Java
向けのリフレクションを利用したマッピングライブラリはKotlin
のdata class
に対して機能しません。
ただ、無理やりにでも動かす方法は幾つか有ります。No-arg compiler pluginを使う
Kotlin
公式で、実行時にのみ呼び出せる引数無しコンストラクタを追加するプラグインが提供されています。
これを用いることで、null
安全は壊れるものの、見かけ上は従来のライブラリが機能するようになります。引数無しコンストラクタをKotlin上に書く
マップ先クラスを
Java
のPOJO
っぽく機能するように書けば従来のライブラリが機能するようになります。
また、外部に公開するインターフェースと実際のデータを入れるクラスの実装を分離すれば、「外から見る限り普通のdata class
っぽく見える」状況は実現できます。完全な
Kotlin
化を諦めるなら、POJO
で実装した上でゲッターにNullability
を表すアノテーションを付けるというのも手です。Kotlinに対応したライブラリについて
最後に、自分が把握している限りで
Kotlin
に対応したObject to Object
マッピングライブラリに関して書きます。KMapper
筆者が作成した、
Kotlin
のリフレクションによる関数呼び出しベースのマッピングライブラリです。
Object
以外にも、Map
などを引数に取ったり、複数引数からマッピングを行うことができます。自作する
Kotlin
のリフレクションによる関数呼び出しは割と簡単に実装できるため、機能性を求めなければサクッと自作してしまうのが簡単という気がしています。MapStruct
記事執筆時点ではベータ版ですが、
MapStruct
は1.4
からコンストラクタ呼び出しによるマッピングをサポートするそうです。補足として、詳しい話は省きますが、
annotation-processor
ベースのライブラリであればJava
向けのものでも原理的にKotlin
対応がしやすいのかなと思っています。Jackson
Jackson
にはKotlin
サポートが有るため、これによって「1度JSON
化する -> デシリアライズする」という手順でマッピングが実現できます。
機能は非常に充実していますし、シリアライズさえできれば多様なソースにも対応できることから、実行速度を除けばJackson
を用いるのが案外よいかもしれません。