複数SIMカードを使い分けている話
どうしてこの記事を書こうと思ったのか
近年、スマートフォンがますます重要なデバイスとなってきており、重要な情報がスマートフォンにいると思います。
仮に利用しているスマートフォンが紛失・破損した際に色々と不便を被る可能性が高くなっています。
そのため、私はスマートフォンを複数保持しており、多重化を行なっています。それに合わせて、SIMカードも複数利用しているのでそれについて紹介していきます。
利用端末
以下の端末を利用しています。
メイン端末: iPhone 14 Pro Max
サブ端末: Pixel 7
iPhone、Androidどちらも利用しておきたいので、各OSで欲しい端末を揃えている形です。
また、iPhoneは毎年Apple Storeで買い替えるようにしています。
iPhoneは綺麗に使っていればリセールバリューが高いので、毎年数万円で最新のiPhoneが利用できます。
Android端末としてPixel 7を選択したのには以下の理由があります。
利用しているSIMカード
そんな中、SIMカードは以下のものを利用しています。
iPhone 14 Pro Max
- IIJ mio タイプD 4ギガプラン 音声SIM (物理SIM)
- povo 2.0 (eSIM)
Pixel 7
- 楽天モバイル (物理SIM)
使い分け
基本的にはiPhone 14 Pro Maxのみを持ち歩いており、メインのSIMカードは IIJ mioとなっています。
IIJ mioが通信できない時や、旅行などでWi-Fiが利用できない時間が長い時は、Povoでトッピングを追加して利用しています。
そのため、こちらは月額 990円で利用できています。(音声通話やPovoの利用がない場合)
Pixel7はほぼ家の中でしか利用していません。そのため、楽天モバイルは 月額 1,078円で利用できています。
解約やPovoなどの維持費が安いSIMに移行する手もありますが、楽天経済圏の恩恵を受けられているため現状は継続予定です。
合計すると、月の通信費は 2,068円となります。
現状はこれだけのコストで運用できているため、非常に満足度が高いです。
一方、上記内容と重複する部分もありますが、不満点です。
今後
auやsoftbankがデュアルSIMを検討するなど、この分野についても新しいサービスがどんどん出てきそうです。
個人的には、リモートワーク主体なので現状であまり不満もありませんが、外出機会が増えてメイン回線の速度や容量で不満を感じるようなことがあれば、メイン回線を ahamoなどに切り替える検討をするかもしれません。
引き続き、情報感度を高くして最適な状態を検討して行きたいと思います。
AirPods Proの修理サービスプログラムに申し込んでみた。
2019年12月に購入した AirPods Proですが、Web会議中に自分が声を発する際、左側のAirPods Proからカサカサという音が聞こえるようになりました。
最初はイヤーチップが合わなくなってきたのかな?と思い、掃除をしてみたり別のサイズに変えてみたりしましたが、症状は改善せず、むしろ徐々に悪化していく状況となりました。
そんな折、AirPods Proの修理プログラムがあることを知り、事象も近そうだということでAppleのサポートに問い合わせをしてみました。
対象の修理プログラムは↓こちら
今回はチャットサポートをお願いしました。
接続まで2分と表示されていましたが、すぐに繋がりました。
チャットサポート
※チャットサポート全体で、20~30分くらいの時間を要しました。
チャットでは主に以下のことを質問されました。
- 事象の確認
- AirPods Proのシリアル番号
- 接続しているiOSのバージョン
- Bluetooth/アクティブノイズキャンセリング ON/OFFで事象が変わらないか
- 耳に装着した状態でタップし、異音がするか
- 傷や水没がないか
諸々のヒアリングの後、無償修理プログラムの対象に該当するということになり、更に追加で以下の情報を確認されました。
- 名前
- 住所
- メールアドレス
その後すぐに、Appleより以下の文面のメールが届き、クレジットカード情報を入力します。
※入力したタイミングで、10,780円(左耳分)のクレジットカード与信枠を確保されます。
※Appleに故障したAirPods Proが到着し、過失による問題でないと確認された後、取り消しされるそうです。
※返送されない、過失が認められた場合はそのまま請求されるようです。
クレジットカード情報を登録したタイミングで完了のメールが届きます。
さらにしばらくすると、今後の流れを記載したメールが届きました。
チャットサポート完了後
チャット自体は夜だったので、発送の連絡は翌日に届き、さらにその翌日に交換品が届きました。
受け取りの際に、故障したAirPods Proの左側からイヤーチップを渡すのですが、
その場で届いた箱を開封 → 中身の小さい箱と説明書を取り出し → 届いた箱に故障したAirPods Proを入れて渡す
という流れで、少し戸惑いました。(Appleの修理プログラム自体が初のため・・)
受け取ったのは説明書と、シンプルな箱です。
箱の中には、左側のAirPods Proの交換品。
さらにイヤーチップの別サイズ。
一番下には、リセットの手順の図が書かれていました。
その後、接続をリセットして新しいAirPodsとして接続し直したところ、無事ノイズも乗らず快適に使えるようになりました。
まとめ
Appleの修理プログラムの利用自体は初めてで、チャットでの確認は少し戸惑う部分もありましたが、交換品のパーツ発送もすぐに行われ、全体のプロセスが洗練されていると感じました。
故障したAirPods Proの到着確認がまだ完了しておらず、請求のキャンセルはまだされていない状況ですが、リファービッシュ品との交換と修理プログラムのスムーズさを考えると妥当かと感じました。
まだAirPods Proは購入してから1年と少ししか使っていないので、今後もバリバリ使っていこうと思います。
macでUS配列とJIS配列の両方を切り替えて使う場合の設定
年末にM1チップを搭載した MacBook Airを購入しました。
購入したモデルは一番下のモデル(8GB RAM / 256GB SSD)です。
MacBook Airを購入した理由
本来は来年発売されると噂されている14インチ MacBook Proまで待ちたい所でしたが、
- メインPCの調子が悪い。
- US配列やRAM16GBへのカスタマイズは1月下旬から2月頭までかかる。
- Amazonでたまたま在庫が復活したのを発見した。(通常は1~2ヶ月待ちの表示)
- MacBook Airは繋ぎとして利用し、高スペックのMacBook Proが発売されたら US配列にカスタマイズして購入する予定
といった理由で購入しました。
しかし、普段(仕事用のMacBook Proや以前使っていたPC)はUS配列のキーボードを使っているため、JIS配列のキーボードで問題ないかという懸念がありました。 実際に使って、使いにくい部分もありながらも「これならいけそう」という状態に持っていけたので、記事として書いてみました。
キーボードの種類
- MacBook Air本体: JIS配列
- Keychron K2: US配列
※ Keychronと書いていますが、通常のUS配列キーボードなら何でも適用できるはずです。
必要なソフトウェア
定番の Karabiner-Elements を使用します。
設定方法
Karabiner-Elementsの preferences...
から、Complex modifications
で設定していきます。
Add Rulesから以下のページを開きます。
karabiner-elements-complex_modifications
Japanese などで検索し、
以下の2つを import します。
- For Japanese (JIS配列をASCII配列風にする設定)
- For Japanese (日本語環境向けの設定) (rev 5)
本体キーボード向けの設定
以下のルールを有効にします。
この設定では、以下の挙動になります。
「英数」: 英数に切り替え / 別のキーと同時押しでCommandとして扱う
「かな」: 日本語入力に切り替え / 別のキーと同時押しでCommandとして扱う
Command: Option
また、Google日本語入力を利用している場合、¥マークの入力で \ (バックスラッシュ) に切り替えることができます。
US配列向けの設定
US配列向けのProfileを設定します。
ここでは、Keychronという名前で設定しました。
ProfileをKeychronにした状態で、以下の設定をしていきます。
この設定では、以下の挙動になります。
左Command: 英数に切り替え / 別のキーと同時押しでCommandとして扱う
右Command: 日本語入力に切り替え / 別のキーと同時押しでCommandとして扱う
利用するキーボードを切り替える場合
面倒ですが、メニューバーのKarabiner-Elementsから、使いたいキーボードに合わせてプロファイルを切り替えます。
また、たまにMacが認識しているキーボードが切り替わらない場合があるので、その際はシステム環境設定 -> キーボード -> キーボードの種類を変更 で再認識させることができます。
まとめ
MacBook Air自体は、他の方のレビューにもある通りサクサクで、通常のユースケースであれば吊るしモデルでも快適に使えるのではと思いました。
やはり普段US配列のキーボードを使っているとJIS配列にはなかなか慣れず、普段はクラムシェルっぽく利用して外付けUSキーボードで運用し、ソファーなどで利用する際に本体のキーボードを使うという運用に落ち着きそうです。
バッテリー持ちも良く、本体キーボードも以前のモデルより打ちやすくなっているため、高性能MacBook Proの発売が楽しみになりました。
新しいKyashCardが到着!
KyashCardとは?
Kyash社が提供している Kyashというサービスの物理カードです。
今回、新たにタッチレス決済に対応しています。
KyashCardは何が嬉しいのか
以下のメリットがあるので、昔からKyashを利用しています。
ポイントの二重取りができる
KyashCardは決済金額の1%がKyashのポイントとして還元されます。
このポイントは1ポイント = 1円としてKyashの残高にチャージすることができます。
さらに、Kyashに登録しているクレジットカードで決済されるため、クレジットカードのポイントも貯まるのでポイントが二重取りできます。
さらに、PayPayのクレジットカードをKyashとして登録すると PayPayの0.5%も上乗せされます。
PayPayしか使えないというお店も多いので、PayPayも登録すると便利です。
1点デメリットとして、KyashCardは3Dセキュアに対応しないため、PayPayでは直近30日で5,000円の制限がかかってしまいます。
また、モバイルSuicaの支払い元カードをKyashに設定すると、Kyashのポイントは貯まりませんがその先のクレジットカードのポイントは通常通り貯まります。
これはクレジットカードの請求元がKyashになるからだと思われます。
KyashCardのデメリット
あまりないのですが、以下はデメリットだと感じています。
- AMERICAN EXPRESSのカードを紐付けできない。
- 3Dセキュアに対応していない。
- 発行するのに900円かかる。
- ポイント還元対象は月12万円の決済まで。
申し込みから手元に届くまで
紆余曲折ありましたが、なんとか無事に手元に到着しました。
時系列
2/25 KyashCardの申し込み開始
2/25 お昼前くらいに申し込み完了
2/26 Kyashの決済が失敗し、ロックされていることが判明し問い合わせ
2/27 問い合わせの回答としてロック解除の連絡があり、解除される
2/27 KyashCardの申し込みのステータスがリセットされており、再度問い合わせ
3/3 テンプレ回答なのでスルーしていたら、申込みステータスが復活
.
.
.
4/11 手元に到着
到着
申し込んだと同じ色のシンプルな封筒で到着しました。
中身もシンプルです。
カードの有効化はアプリから行うことができます。
※上記のQRコードが読み込めなかったので手動で切り替えました。
この画面から数ステップであっという間に登録完了でした。
最後に、旧カードとの比較です。
表面からカード番号や KYASH MEMBERという名前がなくなって非常にシンプルなデザインになりました。
AWS SAMでローカル環境でS3とDynamoDBを扱うLambdaを実行する
この記事は2018年10月26日にQiitaに投稿した記事を移行したものです
この投稿について
Serverlessconf Tokyo 2018で色々と刺激を受け、Lambdaに取り組んでみようと思い、色々と試す上でLambdaをローカル環境で開発や動作確認をするのに色々迷う部分が多かったので、メモとして残したものです。
動作環境
以下のものを使用しています。
# Pythonはvenv $ python --version Python 3.6.6 $ aws --version aws-cli/1.16.24 Python/3.7.0 Darwin/18.0.0 botocore/1.12.14 $ sam --version SAM CLI, version 0.6.0
SAMプロジェクトの作成
sam init
コマンドを使用して空のプロジェクトを作成します。
- -r はruntime
- -n はプロジェクト名
を指定しています。
$ sam init -r python3.6 -n sam-s3-lambda-app
無事に成功すると以下のようなパッケージ構成が作成されます。
$ cd sam-s3-lambda-app $ tree . . ├── README.md ├── hello_world │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ └── app.cpython-37.pyc │ └── app.py ├── requirements.txt ├── template.yaml ├── tests │ └── unit │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ └── test_handler.cpython-37.pyc │ └── test_handler.py
hello_worldの動作確認
まずは自動的に作成された、hello_world関数を実行してみます。
# ライブラリのインストール $ pip install -r requirements.txt -t hello_world/build/ $ cp hello_world/*.py hello_world/build/ # イベントの生成 $ sam local generate-event apigateway aws-proxy > api_event.json # テスト実行 $ sam local invoke HelloWorldFunction --event api_event.json 2018-10-25 18:31:13 Invoking app.lambda_handler (python3.6) Fetching lambci/lambda:python3.6 Docker container image...... . . . {"statusCode": 200, "body": "{\"message\": \"hello world\", \"location\": \"IP Address\"}"}
無事に動作確認ができました。
実際にS3とDynamoDBを扱う部分を実装
LocalStackコンポーネントの作成
今回はS3とDynamoDBをエミュレートするために、LocalStackを使用しました。 https://github.com/localstack/localstack
LocalStack自体はdockerで立ち上げます。
以下のようにdocker-compose.yml
ファイルを作成します。
version: "3.3" services: localstack: container_name: localstack image: localstack/localstack ports: - "4569:4569" - "4572:4572" environment: - SERVICES=dynamodb,s3 - DEFAULT_REGION=ap-northeast-1 - DOCKER_HOST=unix:///var/run/docker.sock
s3とdynamodbがそれぞれ使用するポートはこちら
- s3:4572
- dynamodb:4569
LocalStackの起動 ※初回はイメージのダウンロードがあるため、時間がかかります。
$ docker-compose up
LocalStack用credentialの作成
LocalStack用にcredential情報を追加します。
[localstack] aws_access_key_id = dummy aws_secret_access_key = dummy
[profile localstack] region = ap-northeast-1 output = json
LocalStack上にDynamoDBのテーブルを作成
AWSのドキュメントを参考にテーブルを作成します。 https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Tools.CLI.html
$ aws dynamodb create-table \ > --table-name Music \ > --attribute-definitions \ > AttributeName=Artist,AttributeType=S \ > AttributeName=SongTitle,AttributeType=S \ > --key-schema AttributeName=Artist,KeyType=HASH AttributeName=SongTitle,KeyType=RANGE \ > --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \ > --endpoint-url http://localhost:4569 --profile localstack $ aws dynamodb list-tables --endpoint-url http://localhost:4569 --profile localstack TABLENAMES Music
LocalStack上のS3にテストデータを配置
LocakStackのS3にバケットの作成と、実ファイルの配置を行います。
# bucketの作成 $ aws s3 mb s3://music/ --endpoint-url=http://localhost:4572 --profile localstack make_bucket: music # テストデータのput $ aws s3 cp ./testdata.json s3://music/rawdata/testdata.json --endpoint-url=http://localhost:4572 --profile localstack upload: ./testdata.json to s3://music/rawdata/testdata.json
テストデータは以下の内容で作成しました。
{ "Artist": "Acme Band", "SongTitle": "Happy Day", "AlbumTitle": "Songs About Life" }
関数のベースを作成
まずは依存関係にboto3
を追加します。
boto3
はPythonでS3やDynamoDBなどのリソースを扱うためのライブラリです。
https://github.com/boto/boto3
$ pip install boto3 $ pip freeze > requirements.txt
関数の作成 今回はs3_dynamoという関数にしました。
$ mkdir s3_dynamo $ touch s3_dynamo/__init__.py s3_dynamo/app.py
SAMのテンプレートのResources
セクションに、作成した関数を追記します。
Resources: S3DynamoFunction: Type: AWS::Serverless::Function Properties: CodeUri: s3_dynamo/build/ Handler: app.lambda_handler Runtime: python3.6
ローカル実行用のプロファイルとして、以下のファイルも合わせて作成しました。
{ "S3DynamoFunction": { "AWS_SAM_LOCAL": true } }
S3イベントの作成
SAM CLIの機能で各Lambda関数のイベントを生成できるようになっているので、S3をputするイベント定義を作成します。 https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/test-sam-cli.html
わかりにくいですが、 --key
にはS3のバケット配下のパスを渡します。
$ sam local generate-event s3 put --bucket music --key rawdata/testdata.json --region ap-northeast-1 > s3_event.json
出力されたイベントファイルがこちら。
awsRegion
,bucket.name
,object.key
が合っていれば正しくLambda側で判定できそうです。
{ "Records": [ { "eventVersion": "2.0", "eventSource": "aws:s3", "awsRegion": "ap-northeast-1", . .省略 . "s3": { "s3SchemaVersion": "1.0", "configurationId": "testConfigRule", "bucket": { "name": "music", "ownerIdentity": { "principalId": "EXAMPLE" }, "arn": "arn:aws:s3:::music" }, "object": { "key": "rawdata/testdata.json", "size": 1024, "eTag": "0123456789abcdef0123456789abcdef", "sequencer": "0A1B2C3D4E5F678901" } } } ] }
S3からの読み込み処理を実装
まずはS3からファイルを読み取れることを確認するために、以下のようにS3からファイルを読んで標準出力に表示する部分だけを実装します。
import os import json import boto3 import pprint import urllib.parse if os.getenv("AWS_SAM_LOCAL"): dynamodb = boto3.resource( 'dynamodb', endpoint_url='http://host.docker.internal:4569/' ) s3 = boto3.client( 's3', endpoint_url='http://host.docker.internal:4572/' ) else: dynamodb = boto3.resource('dynamodb') s3 = boto3.client('s3') def lambda_handler(event, context): bucket = event['Records'][0]['s3']['bucket']['name'] key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8') print("[bucket]: " + bucket + " [key]: " + key) try: response = s3.get_object(Bucket=bucket, Key=key) d = json.loads(response['Body'].read()) pprint.pprint(d) except Exception as e: print(e) raise e
env-local.json
を使用して実行するため、今回はS3 / DynamoDBとしては以下の定義が使われます。
sam localで実行する場合もdocker内部から実行されるようで、ホスト名はhost.docker.internal
を使っています。
dynamodb = boto3.resource( 'dynamodb', endpoint_url='http://host.docker.internal:4569/' ) s3 = boto3.client( 's3', endpoint_url='http://host.docker.internal:4572/' )
実際に実行してみます。
$ pip install -r requirements.txt -t s3_dynamo/build $ cp s3_dynamo/*.py s3_dynamo/build/ $ sam local invoke S3DynamoFunction --event s3_event.json --profile localstack # result [bucket]: music [key]: rawdata/testdata.json {'AlbumTitle': 'Songs About Life', 'Artist': 'Acme Band', 'SongTitle': 'Happy Day'}
無事にbuket、keyが渡され、S3ファイルの中身を表示することができています。
DynamoDBへputするコードの実装
以下が最終型です。
import os import json import boto3 import pprint import urllib.parse if os.getenv("AWS_SAM_LOCAL"): dynamodb = boto3.resource( 'dynamodb', endpoint_url='http://host.docker.internal:4569/' ) s3 = boto3.client( 's3', endpoint_url='http://host.docker.internal:4572/' ) else: dynamodb = boto3.resource('dynamodb') s3 = boto3.client('s3') table = dynamodb.Table('Music') def lambda_handler(event, context): bucket = event['Records'][0]['s3']['bucket']['name'] key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8') print("[bucket]: " + bucket + " [key]: " + key) try: response = s3.get_object(Bucket=bucket, Key=key) d = json.loads(response['Body'].read()) pprint.pprint(d) table.put_item( Item=d ) except Exception as e: print(e) raise e
わかりにくいですが、以下のコードを追加しています。
table = dynamodb.Table('Music') . . . table.put_item( Item=d )
LambdaのTimeoutを伸ばす
デフォルトではLambdaの実行時間上限は3秒になっているので、適当に伸ばしておきます。
Globals: Function: Timeout: 100
結果確認
再度実行してみます。
$ cp s3_dynamo/*.py s3_dynamo/build/ $ sam local invoke S3DynamoFunction --event s3_event.json --profile localstack
$ aws dynamodb scan --table-name Music --endpoint-url http://localhost:4569 --profile localstack 1 1 ALBUMTITLE Songs About Life ARTIST Acme Band SONGTITLE Happy Day
無事にDynamoDBのテーブルにデータがputされています。
おわりに
今までLambdaのコンソール上で短いコードを書いて実行するといったことはやったことがありましたが、実際にローカルに開発環境を用意して実行してみるという部分は初めてだったので、ハマりどころや分からない部分も多くありました。 実際にSAMを使用してAWS上にデプロイするにはCloudFormationを使うなど、もう少し学ぶところがありそうだという印象でした。 Serverlessのメリットを享受するために、これから吸収していければと思います。
Spring Boot 1.4系から2.0系へのマイグレーションでやったこと
これは2018年08月07日にQiitaに投稿した記事を移行したものです
はじめに
前回は、SpringBoot 1.5からSpring Boot 2.0へのバージョンアップを行いました。 今回は別のプロジェクトで、1.4から2.0に上げた際にハマった事などを書いていきます。
今回主に苦労したのは、
- 問題の原因が 1.4->1.5の部分なのか、1.5->2.0の部分なのかの切り分けに時間がかかった
- Thymeleafの影響が大きいのでテストが大変
- SpringSessionでのシリアライズ/デシリアライズまわり
- 本番デプロイ時の対策
あたりです。
Spring Bootのバージョンを上げる
spring-boot-starter-parentのバージョンを更新
pom.xmlでSpringBootのバージョンを指定します。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath /> </parent>
HikariPCの依存関係を削除
元々HikariPCを使用していたので、依存関係から削除します。
<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency>
コンパイルしてエラーを潰していく
SpringBootServletInitializerが見つからない
SpringBootServletInitializer
のパッケージが変わっているので、再importする
org.springframework.boot.context.embedded.が見つからない
パッケージが org.springframework.boot.web.servlet.
に変わっているので再importする
DataSourceBuilderが見つからない
パッケージが変わっているので再importする
WebMvcConfigurerAdapterの非推奨化
extends WebMvcConfigurerAdapter
を implements WebMvcConfigurer
に変更する
@EnableWebMvcSecurityの非推奨化
@EnableWebSecurity
に変更する。
https://docs.spring.io/spring-security/site/docs/current/reference/html/mvc.html
org.apache.velocity.appが見つからない
velocity
から mustache
に移行する。
- <dependency> - <groupId>org.apache.velocity</groupId> - <artifactId>velocity</artifactId> - </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-mustache</artifactId> + </dependency>
velocityのテンプレートファイル *.vm
を mustacheのテンプレートファイル *.mustache
に置換する。
※ここは application.properties
などで変更可
mustacheのテンプレートの形式に合わせて
${hoge} ↓ {{hoge}}
こんな感じで全部置き換える
shellとかで一括置換してもいいけど、IntelliJのリファクタでやると呼び出し元コードも発見して教えてくれるので数が少なければIntelliJのリファクタがいいかもしれない。
org.jsonが見つからない
以下の依存関係を追加する
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> </dependency>
以下のパッケージで importしなおす
org.springframework.boot.configurationprocessor.json.JSONObject
SpringApplication.runでエラー
SpringApplication.runの引数が変更になっている
以下のように修正
public static void main(String[] args) { - Object[] objects = { HogeApplication.class, FugaService.class }; - SpringApplication.run(objects, args); + final SpringApplication application = new SpringApplication(HogeApplication.class, FugaService.class); + application.run(args); }
ついでに以下のクラスを継承する
SpringBootServletInitializer
JPAのメソッド変更まわりの対応
黙々と直していく
AutoConfigureTestDatabaseが見つからない
import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestDatabase;
から
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
にパッケージを変更
Thymeleafのマイグレーション
https://www.thymeleaf.org/doc/articles/thymeleaf3migration.html
th:substitutebyを th:replaceに置き換える
こんな感じで機械的に
find src/main/resources -type f -name "*.html" -print | xargs sed -i -e 's/th:substituteby/th:replace/g'
linkタグのcss読み込みのtype="text/css"を削除
こんな感じで機械的に
find src/main/resources -type f -name "*.html" -print | xargs sed -i -e 's@type=\"text/css\"@@g'
inline="text" / inline="inline"を削除
一応中身を見ながら、削除していく。
Scriptタグ内のthymeleafが展開されない
こういうやつ
<script>(window.dataLayer || (window.dataLayer = [])).push(<span th:remove="tag" th:utext="${hoge}"/>)</script>
こんな風に修正する
<script type="text/javascript" th:inline="javascript">/*<![CDATA[*/ (window.dataLayer || (window.dataLayer = [])).push(/*[(${hoge})]*/) /*]]>*/</script>
その他
SpringBootでは@EnableWebMvcがついていたら削除する
SessionScope
SpringSecurityの onAuthenticationSuccess
実行時に @SessionScope
を参照できなくてエラー
Error creating bean with name 'user': Scope 'session' is not active for the current thread;
以下のようなコンフィグファイルを作成しておいておく
package jp.hoge; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestContextListener; import javax.servlet.annotation.WebListener; @Configuration @WebListener public class WebRequestContextListener extends RequestContextListener { }
Hibernate SaveAndFlushメソッドでエラー
java.sql.SQLSyntaxErrorException: Table 'hoge.hibernate_sequence' doesn't exist at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:536) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:513) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:115) at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:1983) at com.mysql.cj.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1826) at com.mysql.cj.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1923) at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
GenerationType
を変更する
- @GeneratedValue(strategy=GenerationType.AUTO) + @GeneratedValue(strategy=GenerationType.IDENTITY)
実行中にHibernateエラー
org.springframework.dao.InvalidDataAccessResourceUsageException: error performing isolated work; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: error performing isolated work at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:242) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:225) at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527) at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242) at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) at com.sun.proxy.$Proxy127.save(Unknown Source) at jp.hoge.service.FugaService.execute(FugaService.java:218) at jp.hoge.controller.FugaController.execute(FugaController.java:101) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498)
HibernateのGenerator mappingsが変更になっている。
application.propertiesに以下の設定を追加することで対処
spring.jpa.hibernate.use-new-id-generator-mappings=false
STGなどへのデプロイ後
プロファイルの読み込みエラー
logback-spring.xml
の <springProfile>
でプロファイルが正しく読み込めずにエラー
executableJarの場合、-Dspring-boot.run.profiles=環境名
のように定義すればいい。
今回のプロジェクトではtomcatにwarをデプロイするので、
application.properties
に spring-boot.run.profiles=環境名
みたいに定義してやる。
実際のところは、pom.xmlでwarファイル生成時にプロファイルを分けているので、以下のように定義しています。
spring-boot.run.profiles=${spring.profiles.active}
<profiles> <profile> <id>local</id> <properties> <spring.profiles.active>local</spring.profiles.active> </properties> </profile> <profile> <id>stg</id> <properties> <spring.profiles.active>stg</spring.profiles.active> </properties> </profile> </profiles>
ビルド時にプロファイル
mvn package -Pstg
という感じで、 application.properties
に埋め込んでいる。
SpringSecurity経由でのログイン後にSession取得後のUserオブジェクトのメンバ変数が全てnullになっている
Userのメンバ変数にアクセスしようとしてNullPointerExepotionが発生 とはいえ、Redisの中身を見てもシリアライズされているため読めない・・・
※Memberは @SessionScope
一旦、以下のような感じでJSONに変換してRedisに登録してくれるSerializerを作成する
@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "redis", matchIfMissing = false) @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 20 * 24 * 60 * 60) //default=1800 -> 20days @Configuration public class HttpSessionConfig implements BeanClassLoaderAware { @Autowired private LettuceConnectionFactory lettuceConnectionFactory; private ClassLoader classLoader; @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { final ObjectMapper mapper = new ObjectMapper() .registerModules(SecurityJackson2Modules.getModules(classLoader)) .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); return new GenericJackson2JsonRedisSerializer(mapper); } // Elasticache用 @Bean public static ConfigureRedisAction configureRedisAction() { return ConfigureRedisAction.NO_OP; } @Override public void setBeanClassLoader(final ClassLoader classLoader) { this.classLoader = classLoader; } }
そうするとlogin完了後からControllerに遷移する際に、全フィールドnullの状態でRedisにセットされていた。
ログイン後のControllerで User
オブジェクト内の id
がnullかどうか判定し、nullであれば詰め直すように対応。
ログイン以降は問題なくセッションを扱えている。
本当はJSON形式でセッション情報を持てるようにしたかったものの、Userクラスの構造が複雑なネスト構造になっていたため、時間との兼ね合いで断念・・・ Userクラスに情報をもたせすぎ・・・・
本番デプロイ時の対応
SpringSession のバージョンアップに伴い、内部的にシリアライズ・デシリアライズで扱う SerialVersionUID
が変わっているらしいので、バージョンアップしたコードをデプロイすると、既にログイン中のユーザーはセッション情報をデシリアライズできなくて詰む。
なので、デシリアライズするときの対応を入れる。
https://sdqali.in/blog/2016/11/02/handling-deserialization-errors-in-spring-redis-sessions/
少し記事が古く、依存ライブラリが変更になっているので、以下の通り変更している。
- public class HttpSessionConfig { + public class HttpSessionConfig extends RedisHttpSessionConfiguration { + @Autowired + RedisTemplate<Object, Object> redisTemplate; + @Bean + @Override + public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) { + return super.springSessionRepositoryFilter(new SafeDeserializationRepository<>(sessionRepository, redisTemplate)); + }
public class SafeDeserializationRepository<S extends Session> implements SessionRepository<S> { private final SessionRepository<S> delegate; private final RedisTemplate<Object, Object> redisTemplate; private static final String BOUNDED_HASH_KEY_PREFIX = "spring:session:sessions:"; public SafeDeserializationRepository(SessionRepository<S> delegate, RedisTemplate<Object, Object> redisTemplate) { this.delegate = delegate; this.redisTemplate = redisTemplate; } @Override public S createSession() { return delegate.createSession(); } @Override public void save(S session) { delegate.save(session); } @Override public S findById(String id) { try { return delegate.findById(id); } catch(SerializationException e) { log.info("Deleting non-deserializable session with key {}", id); redisTemplate.delete(BOUNDED_HASH_KEY_PREFIX + id); return null; } } @Override public void deleteById(String id) { delegate.deleteById(id); } }
これで既存のユーザーは、一度ログアウトさせられ、再ログインで新しいSessionデータをRedisに保存し、以降はそちらを参照する。
まとめ
大体、上記の対応で本番環境で動作するようになりました。 (他にもある気がするけど)
また、本来であればTomcatは8.5系以上にしなければならないのですが、8.0で問題なさそうであったため、Tomcatのバージョンアップは見送りました。
SpringBootのメジャーバージョンをいくつか飛ばしてしまうと、とてもつらいのでバージョンアップはこまめにやろう・・・!!
参考ページ
Spring Boot 1.5系から2.0系へのマイグレーションでやったこと
これは2018年07月01日にQiitaに投稿した記事を移行したものです
はじめに
こちらでKotlin対応を行ったプロジェクトに対して、SpringBoot 2.0.2にアップデートを行いました。 ここではそのときに発生した作業などについて書いていきます。
※Spring Boot2.0からはJava8以降が必須になっています。
また、このプロジェクトはREST APIのサーバーとして動作しているため、Thymeleafのマイグレーションなどは回避することができました。 別途、Spring MVCのプロジェクトをマイグレーションしているので、そのあたりは別で書こうと思います。
以下のサイトや記事を主に参考にさせていただきました。
まずはSpring Bootのバージョンアップ
pom.xml
を修正してSpring Bootのバージョンを上げていきます。
変更点を抜粋
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>1.5.8.RELEASE</version> + <version>2.0.2.RELEASE</version> <relativePath /> </parent> ・・・ <dependency> <!-- https://docs.spring.io/spring-session/docs/current/reference/html5/guides/java-redis.html --> <groupId>org.springframework.session</groupId> - <artifactId>spring-session</artifactId> + <artifactId>spring-session-data-redis</artifactId> </dependency> ・・・ <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#jackson--json-support --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-json</artifactId> + </dependency> ・・・ <!- 元々HikariPCを使用していたので依存関係を削除 --> <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#configuring-a-datasource --> - <dependency> - <groupId>com.zaxxer</groupId> - <artifactId>HikariCP</artifactId> - </dependency> ・・・ <!-- ここはテストが終わったら削除 -> <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#before-you-start --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-properties-migrator</artifactId> + <scope>runtime</scope> + </dependency>
ApplicationクラスのWebMvcConfigurerAdapterを使わないようにする
- public class XxxApplication extends WebMvcConfigurerAdapter { + public class XxxApplication implements WebMvcConfigurer{
ビルド->修正->再ビルド
pom.xml
の設定が終わったら、ビルドして修正を繰り返していきます。
JPA RepositoryのfindOneでコンパイルエラー
まずは以下のようなエラーが発生しました。
Error:(41, 41) Kotlin: Type inference failed: fun <S : #{テーブル名}!> findOne(p0: Example<S!>!): Optional<S!>! cannot be applied to (Long) Error:(41, 49) Kotlin: Type mismatch: inferred type is Long but Example<(???..???)>! was expected
これはSpring DataのCRUDメソッドが変更になっていることに起因します。 https://spring.io/blog/2017/06/20/a-preview-on-spring-data-kay#improved-naming-for-crud-repository-methods
1行目のエラーは、findOne
からfindById
に変更する。
2行目のエラーは、findById
の戻り値がJavaのOptional
になっているので、Optional
からget
する。
同様に、save
, delete
の引数もList
を渡すものからEntity
を渡すものに変わっているため、代わりに saveAll
, deleteAll
メソッドを呼ぶようにする。
もちろん、1レコードのみが影響を受けるのであれば、save
、delete
を使う方がいいので、マイグレーションに割ける時間と相談で。
ConfigureRedisActionを解決できない
Error:(13, 53) java: パッケージorg.springframework.session.data.redis.configは存在しません
順番が前後していますが、ElastiCacheを利用するために設定している以下の定義で、ConfigureRedisAction
が解決できなくなっていました。
これはspring-session-data-redis
というパッケージに切り出されたためです。
https://docs.spring.io/spring-session/docs/current/reference/html5/guides/java-redis.html
@Bean public static ConfigureRedisAction configureRedisAction() { return ConfigureRedisAction.NO_OP; }
修正内容はpom.xml
のspring-session
の部分を参照。
また、lettuce
の依存関係を追加していないのは、元々spring-boot-starter-data-redis
が依存関係に含まれているためです。
org.hibernate.validator.constraints.NotBlankなどの非推奨化
これに合わせて、javax.validation.constraints.NotEmpty
などを使うように置き換えています。
jdbcのautoconfigureでエラー
Error:(7, 51) java: シンボルを見つけられません シンボル: クラス DataSourceBuilder 場所: パッケージ org.springframework.boot.autoconfigure.jdbc
これは複数のデータソースを扱っているために発生しているようでした。
hoge.datasource.driver-class-name = com.mysql.jdbc.Driver hoge.datasource.url = jdbc:mysql://127.0.0.1:33306/hoge hoge.datasource.username = hoge hoge.datasource.password = fuga # その他は省略 # 上記x複数データソース分
元々は以下のように設定されていました
@Configuration @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "hogeEntityManagerFactory", transactionManagerRef = "hogeTransactionManager", basePackages = { "jp.xxx.data.hoge.repository" }) public class HogeDatabaseConfig extends AbstractDatabaseConfig { @Bean(name = "hogeDataSource") @ConfigurationProperties(prefix = "hoge.datasource") public DataSource hogeDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "hogeEntityManagerFactory") public LocalContainerEntityManagerFactoryBean hogeEntityManagerFactory(EntityManagerFactoryBuilder builder, return builder.dataSource(hogeDataSource) .packages("jp.xxx.data.hoge.entity").persistenceUnit("hoge") .properties(buildProperties()).build(); } @Bean(name = "hogeTransactionManager") public PlatformTransactionManager hogeTransactionManager( @Qualifier("hogeEntityManagerFactory") EntityManagerFactory hogeEntityManagerFactory) { return new JpaTransactionManager(hogeEntityManagerFactory); } }
が、以下のように修正しました。 この部分についてはもう少しうまいやり方があるんじゃないかなーとは思っています。
@Configuration @ConfigurationProperties(prefix = "hoge.datasource") @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "hogeEntityManagerFactory", transactionManagerRef = "hogeTransactionManager", basePackages = { "jp.xxx.data.hoge.repository" }) public class HogeDatabaseConfig extends AbstractDatabaseConfig { @Autowired private Environment env; @Bean(name = "hogeDataSource") public DataSource hogeDataSource() { final DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(env.getProperty("hoge.datasource.driver-class-name")); dataSource.setUrl(env.getProperty("hoge.datasource.url")); dataSource.setUsername(env.getProperty("hoge.datasource.username")); dataSource.setPassword(env.getProperty("hoge.datasource.password")); return dataSource; } @Bean(name = "hogeEntityManagerFactory") public EntityManagerFactory hogeEntityManagerFactory( @Qualifier("hogeDataSource") DataSource hogeDataSource) { final LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); factoryBean.setDataSource(hogeDataSource); factoryBean.setPersistenceUnitName("hoge"); factoryBean.setPackagesToScan("jp.xxx.data.hoge.entity"); factoryBean.setJpaPropertyMap(buildProperties()); factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); factoryBean.afterPropertiesSet(); return factoryBean.getNativeEntityManagerFactory(); } @Bean(name = "hogeTransactionManager") public PlatformTransactionManager hogeTransactionManager( @Qualifier("hogeEntityManagerFactory") EntityManagerFactory hogeEntityManagerFactory) { return new JpaTransactionManager(hogeEntityManagerFactory); } }
ここでTomcatは起動
Tomcatは起動したものの、以下のエラーログが流れていました。
java.lang.ClassCastException: jp.xxx.service.HogeService$Fuga cannot be cast to org.springframework.core.io.support.ResourceRegion at org.springframework.http.converter.ResourceRegionHttpMessageConverter.writeResourceRegionCollection(ResourceRegionHttpMessageConverter.java:182) at org.springframework.http.converter.ResourceRegionHttpMessageConverter.writeInternal(ResourceRegionHttpMessageConverter.java:139) at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:102) at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:272) at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:180) at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:119) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866) at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
データソースにはPostgreSQLもあるので発生しているようです。
これはhibernate.properties
を作成して配置しておくことで回避しました。
hibernate.jdbc.lob.non_contextual_creation = true
ここまでで起動までにエラーは発生しなくなりました。
突然のClassCastException
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method jp.xxx.controller.HogeController.createFuga, parameter form at jp.xxx.controller.HogeController.createCampaign(HogeController.kt) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877) at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
form
の引数の型をHogeForm
からHogeForm?
のnull許容型に変換して対応しました。
日付型の変換エラー
Failed to convert value of type 'java.lang.String[]' to required type 'java.util.Date'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2018-06-11T00:00:00'; nested exception is java.lang.IllegalArgumentException
ここは少しハマって原因がよくわからない部分ではあったのですが、データを受け取っていたオブジェクトがKotlinのDataクラスだったものを、普通のKotlinクラスにすることで動作しました。
PUTだとformの値が渡らない
原因を調べる時間がなかったのでPUTしている部分をPOSTに書き換えました・・・
REST Controllerの戻り値の型がList<Object>
だとJSONに変換できずにエラーになる。
エラーログ取り忘れ。
元々はこんな実装 そもそもList
@GetMapping("/hoge") public List<Object> hoge() { return hogeService.hoges(); }
戻りの型が結構複雑だったので、Controller側でJSONにして返すようにしました。
@GetMapping("/hoge") public String hoge() throws JsonProcessingException { final List<Object> hoges = hogeService.hoges(); final ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(hoges); }
以上で、マイグレーションが完了し、移行させることができました。