【Java】Gsonオブジェクトが、toJsonメソッドを呼び出した際に値がnullのものは、消失してしまう

タイトルだけだとどういうことかわからないので、サンプルを書きます。

目標はGsonを使って以下のようなJsonを生成することです。

{"hoge":"hoge","fuga":null}

Gsonを使ってJsonを生成するのは慣れていた(つもり)だったので以下のようにサラサラと書いてみます。

Map<String, String> map = new HashMap<>();
map.put("hoge", "hoge");
map.put("fuga", null);

Gson gson = new Gson();
String json = gson.toJson(map);

jsonの中身を見ると…

{"hoge":"hoge"}

俺のfugaが消えています…

調べてみると、GsonにはBuilderが用意されてて、そこでserializeNulls()を呼んで create()すると良いみたいです。

Map<String, String> map = new HashMap<>();
map.put("hoge", "hoge");
map.put("fuga", null);
GsonBuilder gsonBuilder = new GsonBuilder().serializeNulls();
Gson gson = gsonBuilder.create();
String json = gson.toJson(map);

結果

{"hoge":"hoge","fuga":null}

fugaを取り戻しました。

serializeNulls()の中身を見ると、GsonBuilderのserializeNullsプロパティがtrueにするような処理が走っていました。
serializeNullsプロパティはデフォルトではfalseになっていました。

そもそも GsonBuilder なるものがあるのを知らなかったですね…勉強になりました。

<参考サイト>

【Quita 】Gsonはデフォルトではnullなフィールドをシリアライズしない

【Java】JSONObjectで、getIntやoptIntで null の場合は0にせず null のままでいてほしい

int 型 はnullにならないので、しょうがないのですが、optIntegerのようなものが欲しいわけです。
本件、タイトルのことを上司に聞いたら即、一番下にある参考サイトを見つけてくれました。
情報収集力に雲泥の差がありますね…。

JSONObjectにそんなものがなかったのでなければ、作りましょうというのが今回のお話です。
ついでにDouble型も作ります。

public class JSONUtil {

    public static Integer optInteger(JSONObject jsonObject, String key) throws JSONException {
        return (jsonObject.isNull(key)) ? null : jsonObject.getInt(key);
    }

    public static Double optWrapperDouble(JSONObject jsonObject, String key) throws JSONException {
        return (jsonObject.isNull(key) || Double.isNaN(jsonObject.getDouble(key))) ? null : jsonObject.getDouble(key);
    }

}

0 とか 入力なしをきちんとわけたいときってどうするのがいいのかいまだに迷います。
Stringは、””でいい気はしますが…。
個人的には、nullは、 すぐNullPointerExceptionにはまるため使いたくないので…。

<参考サイト>

【Stack Overflow】Json Parsing and Nullable int value in android

【Java】Caller パッケージ名 needs to hold android.permission.SCHEDULE_EXACT_ALARM to set exact alarms.

タイトルのエラーに出くわしてしまいました。

AndroidManifest.xml内 で以下のように宣言するだけで良いと思っていたので出くわす理由がわかりませんでした…

<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

公式ドキュメント

AlarmManager を使って正確な時間にアラームを設定してサービスを動かしたかったのでこれで、AlarmManager の setExact() などのexactを含むメソッドを利用できると思っていました。

てか、実際にそう出来る端末がありました。
ただ、他にandroid でタイトルのエラーを起こす機種がいました。

何はともあれ対策です。

どうやら AlarmManager には 正確にスケジューリング出来るか確認できるメソッド canScheduleExactAlarms() があるようです。

android12以上だと使えます。

AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
    alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
} else {
    alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
}

canScheduleExactAlarms() メソッドのドキュメントを見ると

SCHEDULE_EXACT_ALARM権限を持つ、またはデバイスの省電力除外リストに入っているとtrueを返してくれるみたいです。

つまりエラーが起きたということはどちらともダメだったということです。
上述した実際に出来た端末は元々デバイスの省電力除外リストに入れていたからっぽいですね…

なんにせよユーザー側で設定を変えれるので上記のような分岐は行う必要があります。

