modelmapperの曖昧なマッピングを厳密なマッピングに

エキサイト株式会社の中尾です。

今回はModelMapperのよく使われるオプションについて、説明します。

以下の例は、userIdを別のモデルにマッピングします。 userIdからuserIdなのでもちろんテストは通ります。

@ExtendWith(MockitoExtension.class)
public class ModelMapperConfig{

    @Description("ModelMapperConfig")
    @Test
    void test() {
        final ModelMapper modelMapper = new ModelMapper();
        final TestModel testModel = new TestModel();
        testModel.setUserId("nakao");

        final Model map = modelMapper.map(testModel, Model.class);
        Assertions.assertEquals(
                "nakao",
                map.getUserId()
        );
    }

    @Data
    public static class Model{
        private String userId;
    }

    @Data
    public static class TestModel{
        private String userId;
    }
}

別のモデルにしてみましょう。

    @Data
    public static class Model{
        private long id;

        private String userId;
    }

実行します。

userIdからuseridはマッピングされるのですが、 idにもmappingされます!

ModelMapperのデフォルトマッピング設定は曖昧なマッピングだからです!

ModelMapper - Configuration

useridに関わりそうなプロパティを発見し、勝手にmappingします。怖いですね。。。

変数の型も、同一プロパティに合わせて勝手にmappingします。

@ExtendWith(MockitoExtension.class)
public class ModelMapperConfig{

    @Description("ModelMapperConfig")
    @Test
    void docomoMailReturnTrue() {
        final ModelMapper modelMapper = new ModelMapper();
        final TestModel testModel = new TestModel();
        testModel.setUserId(1);

        final Model map = modelMapper.map(testModel, Model.class);
        System.out.println(map.toString());
        Assertions.assertEquals(
                1L,
                map.getId()
        );
        Assertions.assertEquals(
                1L,
                map.getUserId()
        );
    }

    @Data
    public static class Model{
        private long id;

        private String userId;
    }

    @Data
    public static class TestModel{
        private int userId;
    }
}

上記の事象を防ぐために

        modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        modelMapper.getConfiguration().setFullTypeMatchingRequired(true);

を設定しましょう。厳密なプロパティ名、厳密な型のmappingをします。

AWS上の本番DBから通信コストを抑えて大量データを開発DBに入れる

こんにちは。エキサイト株式会社のエンジニアのAです

今回はAWS上で本番稼働中のRDSから開発用DBクラスター内に新しく作成したスキーマにデータを入れる必要があったため、その際に通信料金をできる限り最小限で抑える方法をご紹介します

料金について

  • インターネット経由で外からDBにアクセスすると料金がかかるので極力VPC内の同一リージョン間で行う

  • 異なるAZ間で通信した場合でも通信料金がかかるので同一AZから通信を行う(料金は微々たるものなので、大量のデータをやり取りする場合のみ考える必要があります)

aws.amazon.com

同一の AWS リージョンでのデータ転送 Amazon EC2Amazon RDS、Amazon Redshift、Amazon DynamoDB Accelerator (DAX)、Amazon ElastiCache インスタンス、Elastic Network Interface、または同じ AWS リージョン内のアベイラビリティーゾーンをまたいだ VPC ピアリング接続間で「受信 (イン)」/「送信 (アウト)」されるデータの転送料金は、各方向 0.01 USD/GB です。

使用構成

  • 本番環境RDS master1台 Read3台+オートスケール

  • 開発環境RDS master1台 Read1台

一時的な移行用DBを作成する

dumpの際はRDSに負荷がかかるため、本番DBのスナップショットから移行用の一時的なDBクラスターを作成しています

RDS → インスタンスを選択 → [スナップショット作成]

スナップショット→[スナップショットを復元]

※この方法だとスナップショット作成以降、本番DBに入ったデータは入らないので注意

Dumpを実行する開発サーバーを用意する

  • 本番環境DBからデータをDumpする際は、本番DBの対象インスタンスと同一のAZにする

  • Dumpしてきたデータを開発DBに入れる際は、対象インスタンスが実行環境と同一AZか確認する

※本番と開発DBの対象インスタンスのAZ異なった場合、DumpデータをS3に置いて開発DBと同じAZの実行環境でS3からDumpデータを取得してデータを入れるなどで対応

データの移行

1.本番DBからデータをDumpする

mysqldump -u [user] -h [本番DBのホスト名] -p [DB名]
  1. 開発DBにデータを入れる
cat [Dumpデータ.gz] | mysql -u [user] -h [開発DBのホスト名] -p -D [DB名]

片付け

  • 一時的に作った移行用DBをクラスターごと削除する

  • 開発環境からDumpデータを削除(S3に保存しておくのもアリかもしません)

終わりに

今回のように既存のDBクラスターを使う必要がない場合は、そのままスナップショットから作ったクラスターのサイズを調整するだけでよいです

また、最初に述べたように大量のデータを通信したり、頻繁にデータ通信が発生するなどの要件がなければAZを合わせる必要はなさそうなため、要件に合わせて変えていくと良いでしょう

第一回Radiotalk配信!!〜新卒と語り合おう〜

はじめまして!エンジニアとして新卒で入社した奥田です。

私は現在、社内の社外広報チームに所属しています。 その一環としてRadiotalkでのライブ配信することになりました! 記念すべき第一回は同期たちと最近の生活や仕事などざっくばらんに配信しました。 興味があれば是非ともRadiotalkをインストールし、アーカイブを覗いてみてください✨

今後も配信や、収録をして様々なコンテンツを届けていきます!

radiotalk.page.link

LogstashをAWS RDSに接続するときの注意点

こんにちは。 エキサイト株式会社の三浦です。

