20200913のJavaに関する記事は8件です。

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()) { %>
    // 管理者メニューを表示
<% } %>

※英語が怪しいのは大目に見てください :bow:

改善後は、メニューの表示条件や、会社やユーザの状態がとても分かり易くなったと思います。
実際のアプリケーションでは、MenuItemのような一つのメニューに該当するクラスを作り、そのクラスで判断させることも考えられますが、この記事ではここまでに留めておきます。

最後に:使用側で値を判断し始めたら要注意

今回の例では、メニュービューで会社やユーザの状態を直に判断し始めたことに問題がありました。
しかしながら、オブジェクト指向では、

求めるな、命じよ

が基本思想になります。
クラスの責務にもよりますが、値を持つ側に処理を任せましょう。

レガシープロジェクトでは、こういったコードをよく見かけることになります。
もはや手に負えないのがほとんどですが、大体はこういった小さな綻びから始まり、それをリファクタリングせずに残した結果です。
可能であれば、リファクタリングは積極的に行いましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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-function

Micronaut 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.java
package 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.java
package 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.java
package native.lambda
import io.micronaut.core.annotation.Introspected

@Introspected
class BookSaved {
    var name: String? = null
    var isbn: String? = null
}
  • [java:BookRequestHandler.java]
    メインの処理を行うクラスです。
BookRequestHandler.java
package 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 を開きましょう。
画面右上の[関数の作成]を押します。

以下のように選択と入力をしましょう。
create-lambda-function.png
MicronautはJavaのアプリですが、ネイティブビルドしたアプリケーションをあげるため、ランタイムはJavaではなく「独自のブートストラップ」を選択しましょう。

基本設定の修正

Lambda作成後の画面で基本設定を編集していきます。
lambda-info-setting.png

右上の編集ボタンから以下のように変更します。

  • ハンドラ: example.micronaut.BookRequestHandler
  • メモリ: 512MB

lambda-info-setting-datail.png
保存します。

ネイティブイメージの作成

CLIで作成する際に --features=graalvm をつけたことで、ネイティブビルド用のファイルが作成されています。

Dockerfile
FROM 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.zip

Lambdaにアップロード

デプロイされたファイルをLambdaにアップロードしていきます。
上で作成したLambda関数を開きましょう。

[関数コード] のアクションを押して、「.zipファイルをアップロード」を押して、先ほど作成した function.zip を選択してアップロードしてください。
lambda-upload.png

Lambdaを動かしてみよう

まずは動かすためのテストイベントを作成します。
上の「テストイベントの設定」を押して以下のように設定します。

bookTest
{
  "body": "{\"name\":\"Book Test!\"}"
}

test-event.png
設定が完了したら「テスト」を押します。
一番下のログ出力の結果を見てみましょう。
mn-lambda-native.png
Init Duration: 450.70 ms
Duration: 189.76 ms

Init Durationは起動時間で、Durationは処理時間です。
起動時間が 0.45秒なので非常に速いことがわかります。

コールドスタートであるJavaを元に作成したアプリケーションでこの時間になるのは感動的ですね!

おまけ

ネイティブビルドではなく、通常のJarファイルでデプロイしたアプリケーションでの実行速度を見てみましょう。
作成してアップロードするところは省略します。
mn-lambda-normal.png
Init Duration: 2909.91 ms
初期起動時間は約3秒です。

ネイティブビルドとの差は約2.5秒ほどあります。

まとめ

簡単にAWS Lambdaまであげれることが確認できました!
CLI経由でアプリケーションを作成するだけで Dockerfile やアップロード用の deploy.sh が生成されるのでデプロイファイルもお手軽に作成できますね。
起動速度も通常のJavaで動かすよりはるかに速いことが体験できました。

次回はMicronautでDBへ接続して処理するようなWebアプリケーションを作成していきたいと思います。

参考文献

この記事は以下の情報を参考にして執筆しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を通じて、プログラムの基礎を身につけて多言語へと派生させていこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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文字が部分文字列として表示される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)もありますが、やはり循環参照しているオブジェクトはダメっぽいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Kotlin】Javaのリフレクションを前提としたマッピングライブラリは基本的にKotlinで動かない