Exactを使わせたい場合は、Intentで「Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM 」を指定して設定してもらうか省電力モードから外す必要がありそうです。

12への変更点に書いておいて欲しかったです…

SQSのvisibleメッセージの数だけECSタスクを起動する方法

ご無沙汰しております。オンラインコンサルタントの直井です。今回はECS周りについてご紹介いたします。

1. 動機

ECSのタスクをSQSのメッセージ数に応じてオートスケールしたいな…と思ったことがことの発端です。

要は、バッチ処理をメッセージに応じてスケーリングするといった内容です。今回の内容ではクラスター内のサービスだと、メッセージ数に応じてタスクをスケールアウトすることは可能ですが、スケールインの設定が難しいため、このような実装方法になりました。

2. 動作環境

Python:3.9

ECSのタスクは、処理が終了すればコンテナが終了してスケールインの操作が不要な状態

3. Lambdaサンプルコード

import boto3
import os
import time
import datetime

ecs_client = boto3.client("ecs")
cw_client = boto3.client('cloudwatch')

ECS_CLUSTER = ""
TASK_DEFINITION = ""
SUBNET_ID_1 = ""
SUBNET_ID_2 = ""
QUEUE_NAME = ""
def lambda_handler(event, context):
    
    response = cw_client.get_metric_statistics(
               Namespace = 'AWS/SQS',
               MetricName = 'ApproximateNumberOfMessagesVisible',
               Dimensions = [
                            {
                                'Name': 'QueueName',
                                'Value': QUEUE_NAME
                            },
                        ],
                StartTime=datetime.datetime.now() - datetime.timedelta(seconds=60),
                EndTime=datetime.datetime.now(),
               Period = 60,
               Statistics = ['Maximum']
               )
    metric = int(response['Datapoints'][0]['Maximum'])
    if metric > 0 :
        print("Create!!")
        for num in range(metric):
            ecs_client.run_task(
                cluster=ECS_CLUSTER,
                launchType="FARGATE",
                networkConfiguration={
                    "awsvpcConfiguration": {
                        "subnets": [SUBNET_ID_1,SUBNET_ID_2],
                        "assignPublicIp": "ENABLED",
                    }
                },
                taskDefinition=TASK_DEFINITION,
            )
            time.sleep(10)
    else:
        print("None")

ざっくりとコード内の説明

発火した過去1分でSQSで利用できるメッセージ数(visibleCount)を取得して、その数に応じて繰り返し処理でECSのタスクを起動させるようにしています。

visibleカウントは処理中ではないメッセージ数に応じて値が変化します。

4. 発火イベントを仕込む

右上のトリガーの追加を選択

バーに「event」と入力して「EventBridgeを選択」

「新規ルールの作成」を選択肢、「ルール名」と「スケジュール式」を入力して「追加」ボタンをクリック

※1分や1時間の場合は、1 minute, 1 hour だが、2以上であれば 2minutes, 2 hoursになるので注意

5. 最後に

サービスをうまく使ってどうにかできないか考えましたが、今回構成したシステム群が疎結合に作れたので、柔軟性があり助かりました。

ECSではちょいちょいコンソール画面に対応していないもしくは、新UIに対応していないことが多いので戸惑うことが多かったです。

SQSの メッセージ最大サイズ「256KB」にキレてます(>_<)