DBからデータをどこかに同期する(例えば、DB内のデータをElasticsearchに同期する)場合、いろいろな方法がありますが、その一つにLogstashを使うというものがあります。 LogstashはElasticsearchと同じくElastic社が提供しているアプリケーションであり、Elastic社の説明によれば

無料かつオープンのサーバーサイドデータ処理パイプラインです。膨大な数のソースからデータを取り込み、変換して、お好みの格納庫(スタッシュ)に送信します。

https://www.elastic.co/jp/logstash

というものとなっています。

Elastic社が提供しているということでElasticsearchと相性がよく、使っている方もいるかと思いますが、今回は実体験をもとに、AWS RDSをソースとしたときのある注意点について説明します。

問題の発生

弊社のあるサービスでは、AWS RDSのデータをLogstashを使ってElasticsearchと同期しています。 最近、変更を加えるためにTest環境にLogstashをデプロイしたところ、大きな変更ではなかったにも関わらずエラーが発生し、正しく動かなくなってしまいました。

エラーをよく見てみるとRDSとの接続エラーのようでしたが、その時の変更点はRDSとのコネクションに関わる部分ではなかったため、どうやらコードの変更が原因ではないようです。 RDSと接続するために使用しているJDBCライブラリの更新なども行ってみましたが、特に改善は見られません。

色々調べた結果、ある記事を発見しました。

解決

stackoverflow.com

この記事によると、 useSSL オプションが悪さをしているとのこと。 本来はSSLでのコネクションでは正しく動かないのですが、デフォルトで使ってしまうので、オプションで useSSL=false をつけることで回避できる、とのことでした。

実際にこのオプションを付けることでコネクションができるようになりました。

最後に

今回はRDS側の設定もLogstash側のRDSとの接続設定も触っていないにもかかわらず突然コネクションができなくなったので、どこでどんな変更が起きた結果エラーになるようになったのかはわかりませんが、なにはともあれLogstashをRDSと接続させるにはSSLを使用しない設定にする必要があるようです。

TabBarの選択ボタンを中央寄せにするアニメーション付き無限スクロール化するTips

こんにちは。 エキサイト株式会社の高野です。

今回はTabBarをListviewで実装することを前提にお話ししていくことを前提としていきます。

FlutterでTabBarを無限スクロール化する方法はいくつかあり、ライブラリでいうと infinite_listviewindexed_list_view などがあります。 純粋にListviewでたくさんの要素を確保し、中央のoffsetをinitialOffsetで指定するという方法もあります。
今回で言いますと、ボタンをタップした際にそのボタンを中央へ移動するアニメーションを付与した上で無限スクロール化したいという要件です。

各バージョン

Flutter: 2.0.6
iOS: 14.5
Android: 11.0

使用ライブラリ

scroll_to_index: 2.0.0

実装方法

使用ライブラリで挙げたようにアニメーションに scroll_to_index を使用し、無限スクロールは素のListviewで実装します。

ListView.builder(
   controller: AutoScrollController(),
   scrollDirection: Axis.horizontal,
   itemCount: 999,
   itemBuilder: (_, index) {
     return AutoScrollTag(
        key: ValueKey(index),
        controller: AutoScrollController(),
        index: index,
        child: Button(),
      );
   },
),

表示の方はこのようになっています。initialoffsetを中央あたりで指定してあげることによりTabBarの見た目の方は完成です。 この後に、ボタンのタップ処理発火時に

controller.scrollToIndex(index, preferPosition: AutoScrollPosition.middle);

以上で中央へのスクロールアニメーションができます。

まとめ

いかがでしたでしょうか? 中央のOffsetをinitialOffsetで指定するとその端末でしか中央にならないので今後initialOffsetを動的に設定する記事も書けたらいいなと思っています。 元々タブ要素の三倍確保しoffsetを監視して、常に中央のタブ要素を表示するという方法を試みていました。 前に要素を追加した時などoffsetが変わらず無限スクロールを実装できませんでしたので他にも無限スクロールを実装する方法があるならばご教示していただけると幸いです。

Java9から導入されたMatcherのreplaceAllを使って文字列を置換する

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 PHPで書かれたAPIからSpringBoot / Javaでリビルドを進めていく上で、 独自のイメージタグからHTMLのimgタグに置換する処理をJavaで実装することになりました。 シンプルでスッキリとしたコードを書くことができたので共有します。

導入

下記図のように、ある文字列の中から正規表現に一致する文字列を使用して別の文字列に置換したいとします。 このとき、文字列.replaceAll(正規表現, 置換する文字列) では正規表現に一致した文字列の中身を考慮することができないため、意図した結果を得ることができません。 そのため、何らかの方法で正規表現に一致した文字列を取得した上で置換する必要があります。

今回扱うデータ

今回扱うデータは下記のとおりです。 独自のイメージタグがあり、イメージタグの中にパス、長さ、高さの3つの要素が含まれているとします。

@Data
public class ImageTag {
    private String src;
    private int width;
    private int height;

    public String toImg() {
        return String.format(
                "<img src=\"%s\" width=\"%d\" height=\"%d\"/>", 
                src, width, height
        );
    }
}

また、イメージタグの正規表現imageTagPatternとして扱い、 抽出したイメージタグからImageTagクラスのインスタンスを生成するメソッドをtoImageTagとして扱います。

// [IMAGE|src|width|height] に一致するような正規表現(省略)
Pattern imageTagPattern = Pattern.compile("...");

// [IMAGE|src|width|height]を受け取り、ImageTagのインスタンスを生成するメソッド
public ImageTag toImageTag(String imageTagStr) {
  /* src, width, heightをsplit()などを使用して抽出 */
  return new ImageTag()
            .setSrc(src)
            .setWidth(width)
            .setHeight(height);
}

結論