ModelMapperなど、「Java向けのリフレクションを利用したマッピングライブラリがKotlindata classに対して機能しない」という話をしばしば見かけるので、何故そうなるのかと、それに対応するためにどのような手段が有るかを書きます。

TL;DR

  • Java向けのマッピングライブラリは「引数無しコンストラクタでインスタンス生成 -> 個々のフィールドを初期化」という手順を前提としている
  • 一方、Kotlindata classは基本的に引数無しコンストラクタを持たない
  • このため、Java向けのリフレクションを利用したマッピングライブラリはKotlindata 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向けのリフレクションを利用したマッピングライブラリはKotlindata classに対して機能しません。
ただ、無理やりにでも動かす方法は幾つか有ります。

No-arg compiler pluginを使う

Kotlin公式で、実行時にのみ呼び出せる引数無しコンストラクタを追加するプラグインが提供されています。
これを用いることで、null安全は壊れるものの、見かけ上は従来のライブラリが機能するようになります。

引数無しコンストラクタをKotlin上に書く

マップ先クラスをJavaPOJOっぽく機能するように書けば従来のライブラリが機能するようになります。
また、外部に公開するインターフェースと実際のデータを入れるクラスの実装を分離すれば、「外から見る限り普通のdata classっぽく見える」状況は実現できます。

完全なKotlin化を諦めるなら、POJOで実装した上でゲッターにNullabilityを表すアノテーションを付けるというのも手です。

Kotlinに対応したライブラリについて

最後に、自分が把握している限りでKotlinに対応したObject to Objectマッピングライブラリに関して書きます。

KMapper

筆者が作成した、Kotlinのリフレクションによる関数呼び出しベースのマッピングライブラリです。
Object以外にも、Mapなどを引数に取ったり、複数引数からマッピングを行うことができます。

自作する

Kotlinのリフレクションによる関数呼び出しは割と簡単に実装できるため、機能性を求めなければサクッと自作してしまうのが簡単という気がしています。

MapStruct

記事執筆時点ではベータ版ですが、MapStruct1.4からコンストラクタ呼び出しによるマッピングをサポートするそうです。

補足として、詳しい話は省きますが、annotation-processorベースのライブラリであればJava向けのものでも原理的にKotlin対応がしやすいのかなと思っています。

Jackson

JacksonにはKotlinサポートが有るため、これによって「1度JSON化する -> デシリアライズする」という手順でマッピングが実現できます。
機能は非常に充実していますし、シリアライズさえできれば多様なソースにも対応できることから、実行速度を除けばJacksonを用いるのが案外よいかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Kotlin】Javaを前提としたリフレクションによるマッピングライブラリは基本的にKotlinで動かない

ModelMapperなど、「Java向けのリフレクションを利用したマッピングライブラリがKotlindata classに対して機能しない」という話をしばしば見かけるので、何故そうなるのかと、それに対応するためにどのような手段が有るかを書きます。

TL;DR

  • Java向けのマッピングライブラリは「引数無しコンストラクタでインスタンス生成 -> 個々のフィールドを初期化」という手順を前提としている
  • 一方、Kotlindata classは基本的に引数無しコンストラクタを持たない
  • このため、Java向けのリフレクションを利用したマッピングライブラリはKotlindata 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向けのリフレクションを利用したマッピングライブラリはKotlindata classに対して機能しません。
ただ、無理やりにでも動かす方法は幾つか有ります。

No-arg compiler pluginを使う

Kotlin公式で、実行時にのみ呼び出せる引数無しコンストラクタを追加するプラグインが提供されています。
これを用いることで、null安全は壊れるものの、見かけ上は従来のライブラリが機能するようになります。

引数無しコンストラクタをKotlin上に書く

マップ先クラスをJavaPOJOっぽく機能するように書けば従来のライブラリが機能するようになります。
また、外部に公開するインターフェースと実際のデータを入れるクラスの実装を分離すれば、「外から見る限り普通のdata classっぽく見える」状況は実現できます。