お久しぶりです。オンラインコンサルタントの直井です(^^♪
皆様いかがお過ごしでしょうか?最近は暑さも和らぎ、比較的過ごしやすい日々が続いております。気温の変化は体調にも表れやすいので、どうぞお気をつけてお過ごしください。

という堅苦しいはいりは置いておいて、今回はタイトルの通りSQSの最大メッセージサイズが256KBということにキレています。このサイズ、微妙に足りなくないですか…?そういうのには向いてないとか、そもそもそんな大きい内容をキューに入れておくな!とか言われるかもしれませんが、入れたいんですわ!入れないと進めないんですわ!なんて状況は多々あると思います。

1. SQSを利用する構成

そもそも、どんな感じでSQSを利用するか?について説明いたします。今回弊社ではルート計算プログラムをサーバレス環境に移行すべく、API Gateway,Lambda, SQSを利用を検討しました。以下の図の通りです。

これまで動いていた環境では、高性能なサーバで常にリクエストを待ち受けて、リクエストがあったら計算する形だったので、待ち受け時間が多いと不要なリソースとして問題がありました。これを解決するため、イベント駆動型に近いサーバレス環境に移行する計画が実行されました。

2. リクエストが肥大化していた

プログラムで計算するための情報は、計算して欲しい地点の位置情報(緯度経度)情報くらいなのでPOSTデータはそこまで大きくないだろう(^^♪ラッキー!標準機能でごり押し移行ができると考えていました。

ですが、実際にリクエストされる内容を確認してみると….その地点の位置情報だけでなく、備考情報やid等計算に必要ない内容もPOSTされており、戦慄しました。また、その内容をそっくりそのまま返す必要があります。

このため、リクエストが、2MBや5MBなどを超えることが多々あります。このことから、SQS標準のメッセージ容量では足りないことが明らかとなりました。

3. メッセージを拡張

このような困った状況でも、AWSは最適なソリューションを用意してくれていました。S3を使えばメッセージの容量を最大2GBまで拡張できるよ!とのことです。これは心強い!2GBもあれば何でもできます。なんでも。というかS3を使った時点で何でもできるような気がしてきました。

https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-s3-messages.html

4. リクエストを受け付けてSQSに登録する

できそうなことがわかったので、とりあえず作っていきます。作る部分は、APIGatewayから受け取るLambda関数です。今回の計算プログラムがScalaで掛かれていたので、それそそっくりそのまま受け継ぎます。Lambdaのランタイムは、java on amazon linux2 を利用します。

受付部分はさっきの公式ドキュメントをそのまま利用します。

    import software.amazon.awssdk.regions.Region
    import software.amazon.awssdk.services.sqs.SqsClient
    import software.amazon.awssdk.services.sqs.model._
    import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
    import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
    import com.amazon.sqs.javamessaging.AmazonSQSExtendedClient
    import com.amazon.sqs.javamessaging.ExtendedClientConfiguration
    import software.amazon.awssdk.services.s3.S3Client
    import software.amazon.awssdk.services.s3.S3ClientBuilder

    def sentSqsMessage(request_json: String): SendMessageResponse = {

        val S3_BUCKET_NAME: String = "sqs_message_bucket_name"
        val AWS_ACCESS_KEY: String = ""
        val AWS_SECRET_KEY: String = ""
        val QUEUE_URL: String = "sqs_queue_url"

        val awsCredentials = StaticCredentialsProvider.create(AwsBasicCredentials.create(AWS_ACCESS_KEY, AWS_SECRET_KEY))
        val s3Client: S3Client = S3Client.builder().region(Region.AP_NORTHEAST_1).credentialsProvider(awsCredentials).httpClient(ApacheHttpClient.create()).build()
        val qsClient: SqsClient = SqsClient.builder().region(Region.AP_NORTHEAST_1).credentialsProvider(awsCredentials).httpClient(ApacheHttpClient.create()).build()
        val extendedClientConfig = new ExtendedClientConfiguration().withPayloadSupportEnabled(s3Client, S3_BUCKET_NAME)
        val sqsExtendClient: AmazonSQSExtendedClient = new AmazonSQSExtendedClient(sqsClient, extendedClientConfig)
        val sendMessageRequest: SendMessageRequest = SendMessageRequest.builder().queueUrl(QUEUE_URL).messageBody(request_json).build()
        val sendResult: SendMessageResponse = sqsExtendClient.sendMessage(sendMessageRequest)

        return sendResult
    }

ざっくりと処理の流れを記載すると、AWS クレデンシャルをを生成し、S3とSQSのクライアントを作成します。その後に、SQSをS3を利用して拡張するためのクライアントを作成しリクエストを投げる流れになります。

5. 最後に

あまり日本語で書かれている記事がなく、試し試しで動かしたことを昨日のことのように思い出します….