Java9から導入されたMatcherのreplaceAll(Function<MatchResult, String>)使用することで、正規表現に一致した文字列を置換することができます。 下記コードでは、e正規表現と一致したもの(MatchResult)を表し、返り値(String)が置換するものを表しています。 今回のケースでは、正規表現と一致した文字列のイメージタグからImageTagインスタンスを生成し、インスタンスからimgタグを作成しています。 (インスタンスを生成せずに、直接変換することもできます。)

何をしたいのかがひと目でわかり、とても見通しの良いコードに仕上げることができました👏

public String replace(String text) {
    return imageTagPattern
            .matcher(text)
            .replaceAll(e -> toImageTag(e.group()).toImg());
}

Streamを使用した他の実装方法

replaceAll(Function<MatchResult, String>)を使用せずに実装する方法についても紹介します。

イテレータを作成して繰り返し置換する方法

Java9から導入されたMatcherのresults()を使用することで、正規表現に一致した文字列をStreamで取得することができます。 イテレータを作成して引数で受け取ったtextに対して繰り返し置換を行うようにしてみました。 replaceAll(Function<MatchResult, String>)を使用したコードと比較すると、コードが長くなってしまいますが、 比較的わかりやすいコードになるのかなと思います。

public String replace1(String text) {
    final Iterator<String> imageTagIterator = imageTagPattern.matcher(text)
            .results()
            .map(e -> e.group())
            .iterator();

    while (imageTagIterator.hasNext()) {
        final String imageTagStr = imageTagIterator.next();
        final ImageTag imageTag = toImageTag(imageTagStr);
        text = text.replace(imageTagStr, imageTag.toImg());
    }
    return text;
}

AtomicReferenceを使用して繰り返し置換する方法

AtomicReferenceを使用することで、引数で受け取ったtextforEachの中で繰り返し置換することができます。 Javaでは、ラムダ式の中で外部の変数(この場合はtext)を書き換えることができないため、一工夫しないといけません。 どうしてもラムダ式の中で外部の変数を書き換えたいとき以外は、この方法を避けたほうがよいのではないかと思います。

public String replace2(String text) {
    final AtomicReference<String> reference = new AtomicReference<>();
    reference.set(text);

    imageTagPattern
            .matcher(reference)
            .results()
            .forEach(e -> {
                final String s1 = reference.get();
                final ImageTag imageTag = toImageTag(e.group());
                final String s2 = s1.replace(e.group(), i.toImg());
                reference.set(s2);
            });

    return reference.get();
}

まとめ

Java9から導入されたreplaceAll(Function<MatchResult, String>)を使用することで、シンプルな置換処理を記述することができるようになりました。 調べてみると、正規表現に一致した文字列を別の文字列に置換する記事が多くヒットしてしまい、なかなかたどり着けなかったです。 また、Java8では導入されていないため、現在プロダクトで使用しているバージョンと同じドキュメントを読むのが大事だなと感じました。

誰かのお役に立てれば幸いです!

AtomicIntegerを使ってみた

エキサイト株式会社 メディア開発のしばたにえんです。

さっそくですが、下記のコードをご覧ください

class CountTest {

    private int num = 0;

    @SneakyThrows
    void CountThreadNum() {
        int threadNum = 10;
        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int n = 0; n < threadNum; n++) {
            service.submit(() -> {
                for (int m = 0; m < 1000; m++) {
                    num++;
                }
            });
        }
        service.shutdown();
        service.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(num);
    }
}
// 9818

スレッドを10個でそれぞれ1000回ずつ1を加算していくといった処理になりますが、 この結果は10000にはなりません。 intはアトミックではないためです。

概要

そもそもアトミックとは

コンピュータ上のプログラムの動作で、密接に関連する複数の処理が外部から一つの操作に見え、途中の状態を観測したり介入できない性質を、操作のアトミック性、不可分性などという。

要するに 複数のスレッドからのデータを書き込んでも最終的な値を保証しているというものです。

上記の問題を解決するのはAtomicIntegerです。 AtomicIntgerは多くのスレッドを同時に使用できるIntegerクラスです。

実際に使ってみます

class CountTest {

    // private int num = 0;
    private AtomicInteger atomicInteger = new AtomicInteger();

    @SneakyThrows
    void CountThreadAtomicIntegerNum() {
        int threadNum = 10;
        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int n = 0; n < threadNum; n++) {
            service.submit(() -> {
                for (int m = 0; m < 1000; m++) {
                    atomicInteger.incrementAndGet();
                }
            });
        }
        service.shutdown();
        service.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(atomicInteger.get());
    }
}
// 10000

期待されている値が取れました。

使い方

AtomicIntegerの使い方は主に二つあります
①カウンターとしての役割をアトミック性を保ちながら行う
②比較処理をアトミック性を保ちながら行う

① 1ずつ加算する場合

void atomicIntegerIncrementAndGet() {
    final List<String> characters = List.of("a", "b", "c", "d", "e", "f", "g", "h");
    final AtomicInteger atomicInteger = new AtomicInteger();
    characters.stream().forEach(
            charcter -> System.out.println(
                    String.format("%s:%s", charcter, atomicInteger.incrementAndGet())
            )
    );
}
// a:1
// b:2
// c:3
// d:4
// e:5
// f:6
// g:7
// h:8

初期値を入れて1ずつ加算する

void atomicIntegerIncrementAndGet() {
    final List<String> characters = List.of("d", "e", "f", "g", "h");
    final AtomicInteger atomicInteger = new AtomicInteger(3);
    characters.stream().forEach(
            charcter -> System.out.println(
                    String.format("%s:%s", charcter, atomicInteger.incrementAndGet())
            )
    );
}
// d:4
// e:5
// f:6
// g:7
// h:8

5ずつ加算する