完全なKotlin化を諦めるなら、POJOで実装した上でゲッターにNullabilityを表すアノテーションを付けるというのも手です。

Kotlinに対応したライブラリについて

最後に、自分が把握している限りでKotlinに対応したObject to Objectマッピングライブラリに関して書きます。

KMapper

筆者が作成した、Kotlinのリフレクションによる関数呼び出しベースのマッピングライブラリです。
Object以外にも、Mapなどを引数に取ったり、複数引数からマッピングを行うことができます。

自作する

Kotlinのリフレクションによる関数呼び出しは割と簡単に実装できるため、機能性を求めなければサクッと自作してしまうのが簡単という気がしています。

MapStruct

記事執筆時点ではベータ版ですが、MapStruct1.4からコンストラクタ呼び出しによるマッピングをサポートするそうです。

補足として、詳しい話は省きますが、annotation-processorベースのライブラリであればJava向けのものでも原理的にKotlin対応がしやすいのかなと思っています。

Jackson

JacksonにはKotlinサポートが有るため、これによって「1度JSON化する -> デシリアライズする」という手順でマッピングが実現できます。
機能は非常に充実していますし、シリアライズさえできれば多様なソースにも対応できることから、実行速度を除けばJacksonを用いるのが案外よいかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Kotlin】Java向けのリフレクションを利用したマッピングライブラリは基本的にKotlinで動かない

ModelMapperなど、「Java向けのリフレクションを利用したマッピングライブラリがKotlindata classに対して機能しない」という話をしばしば見かけるので、何故そうなるのかと、それに対応するためにどのような手段が有るかを書きます。

TL;DR

  • Java向けのマッピングライブラリは「引数無しコンストラクタでインスタンス生成 -> 個々のフィールドを初期化」という手順を前提としている
  • 一方、Kotlindata classは基本的に引数無しコンストラクタを持たない
  • このため、Java向けのリフレクションを利用したマッピングライブラリはKotlindata 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向けのリフレクションを利用したマッピングライブラリはKotlindata classに対して機能しません。
ただ、無理やりにでも動かす方法は幾つか有ります。

No-arg compiler pluginを使う

Kotlin公式で、実行時にのみ呼び出せる引数無しコンストラクタを追加するプラグインが提供されています。
これを用いることで、null安全は壊れるものの、見かけ上は従来のライブラリが機能するようになります。

引数無しコンストラクタをKotlin上に書く

マップ先クラスをJavaPOJOっぽく機能するように書けば従来のライブラリが機能するようになります。
また、外部に公開するインターフェースと実際のデータを入れるクラスの実装を分離すれば、「外から見る限り普通のdata classっぽく見える」状況は実現できます。

完全なKotlin化を諦めるなら、POJOで実装した上でゲッターにNullabilityを表すアノテーションを付けるというのも手です。

Kotlinに対応したライブラリについて

最後に、自分が把握している限りでKotlinに対応したObject to Objectマッピングライブラリに関して書きます。

KMapper

筆者が作成した、Kotlinのリフレクションによる関数呼び出しベースのマッピングライブラリです。
Object以外にも、Mapなどを引数に取ったり、複数引数からマッピングを行うことができます。

自作する

Kotlinのリフレクションによる関数呼び出しは割と簡単に実装できるため、機能性を求めなければサクッと自作してしまうのが簡単という気がしています。

MapStruct

記事執筆時点ではベータ版ですが、MapStruct1.4からコンストラクタ呼び出しによるマッピングをサポートするそうです。

補足として、詳しい話は省きますが、annotation-processorベースのライブラリであればJava向けのものでも原理的にKotlin対応がしやすいのかなと思っています。

Jackson

JacksonにはKotlinサポートが有るため、これによって「1度JSON化する -> デシリアライズする」という手順でマッピングが実現できます。
機能は非常に充実していますし、シリアライズさえできれば多様なソースにも対応できることから、実行速度を除けばJacksonを用いるのが案外よいかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む