void atomicIntegerGetAndAdd() {
    final AtomicInteger atomicInteger = new AtomicInteger();
    for (int n = 0; n < 3; n++) {
        System.out.println(atomicInteger.getAndAdd(5));
    }
}
// 0
// 5
// 10

② 比較

void atomicIntegerIncrementAndGetCompare() {
    final List<String> characters = List.of("a", "b", "c", "d", "e", "f", "g", "h");
    int targetNum = 3;
    final AtomicInteger atomicInteger = new AtomicInteger();
    characters.stream().forEach(
            charcter -> {
                if (atomicInteger.compareAndSet(targetNum, atomicInteger.incrementAndGet())) {
                    System.out.println(
                            String.format("%s:%s", charcter, atomicInteger.get())
                    );
                }
            }
    );
}
// c:3

まとめ

マルチスレッドの場合ちょっとしたループ内の加算処理でも上記のようなずれが起きてしまいます。 AtomicIntegerちゃんと使っていきましょう!

EC2をCloudFormationとAnsibleで構成管理する

エキサイト株式会社のみーです。

最近はコンテナばかり触っていますが、要件によってはEC2の非コンテナ環境で構築しなければいけないこともあると思います。そういう時に出てくるのが、構成管理どうしよう、という悩み。

ヘルパースクリプトを活用する

AWSに触れ始めた頃、CFnで全てを完結させたいという謎のモチベーションがありました。CFnのヘルパースクリプトを使うことで、その目的は達せられます。

docs.aws.amazon.com

例えば、SESにリレーするようにPostfixを設定したEC2を構築するとなると、以下のような感じ。

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Metadata:
      AWS::CloudFormation::Init:
        config:
          files:
            /etc/cfn/cfn-hup.conf:
              content: !Sub |
                [main]
                stack=${AWS::StackId}
                region=${AWS::Region}
                interval=3
              mode: "000400"
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.EC2Instance.Metadata.AWS::CloudFormation::Init
                action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region}
                runas=root
              mode: "000400"
              owner: root
              group: root
            /tmp/sasl_passwd:
              content: |
                [email-smtp.us-east-1.amazonaws.com]:587 foo:bar
              mode: "000600"
              owner: root
              group: root
          commands:
            01_postconf:
              command: |
                postconf -e \
                  "relayhost = [email-smtp.us-east-1.amazonaws.com]:587" \
                  "smtp_sasl_auth_enable = yes" \
                  "smtp_sasl_security_options = noanonymous" \
                  "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" \
                  "smtp_use_tls = yes" \
                  "smtp_tls_security_level = encrypt" \
                  "smtp_tls_note_starttls_offer = yes" \
                  "smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt"
            02_cp_sasl_passwd:
              command: cp /tmp/sasl_passwd /etc/postfix/sasl_passwd
            03_postmap:
              command: postmap hash:/etc/postfix/sasl_passwd
            04_chown_db:
              command: chown root:root /etc/postfix/sasl_passwd.db
            05_chmod_db:
              command: chmod 600 /etc/postfix/sasl_passwd.db
          services:
            sysvinit:
              cfn-hup:
                enabled: true
                ensureRunning: true
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
              postfix:
                enabled: true
                ensureRunning: true
                files:
                  - /tmp/sasl_passwd
    Properties:
      ImageId: !Ref ImageId
      KeyName: !Ref KeyName
      SubnetId: !Ref SubnetId
      AvailabilityZone: !Ref SubnetAz
      InstanceType: !Ref InstanceType
      SecurityGroupIds:
        - !Ref SecurityGroupId
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          yum update -y aws-cfn-bootstrap
          /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region}
          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region}

設定ファイルやコマンドなどをテンプレート内に記述するわけですが・・・見ての通り、決して美しいとは言えません。もちろん最適化の余地はあるのですが、このまま使い続ける気にはなりません。本来の目的は構成管理であって、テンプレートを美しく記述することではないわけです。

ということで、別の方法を考えることに。

Ansibleを使う

AWSの構成管理サービスといえばAWS OpsWorksですね。PuppetやChefを使い慣れているのであればベターな選択かと思います。

aws.amazon.com

なのですが、エキサイトでは構成管理ツールにAnsibleを採用することが多かったりします。個人的にもAnsibleは使いやすくて好きなので、今回はOpsWorksは使いません。

AWSでAnsibleプレイブックを実行する場合、SSMドキュメントの AWS-ApplyAnsiblePlaybooks を利用するのがおすすめです。
事前にS3にプレイブックをアップロードしておき、あとはマネコンなどからポチるだけで実行されます。とても快適。

docs.aws.amazon.com

以下のサンプルでは、ansible-playbook-${AWS::Region}-${AWS::AccountId} というS3バケットを用意しておき、playbook.zip という名前で対象のプレイブックをアップロードしていることが前提になっています。

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageId
      KeyName: !Ref KeyName
      SubnetId: !Ref SubnetId
      AvailabilityZone: !Ref SubnetAz
      InstanceType: !Ref InstanceType
      SecurityGroupIds:
        - !Ref SecurityGroupId

  ApplyAnsiblePlaybooks:
    Type: AWS::SSM::Association
    Properties:
      Name: AWS-ApplyAnsiblePlaybooks
      AssociationName: sample-association
      WaitForSuccessTimeoutSeconds: 300
      Targets:
        - Key: InstanceIds
          Values:
            - !Ref EC2Instance
      OutputLocation:
        S3Location:
          OutputS3BucketName: !Sub ansible-playbook-${AWS::Region}-${AWS::AccountId}
          OutputS3KeyPrefix: log
      Parameters:
        SourceType:
          - "S3"
        SourceInfo:
          - !Sub |
            {"path": "https://ansible-playbook-${AWS::Region}-${AWS::AccountId}.s3-${AWS::Region}.amazonaws.com/playbook.zip"}
        InstallDependencies:
          - "True"
        PlaybookFile:
          - "playbook.yml"
        ExtraVariables:
          - "SSM=True"
        Check:
          - "False"
        Verbose:
          - "-v"

これをデプロイすれば、良い感じにEC2インスタンスが作成されて、良い感じにAnsibleプレイブックが実行されます。プレイブックを変更した場合は、SSMのステートマネージャーから即座に適用できます。いいね。

トラブルシューティング

上記のテンプレート、EC2を閉域網に構築した場合には失敗します。InstallDependencies パラメータをTrueにすると、Ansibleや依存関係にあるPythonなどのミドルウェアをインストールしてくれるのですが、Amazon Linux 2の場合は以下のようなコマンドが実行されます。

sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum install -y ansible

思いっきり外に出ようとしているな。

が、そういうことであれば InstallDependencies パラメータはFalseにしておき、その前に AWS-RunShellScript でExtras LibraryからAnsibleをインストールしてしまえば良さそう。

  InstallAnsible:
    Type: AWS::SSM::Association
    Properties:
      Name: AWS-RunShellScript
      AssociationName: sample-association
      WaitForSuccessTimeoutSeconds: 300
      Targets:
        - Key: InstanceIds
          Values:
            - !Ref EC2Instance
      OutputLocation:
        S3Location:
          OutputS3BucketName: !Sub ansible-playbook-${AWS::Region}-${AWS::AccountId}
          OutputS3KeyPrefix: log
      Parameters:
        commands:
          - sudo amazon-linux-extras install ansible2 -y

S3のVPCエンドポイントを設定しておく必要はありますが、これなら閉域網でも問題ありませんね。

おわりに

CFnでは、AWSリソースの構成管理だけに徹するのが良いのかなと思います。銀の弾丸は存在しませんので、最適なツールを選択していきたいところです。

JUnitの@Before系アノテーションのバージョンによる違い

こんにちは。 エキサイト株式会社の三浦です。

JavaJUnitを使ってユニットテスト(UT)を行うとき、各テストの前に共通で行いたい処理がある場合、 @Before から始まるアノテーションを使えば簡潔に実装することが出来ます。

ですが、 @Before 系のアノテーションには @Before , @BeforeEach , @BeforeClass , @BeforeAll などがあり、使い分けが必要です。

JUnit5以前

JUnit5以前では、 @Before@BeforeClass を使うことになります。 @BeforeEach@BeforeAll は存在していません。

@Before

@Before アノテーションを使用すると、各テストメソッド実行前に @Before アノテーションがついたメソッドが実行されます。 そのため、テストメソッドの個数分実行されます。

public class ServiceTest {
    @Before
    public void setup() {
        // ここに毎処理ごとに必要な処理を追加
    }

    @Test
    @Description("サンプルテスト1")
    public void getSample1() {
        // ここが実行される前に、 `setup` メソッドが実行されます
    }

    @Test
    @Description("サンプルテスト2")
    public void getSample2() {
        // ここが実行される前に、 `setup` メソッドが実行されます
    }
}

@BeforeClass

@BeforeClass アノテーションを使用すると、初回のテスト実行前に @BeforeClass アノテーションがついたメソッドが実行されます。 @Before がテストメソッドの処理ごとに実行されるのに対し、 @BeforeClass はテストクラスの処理ごとに実行されるため、クラスが1つであればメソッドがいくつあっても1回しか実行されません。 また関連し、staticメソッドとしてしか定義できません。

public class ServiceTest {
    @BeforeClass
    public static void setup() {
        // 初回に必要な処理を追加
    }

    @Test
    @Description("サンプルテスト1")
    public void getSample1() {
        // ここが実行される前に、 `setup` メソッドが実行されます
    }

    @Test
    @Description("サンプルテスト2")
    public void getSample2() {
        // サンプルテスト1の前で `setup` メソッドが実行されている場合、この処理の前に `setup` メソッドは実行されません
    }
}

JUnit5以後

JUnit以後はアノテーション名が変わり、 @Before@BeforeEach に、 @BeforeClass@BeforeAll になりました。 IntelliJ IDEAの補完等にも出てこなくなってしまうので、注意しましょう。

名前は変わりましたが、使い方は同じです。

まとめ

JUnitのバージョンで名前が変わるため、 @Before 系のアノテーションについて調べ、いざ使ってみようと思ったらエラーになった…ということも起きうると思います(実体験)。 機能自体はとても有用で使いどころもあるので、ぜひ使っていきましょう。

RedisCacheConfigの設定

エキサイト株式会社 メディア事業部エンジニアの中尾です。

SQL Serverからデータを取得し、redisにデータをキャッシュさせようとしたら以下のエラーが出ました。

Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\" to enable handling

どうやらdatatype-jsr310が効いていないようです。

https://github.com/FasterXML/jackson-databind/issues/2983

issuesでもあったようですね。

objectMapperをカスタマイズする

redisとのやり取りの部分をobjectMapperの設定を追加してやります。

ポイントは、redisにキャッシュする際のデータの書き読み込みする際のobjectMapperの設定をGenericJackson2JsonRedisSerializerに設定しているところです。 SerializationFeature.WRITE_DATES_AS_TIMESTAMPSをすることでISO8601 形式にシリアライズします。

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * ElastiCacheではconfigコマンドの実行が禁止されているため、「ConfigureRedisAction」を「NO_OP」に設定しておく
     *
     * @return ConfigureRedisAction
     */
    @Bean
    public ConfigureRedisAction configureRedisAction() {
        return ConfigureRedisAction.NO_OP;
    }

    /**
     * Redisの設定
     *
     * @param factory
     * @return ReactiveRedisTemplate
     */
    @Bean
    public ReactiveRedisTemplate reactiveRedisTemplate(LettuceConnectionFactory factory) {
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(this.objectMapper());
        RedisSerializationContext.RedisSerializationContextBuilder<String, Object> builder =
                RedisSerializationContext.newSerializationContext(keySerializer);
        RedisSerializationContext<String, Object> context = builder.value(valueSerializer).build();
        return new ReactiveRedisTemplate<>(factory, context);
    }

    /**
     * Redisの中に平文でデータを保存するための設定
     *
     * @return RedisCacheManagerBuilder
     */
    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        return builder -> {
            var map = Arrays.stream(CacheKeyType.values())
                    .collect(
                            Collectors.toMap(
                                    e -> e.getKey()
                                    , e -> RedisCacheConfiguration
                                            .defaultCacheConfig()
                                            .entryTtl(Duration.ofSeconds(e.getTtl()))
                                            .disableCachingNullValues()
                                            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
                                            .serializeValuesWith(RedisSerializationContext.SerializationPair
                                                    .fromSerializer(new GenericJackson2JsonRedisSerializer(this.objectMapper())))
                            ));
            builder.withInitialCacheConfigurations(map);
        };
    }

    /**
     * redisにキャッシュする際のデータの書き読み込みする際のobjectMapperの設定
     *
     * LocalDateTime などの Date and Time API 関連のフィールドを扱う
     * 不明なプロパティがあっても無視
     * オブジェクト情報を追加する
     *
     * @return
     */
    private ObjectMapper objectMapper() {
        return new ObjectMapper()
                .registerModule(new JavaTimeModule())
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
                                .allowIfBaseType(Object.class)
                                .build(),
                        ObjectMapper.DefaultTyping.EVERYTHING)
                ;
    }

    /**
     * キャッシュからのデシリアライズに失敗した場合は、
     * エラーにはせずに対象のメソッドをそのまま呼ぶ
     * その結果はキャッシュされる
     *
     * @return
     */
    @Override
    @Bean
    public CacheErrorHandler errorHandler() {

        return new SimpleCacheErrorHandler() {

            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                if (isSerializationError(exception)) {
                    return;
                }

                throw exception;
            }

            private boolean isSerializationError(RuntimeException exception) {
                if (exception instanceof SerializationException) {
                    return true;
                }

                Throwable cause = exception.getCause();
                return (cause != null && cause instanceof SerializationException);
            }
        };
    }
}

設定を追加したら以下のようにキャッシュされるようになりました。

[
    "クラス名",
    {
        "id": "0000000",
        "longValue": [
            "java.lang.Long",
            7261626
        ],
        "date": [
            "java.time.LocalDateTime",
            "2021-05-25T06:17:30.743"
        ],

Android 12におけるスプラッシュスクリーン

こんにちは。エキサイト株式会社 Androidエンジニアの克です。

Androidアプリでは、起動時に最初の画面が表示されるまで多少の時間がかかり、真っ白の画面がしばらく続いてしまうということがよくあります。

そのため、最初の画面が表示されるまでアプリのロゴを表示しておくというのはよく使われる手法です。(以下スプラッシュスクリーンと呼称)

Android 12ではこのスプラッシュスクリーンにより拡張性が加わりましたが、注意しなければならない点もあったので紹介させていただこうと思います。

※ 今回紹介する内容は Android 12 Beta におけるものです。正式版では仕様が変わっている可能性もあるため詳細については公式のドキュメントを参照してください。

Android 7までの実装

公式で推奨しているようなやり方は特にはありませんでしたが、概ね下記のようにテーマの差し替えという形で実装されてきました。

<!-- theme.xml -->
<resources>

    <!-- 通常のテーマ -->
    <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar" />

    <!-- スプラッシュスクリーンのテーマ -->
    <style name="AppTheme.Splash">
        <item name="android:windowBackground">@drawable/splash_screen</item>
    </style>

</resources>
<!-- splash_screen.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">

    <!-- 画像をそのまま使うと全体に引き伸ばされてしまうので、layer-listを使う -->

    <item>
        <color android:color="#FFF" />
    </item>

    <item>
        <bitmap
            android:gravity="center"
            android:src="@drawable/splash_image" />
    </item>

</layer-list>
<application
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme.Splash"> <!-- スプラッシュスクリーンのテーマを指定  -->
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>
// MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // スプラッシュスクリーンのテーマから通常のテーマに戻す
        setTheme(R.style.AppTheme)

        setContentView(R.layout.activity_main)
    }
}

こうすることで最初の画面が立ち上がるまではスプラッシュスクリーンが表示され、立ち上がった後は通常の表示に切り替わります。

Android 8からの実装

Android 8からは、スプラッシュスクリーン用のAPIとして windowSplashscreenContentが追加されました。

こちらをテーマに設定することで、テーマの切り替えをしなくても自動でスプラッシュスクリーンの表示と非表示をやってくれます。

<resources>

    <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="android:windowSplashscreenContent">@drawable/splash_screen</item>
    </style>

</resources>

Android 12からの実装

Android 12の端末で起動されたアプリは、なんとデフォルトでアプリアイコンがスプラッシュスクリーンとして表示されます。

そのためアプリアイコンをそのままスプラッシュスクリーンに表示するだけの場合は実装は不要です。

別の画像を表示したい場合はAndroid 8からの場合と同様に windowSplashscreenContentを使用します。

Android 12では、他にもSplash screensという形で専用のAPI群が追加され、スプラッシュスクリーンの細かい表示内容や表示時間などのカスタマイズが可能になりました。 developer.android.com

Android 12での注意点

デフォルトでスプラッシュスクリーンが表示されることは喜ばしいことでもありますが、注意点として windowBackgroundで画像を表示していた場合については無視されます。

また、テーマの設定ではなくスプラッシュスクリーン用の画面を別途用意していた場合、スプラッシュスクリーンが2回表示されてしまうことにも繋がります。

さらに、これはtargetSdkVersionAndroid 12未満に設定されている場合でも適用されます。

よってスプラッシュスクリーンを自前で実装しているアプリについては、Android 12の正式版がリリースされる前に動作を確認しておいたほうがよいでしょう。

まとめ

昔はスプラッシュスクリーンの実装方法も統一されておらず、無駄に長い時間ユーザーを拘束してしまうような実装になっている場合もありました。

OSが新しくなるにつれて画一された実装方式になり、よりシンプルで拡張性のある表現をすることができるようになってきています。

スプラッシュスクリーンはブランドイメージを強調するという側面もあるかもしれませんが、ユーザーはロゴを見るためにアプリを起動するのではないということを念頭に置き、使いやすいアプリにしていきたいですね。

🎉Twitterアカウントを開設しました!🎉

f:id:excitech:20210526151104p:plain

お知らせ

こんにちは。excite新卒デザイナーの山﨑です。

この度excitechという技術発信のTwitterアカウントを開設しました!

twitter.com

じゃ〜ん!

f:id:excitech:20210526143256p:plain

アイコンとヘッダーは内波さんという直属のメンターさんが製作してくれました🙏めちゃくちゃかっこいいです!

個人的にロゴの元になったAvenirという書体が大好きなので、すごく嬉しいです〜!Avenirは最強💪

こちらのテックブログの更新情報や、イベントについても随時発信していく予定なので、是非フォローしてみてください!

それでは!

Javaのメソッドの返り値に、オブジェクトを使うべきかインターフェースを使うべきか

こんにちは。 エキサイト株式会社の三浦です。

Javaで配列を扱う時、すべての配列のインターフェースである List と、具体的に実装したオブジェクトである ArrayListLinkedList 等があります。 メソッドの返り値で配列を返したいときはこれらのどれでも使うことが出来ますが、実際にどれを使うべきなのでしょうか?

今回は、メソッドの返り値にインターフェースを使うべきか、それともオブジェクトを使うべきかについて説明していきます。

Javaの配列のインターフェースとオブジェクト

例えばPHPを使っている人にとっては、配列はそれ以上でもそれ以下でもありません。 ですが実はJavaには、「配列」と一口に言っても内部の保存方法によって種類が存在します。

例えば ArrayListLinkedList という形式がありますが、保存方法の違いにより、 ArrayList はランダムアクセスに強く、 LinkedList は要素の追加・削除に強いという特長があります。 そのため、その配列に対してどんな操作をしたいかによって、使う配列の形式を使い分けるのが良いでしょう。

そして、それらはすべて List というインターフェースを実装したものです。

さて、以上のことを踏まえると、配列を返すメソッドを考える時、ざっくり以下の2通りがあることがわかります。

// 返り値が、インターフェースである List
public List<String> getList() {
    return new ArrayList<String>();
}
// 返り値が、オブジェクトである ArrayList
public ArrayList<String> getList() {
    return new ArrayList<String>();
}

どちらでもコードとしては正しく動きますが、どちらを使うべきなのでしょうか? 実はこれは常に正しい方法が決まっているわけではなく、状況次第で変わってきます。

返り値の決定方法

まずは、それぞれのメリットを見ていきます。

インターフェースを使うメリット

  • 各オブジェクトそれぞれに固有のメソッドを使えなくなるので、配列オブジェクトを別の種類に変更しやすい
  • 空配列を使う場合のみに使用する emptyList 等が併用できる

オブジェクトを使うメリット

  • 各オブジェクトに固有のメソッドが使える
  • 使用するオブジェクトの種類を固定できる

以上のことから、まず各オブジェクトに固有のメソッド(例えば、 ArrayList にしか存在しないメソッド)を使いたい場合は、必ず返り値はオブジェクトにする必要があります。 また、パフォーマンス上どうしても使用するオブジェクトを固定する必要がある場合は、返り値をオブジェクトにしてもいいかもしれません。

それ以外の場合は、コードの柔軟性を考えると基本的にはインターフェースを使うのが良いかと思います。 また、仮にパフォーマンス上オブジェクトを固定する場合でも、メソッドの内部で負荷の高い処理が完結する場合や、よほどコードを固くする(ルールをがっちり決める)必要がない限りは返り値はインターフェースで十分なのではないでしょうか。 多くの場合は、コードの柔軟性確保のほうが全体としてメリットが上がる場合が多いです。

まとめ

どうしてもオブジェクトを使わなければならない状況以外では、コード柔軟性のためにインターフェースを使うほうが良いでしょう。

こうしたインターフェースとオブジェクトの仕組みは、配列だけでなくマップ(インターフェースの Map とオブジェクトの HashMap )等にもありますので、そういった状況でも同じ考え方が通用するはずです。

restTemplateでElasticsearchに問い合わせる

エキサイト株式会社 メディア事業部エンジニアの中尾です。

rest-high-level-clientのようなライブラリではなく、restTmeplateでElasticsearchに問い合わせる方法を説明します。

https://mvnrepository.com/artifact/org.elasticsearch.client/elasticsearch-rest-high-level-client

使う場面として、Elasticsearch5系のバージョン以下の場合、Elasticsearchで使っているオプションがライブラリで扱われていない(auto_generate_synonyms_phrase_query)などあるので、

DAOとして、以下のようにJsonのNodeを渡します。

@Component
@RequiredArgsConstructor
public class ElasticSearchDaoImpl implements ElasticsearchDao {

    @Value("${spring.elasticsearch.url}")
    public String url;

    private final RestTemplate restTemplate;

    private final ObjectMapper objectMapper;

    @Override
    public JsonNode searchRequest(JsonNode jsonNode) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> entity = new HttpEntity<String>(jsonNode.toString(), headers);
        ResponseEntity<String> exchange =
                restTemplate.exchange(this.url, HttpMethod.POST, entity, String.class);

        if (exchange.getStatusCode() != HttpStatus.OK) {
            throw new ElasticsearchException(exchange.getBody());
        }
        try {
            return objectMapper.readTree(exchange.getBody());
        } catch (JsonProcessingException e) {
            throw new ElasticsearchException(e.getMessage());
        }
    }
}

どんなJsonNodeを渡すかというと、以下のようにElasticsearchに問い合わせるのに必要そうな値をセットするデータクラスを用意し、SearchSourceBuilder.toStringでstringにした後、objectMapperでJsonNode化します。

JsonNode jsonNode = objectMapper.readTree(elasticSearchForm.toString());
@Data
@Accessors(chain = true)
public class ElasticSearchForm {

    /**
     * Elasticsearchから取得するインデックス
     */
    private String index;

    /**
     * Elasticsearchから取得するカラム
     */
    private String[] includes;

    /**
     * Elasticsearchから取得しないカラム
     */
    private String[] excludes;

    /**
     * 検索条件
     */
    private BoolQueryBuilder boolQueryBuilder;

    /**
     * offset
     */
    private int from = 0;

    /**
     * limit
     */
    private int size = 10;

    /**
     * order
     */
    private FieldSortBuilder fieldSortBuilder;

    public String toString(){
        return new SearchSourceBuilder()
                .fetchSource(
                        includes,
                        excludes)
                .query(boolQueryBuilder)
                .from(from)
                .size(size)
                .sort(fieldSortBuilder)
                .toString();
    }
}

JsonNode化すれば、不要なところは以下のように消すことができます。

※本当はもっとネストしていると思いますが、簡略しています

((ObjectNode) jsonNode.get("query").........remove("auto_generate_synonyms_phrase_query");

あとは、このJsonNodeを先程のDAOの引数に入れるだけです。

レスポンスについて、以下のようにstreamを使えば、必要なデータのListが作れると思います。 他に必要なデータは適宜JsonNodeから取得してください。

                    StreamSupport.stream(
                            response.get("hits").get("hits").spliterator(), false)
                            ).map(e -> {
                                 // 処理
                                 }
                            )
                            .collect(Collectors.toList());

Elasticsearchに問い合わせるJsonはネストが深く、文字列結合やMapだとどうしても見通しが悪くなる場合があります。

Builderを使えるところは使って、それをjsonに変換した方が見通しがよくなると思うので、よかったら使ってください。

Javaのカスタムバリデーションで2つ以上のプロパティをチェックするアノテーション

エキサイト株式会社 メディア開発の佐々木です。

現在、SpringBootで2つ以上のプロパティをチェックするカスタムバリデーションを共有します。

アノテーションの定義

下記のようにカスタムアノテーションを定義します。 (@Constraint(validatedBy = NotBlankAny.NotBlankAnyValidator.class) ここの部分はエラーになります)

@Documented
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotBlankAny.NotBlankAnyValidator.class)
public @interface NotBlankAny {
    String message() default "please {fields} not empty.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String[] fields();
}

バリデーション定義

次にバリデーション部分を定義します。

    class NotBlankAnyValidator implements ConstraintValidator<NotBlankAny,Object> {

        private String[] fields;

        @Override
        public void initialize(NotBlankAny constraintAnnotation) {
            this.fields = constraintAnnotation.fields();  // ※1
        }

        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (Objects.isNull(value)) {
                return false;
            }
            BeanWrapperImpl beanWrapper = new BeanWrapperImpl(value);
            return Stream.of(fields).allMatch(e -> StringUtils.isNotBlank((String)beanWrapper.getPropertyValue(e))); //  ※2
        }
    }

※1アノテーション内で指定されたフィールドの文字列が配列で入ってきます。※2で一つずつ取り出してチェックします。

使い方

対象のデータクラスに、 @NotBlankAny(fields = {"フィールド名1","フィールド名2"}) を定義すると、そのデータを使ってバリデーションがかかります。

@RestController
@RequestMapping
@RequiredArgsConstructor
@Slf4j
public class DemoController {

    @GetMapping
    public Mono index(@Valid Form form) {
        return Mono.defer(() -> Mono.just(form));
    }

    @Data
    @NotBlankAny(fields = {"firstName","lastName"})  // こんな感じで設定します
    static class Form {
        private String firstName;
        private String lastName;
    }
}

これでチェックができるので、再利用性はかなり高いバリデーションができると思います。

最後に

フィールド複数のバリデーションを使いたいときって結構あると思います。メールアドレスの再入力とか、パスワードを同じもの2つ入れるとか。実際にチェックのところは、一致や異なるものやどれかひとつみたいなものは、バリデーション側での実装でバリエーションは増やせると思います。

宣伝

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

www.wantedly.com

おまけ

全体のコード

@Documented
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotBlankAny.NotBlankAnyValidator.class)
public @interface NotBlankAny {
    String message() default "One of {fields} must not be empty";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String[] fields();

    class NotBlankAnyValidator implements ConstraintValidator<NotBlankAny,Object> {

        private String[] fields;

        @Override
        public void initialize(NotBlankAny constraintAnnotation) {
            this.fields = constraintAnnotation.fields();
        }

        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (Objects.isNull(value)) {
                return false;
            }
            BeanWrapperImpl beanWrapper = new BeanWrapperImpl(value);
            return Stream.of(fields).allMatch(e -> StringUtils.isNotBlank((String) beanWrapper.getPropertyValue(e)));
        }
    }
}