20210827のJavaに関する記事は13件です。

マルチスレッドで動作するマイクラサーバーがあるというので試してみる

Chiyogamiという、Paperをフォークしたマルチスレッドに対応するサーバーソフトがあるというので試してみました。 ビルド サーバーのjarファイルはそのまま落とせず、各自ビルドして利用するようになっています。 https://github.com/Be4rJP/Chiyogami にて日本語による解説があるのでそちらを参考に。 利用するサーバーのバージョンに対応したBranchを選ぶ 現在は1.15.2、1.16.5、1.17.1の3つが用意されています。 今回は1.17.1を選択。 必要ライブラリ等の導入 1.17.1の場合はgit、jdk16が、それ以外の場合はgit、maven、jdk8が必要です。 説明すると長いので各自で導入されてください。 ビルド BranchのREADMEにあるダウンロードリンクもしくはリポジトリをダウンロードします。 解凍後そのディレクトリでWinidowsの場合はgit-bash、linuxもしくはMacの場合はターミナルを開き./buildChiyogami.shを実行します。 自分の環境だと始めビルドに失敗しましたが、2,3回ほど./buildChiyogami.shを実行し、そのフォルダを削除してもう一度解凍からやり直したところ成功しました。 テスト いよいよビルドも完了したので起動していきます。 ビルドによってできたChiyogami-1.1x.x(ビルドしたバージョン).jarを適当な新規フォルダに移し(しなくても良さそうだけど一応)、適当に実行します。 通常のものと同様最初はEULAで弾かれるので、EULA=trueにして再実行。 …? はい。実はこれ現時点ではtimingsが有効化されていると起動に失敗するようです。 timingsの無効化 調べたところ、paper.ymlを変更する模様。 paper.yml(80行目ぐらい) timings: enabled: true -> ここをfalseに これでいざもう一度。 これで起動するかと思います。 CPU使用率もしっかりと分散されているかと思います。 チャンク読み込みそこそこ上げてもついてきてくれてる。 プラグインの動作テスト そのままでは快適に動作することがわかりましたが、プラグインを導入した際にどのような挙動をするのかも見ていきます。 今回導入したのはGeyserとfloodgate、dynmaapの3つです。 以前試したところエラーで起動できませんでしたが修正されたそうなのでそこも含めて試してみます。 Paperの派生なのでSpigot版の物を使用。 何のエラーも吐かずすんなり起動しました。 総括 特にバグ等もなく、プラグインも普通に動くため普通にサーバー運用も可能だと思います。 なにか間違い等あればご指摘ください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSのEC2インスタンスでAmazon Correttoを用いてJDK16を導入する

とあるサーバー向けソフトウェアでJava16を必要とする事があった。 すでにAmazon Corretto 8を用いてJDK8がインストールされていたが、 AmazonLinuxならAmazon CorrettoというOpenJDK互換のソフトウェアが簡単にインストールとバージョン切り替えが可能なのでその方法を説明する。 この記事ではAmazon Corretto 16のインストールについて解説するが、Corretto 8については次の記事を参考にしてほしい。→AWSのEC2インスタンスでAmazon Correttoを用いてJDK8を導入する 環境 AWS EC2 t4g.medium ARM AmazonLinux2 JDK 1.8.0_302 がインストール済み 今回はARM版のAmazonLinux2を利用しているが、x86でも手順はほぼ同様だと思われる。 インストール 今回はyumからインストールを行う。rpmからマニュアルでインストールする方法はこちら→AWSのマニュアル Amazon Linux2でyumリポジトリを有効にする。 AWSが用意したAmazon Corretto用のリポジトリの公開鍵をインポートし、リポジトリをシステムに追加する。 sudo rpm --import https://yum.corretto.aws/corretto.key sudo curl -L -o /etc/yum.repos.d/corretto.repo https://yum.corretto.aws/corretto.repo リポジトリの追加後、以下コマンドでインストールを行う。 sudo yum install -y java-16-amazon-corretto-devel Amazon Corretto 8の場合と比較すると今回インストールするのはJDKだと予測できるが、公式ドキュメントにCorretto 16でのJDK,JREについての記載はなくJDKのリンクのみが記載されていた。 Amazon Corretto 16は/usr/lib/jvm/java-16-amazon-correttoにインストールされる。 Corretto 8の場合と異なり、ディレクトリ名にCPUアーキテクチャーの記載が無いが、yumのログを見ると Downloading packages: java-16-amazon-corretto-devel-16.0.2.7-1.aarch64.rpm | 197 MB 00:00:08 Running transaction check Running transaction test Transaction test succeeded Running transaction Installing : 1:java-16-amazon-corretto-devel-16.0.2.7-1.aarch64 1/1 Verifying : 1:java-16-amazon-corretto-devel-16.0.2.7-1.aarch64 1/1 となっているのでアーキテクチャーに応じたソフトウェアが適宜インストールされているようだ。 バージョンの切り替え 環境によってはすでにJavaが入っており、java --versionで旧バージョンが有効になってしまうことがある。 java -version openjdk version "1.8.0_302" OpenJDK Runtime Environment Corretto-8.302.08.1 (build 1.8.0_302-b08) OpenJDK 64-Bit Server VM Corretto-8.302.08.1 (build 25.302-b08, mixed mode) これは以下コマンドを利用してJavaの切り替えを行い対応する。 sudo alternatives --config java There are 2 programs which provide 'java'. Selection Command ----------------------------------------------- + 1 /usr/lib/jvm/java-1.8.0-amazon-corretto.aarch64/jre/bin/java * 2 /usr/lib/jvm/java-16-amazon-corretto/bin/java Enter to keep the current selection[+], or type selection number: 2 変更を確認する。 java -version openjdk version "16.0.2" 2021-07-20 OpenJDK Runtime Environment Corretto-16.0.2.7.1 (build 16.0.2+7) OpenJDK 64-Bit Server VM Corretto-16.0.2.7.1 (build 16.0.2+7, mixed mode, sharing) 変更が確認できた。 なお、JDKを使用している場合は以下コマンドも合わせて実行する必要がある場合がある。 sudo alternatives --config javac 参考記事 公式のAmazon Corretto 16インストール記事 https://docs.aws.amazon.com/corretto/latest/corretto-16-ug/generic-linux-install.html#rpm-linux-install-instruct 私が過去に執筆したAmazon Corretto 8についてのQiitaの記事 https://qiita.com/honahuku/items/e5464bb3b102710b555a
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaでCOMポートを開いた

あらまし 先日こんなことがあった 解決したので報告 まず本プロジェクトの環境周りを プログラムはほぼ触らずに新しいパソコンへ移植というお仕事 Windows 7 32bit -> Windows Server 2016 64bit Java6 -> Java8 シリアル通信で外部機械との通信あり、シリアル通信にはRXTX-2.1-7というライブラリを使っている 次に起きた問題 // RS232Cポートをオープンする。この処理を実行すると CommPortIdentifier RSport = CommPortIdentifier.getPortIdentifier("COM1"); // このエラーになる。( rxtxParallel.dll と rxtxSerial.dll を見つけられませんだって ) > java.lang.NoClassDefFoundError: no rxtxSerial in java.library.path thrown while loading gnu.io.RXTXCommDriver // Javaが64bitだとこのエラーになる。( 呼び出した.dllが32bit版だから動かせないよだって) > java.lang.UnsatisfiedLinkError: C:\Windows\System32\rxtxSerial.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform thrown while loading gnu.io.RXTXCommDriver やったこと Javaの再インストール(64bit版をアンインストールして32bit版をインストールした) Javaだから環境依存しないでしょ?と何も考えずに64bit版のJavaを入れたのも失敗 上にも書いたけどDLLが32bit版の場合は32bit版のJavaからでないと呼び出せない ちなみに64bit版ライブラリもあるようだ。こちらだったら64bit版Javaでいいかもしれない。(が今回は32bitで押し通す) C:\Windows\SysWOW64にrxtxParallel.dllとrxtxSerial.dllを配置 以前は32bitOSだからsytem32でよかったけど64bitOSで32bitのライブラリを使うならここに入れる。 なぜならWindowsがリダイレクトするので。 RXTXcomm.jarのパスをきっちりクラスパスに設定する 試しに書いたサンプルコードを以下に記す メモ帳で即席で書いたため汚いのはご愛敬で run.bat @echo off @cd %~dp0 set JAVA_CLASSPATH=%任意のフォルダ%\RXTXcomm.jar rem ========= rem コンパイル rem ========= javac -cp "%JAVA_CLASSPATH%" Test.java @if %errorlevel% equ 0 (cls) else (pause) rem ========= rem java実行 rem ========= java -cp "%JAVA_CLASSPATH%" Test pause Test.java import gnu.io.CommPortIdentifier; class Test{ public static void main(String[] arg){ showJavaLibrary(); try{ // RS232Cポートをオープンする。 CommPortIdentifier RSport = CommPortIdentifier.getPortIdentifier("COM3"); }catch(Exception e){ System.out.println(e.toString()); } } public static void showJavaLibrary(){ System.out.println("パス出力ここから================="); for (String str : System.getProperty("java.library.path").split(";")){ System.out.println(str); } System.out.println("パス出力ここまで================="); } } さて実環境を直してきますか
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaでシリアル通信するためにRXTXライブラリを使ってCOMポートを開いた

あらまし 先日こんなことがあった 解決したので報告 まず本プロジェクトの環境周りを プログラムはほぼ触らずに新しいパソコンへ移植というお仕事 Windows 7 32bit -> Windows Server 2016 64bit Java6 -> Java8 シリアル通信で外部機械との通信あり、シリアル通信にはRXTX-2.1-7というライブラリを使っている 次に起きた問題 // RS232Cポートをオープンする。この処理を実行すると CommPortIdentifier RSport = CommPortIdentifier.getPortIdentifier("COM1"); // このエラーになる。( rxtxParallel.dll と rxtxSerial.dll を見つけられませんだって ) > java.lang.NoClassDefFoundError: no rxtxSerial in java.library.path thrown while loading gnu.io.RXTXCommDriver // Javaが64bitだとこのエラーになる。( 呼び出した.dllが32bit版だから動かせないよだって) > java.lang.UnsatisfiedLinkError: C:\Windows\System32\rxtxSerial.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform thrown while loading gnu.io.RXTXCommDriver やったこと Javaの再インストール(64bit版をアンインストールして32bit版をインストールした) Javaだから環境依存しないでしょ?と何も考えずに64bit版のJavaを入れたのも失敗 上にも書いたけどDLLが32bit版の場合は32bit版のJavaからでないと呼び出せない ちなみに64bit版ライブラリもあるようだ。こちらだったら64bit版Javaでいいかもしれない。(が今回は32bitで押し通す) C:\Windows\SysWOW64にrxtxParallel.dllとrxtxSerial.dllを配置 以前は32bitOSだからsytem32でよかったけど64bitOSで32bitのライブラリを使うならここに入れる。 なぜならWindowsがリダイレクトするので。 RXTXcomm.jarのパスをきっちりクラスパスに設定する 試しに書いたサンプルコードを以下に記す メモ帳で即席で書いたため汚いのはご愛敬で run.bat @echo off @cd %~dp0 set JAVA_CLASSPATH=%任意のフォルダ%\RXTXcomm.jar rem ========= rem コンパイル rem ========= javac -cp "%JAVA_CLASSPATH%" Test.java @if %errorlevel% equ 0 (cls) else (pause) rem ========= rem java実行 rem ========= java -cp "%JAVA_CLASSPATH%" Test pause Test.java import gnu.io.CommPortIdentifier; class Test{ public static void main(String[] arg){ showJavaLibrary(); try{ // RS232Cポートをオープンする。 CommPortIdentifier RSport = CommPortIdentifier.getPortIdentifier("COM3"); }catch(Exception e){ System.out.println(e.toString()); } } public static void showJavaLibrary(){ System.out.println("パス出力ここから================="); for (String str : System.getProperty("java.library.path").split(";")){ System.out.println(str); } System.out.println("パス出力ここまで================="); } } さて実環境を直してきますか
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSのEC2インスタンスでAmazon Correttoを用いてJDK8を導入する

とあるサーバー向けソフトウェアでJava16を必要とする事があった。 AmazonLinuxならAmazon CorrettoというOpenJDK互換のソフトウェアが簡単にインストールとバージョン切り替えが可能なのでその方法を説明する。 この記事ではAmazon Corretto 8のインストールについて解説するが、Corretto 16については次の記事を参考にしてほしい。→AWSのEC2インスタンスでAmazon Correttoを用いてJDK16を導入する 環境 AWS EC2 t4g.medium ARM AmazonLinux2 Javaは未インストール 今回はARM版のAL2を利用しているが、x86でも手順はほぼ同様だと思われる。 インストール 今回はyumからインストールを行う。rpmからマニュアルでインストールする方法はこちら→AWSのマニュアル Amazon Linux2でyumリポジトリを有効にする。 sudo amazon-linux-extras enable corretto8 Amazon Corretto 8は、ランタイム環境(JRE)及び完全開発環境(JDK)の2種類が用意されている。 なお、JDKにはJREが含まれるのでJDKをインストールするならばJREをインストールする必要はない。 Amazon Corretto 8をJREとしてインストールする場合 sudo yum install java-1.8.0-amazon-corretto Amazon Corretto 8をJDKとしてインストールする場合 sudo yum install java-1.8.0-amazon-corretto-devel Amazon Corretto 8は/usr/lib/jvm/java-1.8.0-amazon-corretto.<cpu_arch>にインストールされる。 バージョンの切り替え 環境によってはすでにJavaが入っており、java --versionで旧バージョンが有効になってしまうことがある。 java -version openjdk version "1.8.0_302" OpenJDK Runtime Environment Corretto-8.302.08.1 (build 1.8.0_302-b08) OpenJDK 64-Bit Server VM Corretto-8.302.08.1 (build 25.302-b08, mixed mode) これは以下コマンドを利用してJavaの切り替えを行い対応する。 sudo alternatives --config java There are 2 programs which provide 'java'. Selection Command ----------------------------------------------- + 1 /usr/lib/jvm/java-1.8.0-amazon-corretto.aarch64/jre/bin/java * 2 /usr/lib/jvm/java-16-amazon-corretto/bin/java Enter to keep the current selection[+], or type selection number: 2 変更を確認する。 java -version openjdk version "16.0.2" 2021-07-20 OpenJDK Runtime Environment Corretto-16.0.2.7.1 (build 16.0.2+7) OpenJDK 64-Bit Server VM Corretto-16.0.2.7.1 (build 16.0.2+7, mixed mode, sharing) 変更が確認できた。 なお、JDKを使用している場合は以下コマンドも合わせて実行する必要がある場合がある。 sudo alternatives --config javac 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

単純すぎるドメイン駆動設計Javaサンプルコード

背景 チーム内でドメイン駆動設計(以降、DDDと記述)についてちょっとした勉強会をやることになった際、いわゆる手続き型設計との違いについて、具体的なコード例で説明したいと思い、サンプルコードを用意してみました。チーム内でよく使用されているJavaで記述しています。 勉強会参加者にはプログラミングに疎いメンバもいるので、なるべく理解するのに負担が少ないよう、できる限り単純な例にすることを意識しました。 一方で、実際にはDDDを取り入れるまでもない(むしろ取り入れない方がよい)ほど例が単純すぎることによって、DDDの必要性・効果がうまく伝わらず、むしろ面倒なことをしている印象になってしまう懸念はありますが、概要説明用と割り切っている点はご了承ください。 なお、前提として、そもそもDDDとは何かという部分については、個人的に以下のスライドが内容や流れがわかりやすかったので、勉強会でもそのまま紹介させていただくことにしました。 注意点として、DDDでより本質的なのは、ドメインエキスパートと開発者のコミュニケーション、知識のかみ砕き、モデルへの落とし込み、モデルとコードの一致といった部分だと思っていますが、本記事でこれから紹介するのは、あくまでモデルを実装に落とし込んだコードの例であり、実装手段の一部にすぎません。このような実装手段の部分のみにフォーカスした状態は「軽量DDD」と呼ばれることがあり、賛否両論ある点はご注意ください。 説明の流れ Javaで書いたごく単純な足し算アプリを例にして説明します。 まずは、DDDとの対比で手続き型設計と呼ばれるような書き方で書いてみます。 public class TransactionAddition01 { public static void main(String[] args) { // 引数チェック if (args.length != 2) { System.err.println("引数を2つ指定してください"); System.exit(1); } int param1 = 0; int param2 = 0; try { param1 = Integer.parseInt(args[0]); param2 = Integer.parseInt(args[1]); } catch (NumberFormatException e) { System.err.println("引数には整数を指定してください"); System.exit(1); } // 足し算を実行 int result = param1 + param2; // 結果を表示 System.out.printf("%s + %s = %s%n", param1, param2, result); } } これを起点として、以下の流れで説明していきます。 単純すぎるドメイン駆動設計Javaサンプルコード (1) DDDらしいコード まずは、上記コードをDDDらしく修正していきます。 ポイント ドメインの隔離 レイヤ化アーキテクチャ 値オブジェクト 単純すぎるドメイン駆動設計Javaサンプルコード (2) 仕様追加その1 次に、ちょっとした仕様追加が発生した際に、手続き型設計とDDDのそれぞれの場合の対応例を考えます。 ポイント ドメイン知識の凝集 単純すぎるドメイン駆動設計Javaサンプルコード (3) 仕様追加その2 さらに仕様追加が発生し、データの永続化が必要となるケースの対応例を説明します。 ポイント エンティティ リポジトリ 単純すぎるドメイン駆動設計Javaサンプルコード (4) テスト 最後に、手続き型設計とDDDのそれぞれの場合で、テストがどうなるかを説明します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

単純すぎるドメイン駆動設計Javaサンプルコード (1) DDDらしいコード

単純すぎるドメイン駆動設計Javaサンプルコード 単純すぎるドメイン駆動設計Javaサンプルコード (1) DDDらしいコード <-ここ 単純すぎるドメイン駆動設計Javaサンプルコード (2) 仕様追加その1 単純すぎるドメイン駆動設計Javaサンプルコード (3) 仕様追加その2 単純すぎるドメイン駆動設計Javaサンプルコード (4) テスト 前述のコードをDDDらしく修正するうえで、最初に行うのはドメインの隔離です。 足し算アプリの中心となる部分は、もちろん足し算です。前述のコードの中では以下の部分が該当します。この部分をドメインとして扱うことになります。 int result = param1 + param2; レイヤ化アーキテクチャ 前述のコードの処理内容を、以下の3層+ドメイン層のレイヤ化アーキテクチャで分割します。 UI層 (プレゼンテーション層と呼ばれることもあります) ユースケース層 (アプリケーション層と呼ばれることもあります) インフラストラクチャ層 ドメイン層 DDDとセットでよく使われるアーキテクチャには、オニオンアーキテクチャ、ヘキサゴナルアーキテクチャ、クリーンアーキテクチャなど複数ありますが、上記のような各層のネーミングや定義がそれぞれ微妙に異なります。ただ、ドメイン層を隔離する、即ち、ドメイン層は他のどの層にも依存しないようにする、というのが基本的な考え方だと思います。 UI層 この層では、UIに依存する処理を行ったうえで、実際の足し算については、後述のユースケース層のメソッドを呼び出します。 この例では、コマンドラインのUIとしているため、UIに依存する以下の処理を行います。 コマンドライン引数からの入力値の取得とパース 結果を標準出力 コマンドライン引数の代わりに、ファイルやDBを入力としたり、WebAPIでリクエストを受け取ったり、画面から入力したり、のようにUIを変えたければ、この層を変更することになります。 public class DDDAddition01 { public static void main(String[] args) { // 引数チェック if (args.length != 2) { System.err.println("引数を2つ指定してください"); System.exit(1); } int param1 = 0; int param2 = 0; try { param1 = Integer.parseInt(args[0]); param2 = Integer.parseInt(args[1]); } catch (NumberFormatException e) { System.err.println("引数には整数を指定してください"); System.exit(1); } // アプリケーションサービスを生成し実行 AdditionService service = new AdditionService(); int result = service.execute(param1, param2); // 結果を表示 System.out.printf("%s + %s = %s%n", param1, param2, result); } } ユースケース層 この層では、ユースケースごとにUI層から呼び出されるメソッドを定義します。 ここでは「足し算を行う」というユースケースに対応するexecuteメソッドを用意しました。 ユースケースが増えた場合(「足し算の履歴を表示する」等)は、それに対応するメソッドまたはサービスを追加していくことになります。 この層の役割は、ユースケースに応じてドメイン層をオブジェクトを操作することであり、具体的な処理内容はドメイン層のオブジェクトに委譲し、ユースケース層自身の処理は薄く保つように努めます。 public class AdditionService { public AdditionService(){} public int execute(int int1, int int2){ // ドメインオブジェクトを生成 final AdditionElement e1 = new AdditionElement(int1); // 値オブジェクト final AdditionElement e2 = new AdditionElement(int2); // 値オブジェクト // ドメインオブジェクトから結果を取得 final AdditionElement result = e1.plus(e2); // 値オブジェクトのメソッド戻り値も値オブジェクト return result.getValue(); } } ドメイン層 この層ではじめて、足し算という本アプリの中心となる部分を、コードによって表現します。 ここでは、「足し算の要素」というドメインモデルを AdditionElement というオブジェクトで表し、 AdditionElement の plus メソッドで、「足し算の要素どうしを足すことができる」という振る舞いを表現しています。 なお、足し算の結果もまた「足し算の要素」として他の要素と足すことができるため、 plus メソッドの戻り値もまた AdditionElement 型としています。 public class AdditionElement { private final int value; public AdditionElement(int value) { this.value = value; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AdditionElement)) return false; AdditionElement that = (AdditionElement) o; return value == that.value; } @Override public int hashCode() { return Objects.hash(value); } public int getValue() { return this.value; } public AdditionElement plus(AdditionElement element) { return new AdditionElement(this.value + element.getValue()); } } この AdditionElement は、DDDにおいて「値オブジェクト」に分類されるモデルとなります。 値オブジェクトの特徴・条件はいくつかありますが、特に重要な特徴は、値オブジェクトは不変(immutable)であるということです。 不変であることが保証されることによってプログラムの保守性・安定性が高まります。例えば、「1」という値がどこかのタイミングで中身が「2」に変わってしまう可能性があったら(意味がわからないですね)、その値がどこでどう変わったか/変わっていないかを常に気にかけないといけなくなってしまいます。不変であれば、そのような余計な心配・確認が不要となります。 ここでは、 value プロパティにfinal修飾子をつけ、setterメソッドのような value を変更可能なメソッドも公開しないことによって、不変であることを保証しています。 また、本オブジェクトの等価性を定義するために、 equals と hashCode メソッドをオーバーライドし、2つの AdditionElement インスタンスについて、 value が同じであれば同じ値とみなせることとしています。 この部分の実装有無は今回の例ではアプリの実動作には影響がありませんが、少なくともテストコード作成時には値の比較がしやすくなります。 インフラストラクチャ層 この時点では、インフラストラクチャ層とするような内容がないため割愛します。後述する仕様追加の対応の際に登場します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

単純すぎるドメイン駆動設計Javaサンプルコード (2) 仕様追加その1

単純すぎるドメイン駆動設計Javaサンプルコード 単純すぎるドメイン駆動設計Javaサンプルコード (1) DDDらしいコード 単純すぎるドメイン駆動設計Javaサンプルコード (2) 仕様追加その1 <- ここ 単純すぎるドメイン駆動設計Javaサンプルコード (3) 仕様追加その2 単純すぎるドメイン駆動設計Javaサンプルコード (4) テスト 前述のDDDらしいコードの作成では、手続き型設計のコードをレイヤ化アーキテクチャで分割し、足し算アプリの中心部分である足し算の処理を、ドメイン層を隔離しました。 見方によっては、むしろ記述量やクラス数が増えて、わかりにくくなった印象を持たれるかもしれません。それは今回の題材がDDDを取り入れるには単純すぎるのが原因です。 ドメインがより複雑になったり、仕様の追加・変更が何度も発生すると、DDDの効果が出てくるでしょう。 例として、実はこの足し算アプリには以下の要件があることが新たに判明したとします。 このアプリは小学校の算数の教材として使用するものであり、正の整数のみを扱うものとする このとき、手続き型設計の場合とDDDの場合で、どのような違いが生まれるかを見ていきます。 手続き型設計の場合 例えば、以下のように修正することになりそうです。安易にチェック処理を追加しました。 public class TransactionAddition02 { public static void main(String[] args) { // 引数チェック if (args.length != 2) { System.err.println("引数を2つ指定してください"); System.exit(1); } int param1 = 0; int param2 = 0; try { param1 = Integer.parseInt(args[0]); param2 = Integer.parseInt(args[1]); } catch (NumberFormatException e) { System.err.println("整数を指定してください"); System.exit(1); } // チェック処理を追加 if (param1 < 0 || param2 < 0) { System.err.println("負の数は扱いません"); System.exit(1); } // 処理実行 int result = param1 + param2; // 結果を表示 System.out.printf("%s + %s = %s%n", param1, param2, result); } } DDDの場合 ドメイン層 ドメインモデルの AdditionElement に事前条件を追加します。 public class AdditionElement { private final int value; public AdditionElement(int value) { // 事前条件を追加 if (value < 0) { throw new IllegalArgumentException("負の数は扱いません:" + value); } this.value = value; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AdditionElement)) return false; AdditionElement that = (AdditionElement) o; return value == that.value; } @Override public int hashCode() { return Objects.hash(value); } public int getValue() { return this.value; } public AdditionElement plus(AdditionElement element) { return new AdditionElement(this.value + element.getValue()); } } 「足し算の要素は負の数を扱わない(0または正の整数である)」というドメイン知識を表現しています。 「足し算の要素」というドメインモデルに、ドメイン知識が凝集していることがポイントです。これにより「足し算の要素」の振る舞いや条件が、このクラスさえ読めばわかるようになります。これはコードの規模が大きくなるほど、可読性として重要になってきます。 なお、今回の仕様変更では、ドメイン層以外の層は何も変更する必要がありません、と言いたかったところですが、 AdditionElement が例外を投げるようになったので、そのハンドリングのみ以下のように追加しました。 ユースケース層 public class AdditionService { public AdditionService(){} public int execute(int int1, int int2) throws AdditionServiceException { try { // ドメインオブジェクトを生成 final AdditionElement e1 = new AdditionElement(int1); // 値オブジェクト final AdditionElement e2 = new AdditionElement(int2); // 値オブジェクト // ドメインオブジェクトから結果を取得 final AdditionElement result = e1.plus(e2); // 値オブジェクトのメソッド戻り値も値オブジェクト return result.getValue(); // 例外ハンドリングを追加 } catch (IllegalArgumentException e) { throw new AdditionServiceException(e); } } } UI層 public class DDDAddition02 { public static void main(String[] args) { // 引数チェック if (args.length != 2) { System.err.println("引数を2つ設定してください"); System.exit(1); } int param1 = 0; int param2 = 0; try { param1 = Integer.parseInt(args[0]); param2 = Integer.parseInt(args[1]); } catch (NumberFormatException e) { System.err.println("整数を指定してください"); System.exit(1); } // アプリケーションサービスを生成・実行 int result = 0; try { AdditionService service = new AdditionService(); result = service.execute(param1, param2); // 例外ハンドリングを追加 } catch (AdditionServiceException e) { System.err.println("足し算サービスでエラーが発生しました"); e.printStackTrace(); System.exit(1); } // 結果を表示 System.out.printf("%s + %s = %s%n", param1, param2, result); } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

単純すぎるドメイン駆動設計Javaサンプルコード (3) 仕様追加その2

単純すぎるドメイン駆動設計Javaサンプルコード 単純すぎるドメイン駆動設計Javaサンプルコード (1) DDDらしいコード 単純すぎるドメイン駆動設計Javaサンプルコード (2) 仕様追加その1 単純すぎるドメイン駆動設計Javaサンプルコード (3) 仕様追加その2 <- ここ 単純すぎるドメイン駆動設計Javaサンプルコード (4) テスト さらに、以下の要件が追加されたとします。 ユーザごとに足し算の履歴を保存できる 今回の例ではコマンドラインインターフェースとしているので、標準入力で複数回足し算を実行できるように変更し、かつ、ユーザごとに <ユーザ名>.csv のかたちで履歴をファイル出力するようにします。 ここでも、手続き型設計の場合とDDDの場合で、どのような違いが生まれるかを見ていきます。 手続き型設計の場合 以下の変更を行いました。 標準入力を1行ずつ読み取って足し算を実行するように変更 ユーザ名を指定するための引数を追加 足し算の履歴をファイル出力する処理を追加 さすがにちょっと長くなって見通しが悪くなってきました。 public class TransactionAddition03 { public static void main(String[] args) { System.out.println("'username 1 2'のように入力すると、username.csvに'1,2,3'(1つ目の値,2つ目の値,足し算結果)と出力します"); System.out.println("'q'を入力すると終了します"); // 複数回実行できるように変更 Scanner sc = new Scanner(System.in); while (sc.hasNextLine()) { String[] lineArgs = sc.nextLine().split(" "); execute(lineArgs); } } private static void execute(String[] args) { // 終了コマンド if (args[0].equals("q")) { System.out.println("終了します"); System.exit(0); } // 引数チェック if (args.length != 3) { System.err.println("引数を3つ指定してください"); System.exit(1); } // 引数にユーザ名を追加 String username = args[0]; int param1 = 0; int param2 = 0; try { param1 = Integer.parseInt(args[1]); param2 = Integer.parseInt(args[2]); } catch (NumberFormatException e) { System.err.println("整数を指定してください"); System.exit(1); } if (param1 < 0 || param2 < 0) { System.err.println("負の数は扱いません"); System.exit(1); } // 足し算を実行 int result = param1 + param2; // 履歴をファイル出力 String filename = username + ".csv"; try(FileWriter fw = new FileWriter(filename, true)) { fw.write(String.format("%s,%s,%s%n", param1, param2, result)); } catch (IOException ex) { ex.printStackTrace(); System.exit(1); } // 結果を表示 System.out.printf("%s + %s = %s%n", param1, param2, result); } } DDDの場合 ユーザごとに足し算の履歴を保存できる 追加の要件から、新たなドメインモデルを抽出し、ドメイン層に以下のクラスを追加します。 ドメイン層 User ユーザは名前を持ち、名前によって識別される 普通はユーザID的なもので識別すると思いますが、単純にするため名前にしてます。 ユーザは足し算の履歴を保持する 履歴は実行した足し算の式の集合である ユーザの足し算の履歴として、足し算の式を1つずつ追加することができる public class User { private final String name; // 不変で一意な識別子 private final List<AdditionFormula> history = new ArrayList<>(); // 可変の属性(List参照は不変だが中身は可変) public User(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User)) return false; User user = (User) o; return Objects.equals(this.name, user.name); // 名前で識別する } @Override public int hashCode() { return Objects.hash(name); } public String getName() { return this.name; } public List<AdditionFormula> getHistory() { return new ArrayList<>(this.history); } public void addHistory(AdditionElement e1, AdditionElement e2, AdditionElement result) { this.history.add(new AdditionFormula(e1, e2, result)); } } このUserは、DDDにおいて「エンティティ」に分類されるモデルとなります。 エンティティは一意に識別して変更を管理できるものであり、その特徴ゆえに永続化の対象となることが多いです。 ここでは、ユーザは名前によって一意に識別できるものとしています。足し算の履歴が増えていっても、そのユーザが同じユーザであることに変わりはありません。名前が異なれば別ユーザとして扱われ、履歴も別管理されます。 Userのプロパティのうち、識別子である名前(name)が不変であるのに対し、履歴(history)は可変となっています。(履歴のリスト自体はfinalで不変となっていますが、リストの中身は追加可能なので可変とみなしています。) また、 equals と hashCode メソッドをオーバーライドし、 name が同じであることのみを、同一性の条件としています。 値オブジェクトでも同様に equals と hashCode メソッドをオーバーライドしていましたが、エンティティとは目的が異なるので注意が必要です。値オブジェクトでは各属性が持つすべての値が同じかで等価性を判定しますが、エンティティでは一意な識別子の値が同じかで同一性を判定します。 AdditionFormula 足し算の式は、足し算の要素2つと、足し算の結果を含む public class AdditionFormula { private final AdditionElement element1; private final AdditionElement element2; private final AdditionElement result; public AdditionFormula(AdditionElement e1, AdditionElement e2, AdditionElement result) { this.element1 = e1; this.element2 = e2; this.result = result; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AdditionFormula)) return false; AdditionFormula that = (AdditionFormula) o; return Objects.equals(element1, that.element1) && Objects.equals(element2, that.element2) && Objects.equals(result, that.result); } @Override public int hashCode() { return Objects.hash(element1, element2, result); } public int getElement1Value() { return this.element1.getValue(); } public int getElement2Value() { return this.element2.getValue(); } public int getResultValue() { return this.result.getValue(); } } こちらは値オブジェクトです。 振る舞いらしいメソッドを持ってないのでドメインモデルとしては微妙ですが、足し算における要素2つと結果のひとまとまりを表す概念としてモデル化しています。 UserRepository ユーザを保存することができる 名前を指定してユーザを取得することができる public interface UserRepository { void save(User user) throws UserRepositoryException; User find(String username) throws UserRepositoryException; } この UserRepositoryは、DDDにおいて「リポジトリ」に分類されるモデルの抽象インタフェースです。 リポジトリは簡単に言ってしまえばエンティティを永続化する場所です。(トランザクションの単位である「集約」を永続化すると言った方がよいかもしれませんが、今回の単純な例では単一の User エンティティ=集約ルートエンティティとなるので、いったん詳細は割愛します。) この例ではJavaのinterfaceを使用して抽象として定義されており、ファイル出力することによって保存する、という具体的な処理はインフラストラクチャ層の以下のクラスで実装しています。 インフラストラクチャ層 public class UserRepositoryOnFile implements UserRepository { public void save(User user) throws UserRepositoryException { String filename = user.getName() + ".csv"; try(FileWriter fw = new FileWriter(filename)) { for (AdditionFormula formula : user.getHistory()) { String[] arr = { String.valueOf(formula.getElement1Value()), String.valueOf(formula.getElement2Value()), String.valueOf(formula.getResultValue()) }; String csv = String.join(",", arr); fw.write(csv + "\n"); } } catch (IOException e) { throw new UserRepositoryException(e); } } public User find(String name) throws UserRepositoryException { User user = new User(name); File file = new File("./" + name + ".csv"); if (file.exists()) { try { List<List<String>> records = readCsvFile(file); addHistoryTo(user, records); } catch (IOException e) { throw new UserRepositoryException(e); } } return user; } private List<List<String>> readCsvFile(File file) throws IOException { BufferedReader br = new BufferedReader(new FileReader(file)); List<List<String>> records = new ArrayList<>(); String line; StringTokenizer token; while ((line = br.readLine()) != null) { List<String> record = new ArrayList<>(); token = new StringTokenizer(line, ","); while (token.hasMoreTokens()) { record.add(token.nextToken()); } records.add(record); } br.close(); return records; } private void addHistoryTo(User user, List<List<String>> records) { records.forEach(record -> { user.addHistory( new AdditionElement(Integer.parseInt(record.get(0))), new AdditionElement(Integer.parseInt(record.get(1))), new AdditionElement(Integer.parseInt(record.get(2))) ); }); } } ファイル入出力まわりは少し記述が多くてごちゃごちゃしてしまいましたが、その部分はここでは重要ではありません。 ここでは抽象と実装を分離していることが、ドメインの隔離において重要となります。 SOLID原則のD(Dependency Inversion Principle 依存関係逆転の原則)により、利用の方向(ドメイン→インフラストラクチャ)と依存の方向(インフラストラクチャ→ドメイン)を逆にしていることで、ドメイン層の独立性(他のどの層にも依存しない状態)を維持しています。 出力先がファイルだろうがDBだろうがメモリだろうが、ドメイン層は気にすることなく同じ save メソッドによって保存することができます。出力方法を変えたければ、 UserRepository を実装するインフラストラクチャ層のクラスを差し替えればよいのです。 ユースケース層 ユースケース層のアプリケーションサービスの変更は以下となります。 ユーザリポジトリによりユーザを取得・保存する処理を追加 ユーザリポジトリをコンストラクタ引数で受け取り、DIできるよう修正 public class AdditionService { private final UserRepository userRepository; public AdditionService(UserRepository userRepository){ this.userRepository = userRepository; // リポジトリ実装をDI } public int execute(String username, int int1, int int2) throws AdditionServiceException { try { // ドメインオブジェクトを生成 final AdditionElement e1 = new AdditionElement(int1); final AdditionElement e2 = new AdditionElement(int2); // ドメインオブジェクトに処理を委譲 final AdditionElement result = e1.plus(e2); final User user = this.userRepository.find(username); user.addHistory(e1, e2, result); // エンティティを更新 this.userRepository.save(user); // エンティティをリポジトリで永続化 return result.getValue(); } catch (IllegalArgumentException | UserRepositoryException e) { throw new AdditionServiceException(e); } } } UI層 UI層は、以下の修正を行います。 足し算を複数回実行できるように変更(手続き型設計と同じ) ファイル出力を行うユーザリポジトリ実装をアプリケーションサービスにDI public class DDDAddition03 { public static void main(String[] args) { System.out.println("'username 1 2'のように入力すると、username.csvに'1,2,3'(1つ目の値,2つ目の値,足し算結果)と出力します"); System.out.println("'q'を入力すると終了します"); // 複数回実行できるように変更 Scanner sc = new Scanner(System.in); while (sc.hasNextLine()) { String[] lineArgs = sc.nextLine().split(" "); execute(lineArgs); } } private static void execute(String[] args) { // 終了コマンド if (args[0].equals("q")) { System.out.println("終了します"); System.exit(0); } // 引数チェック if (args.length != 3) { System.err.println("引数を3つ指定してください"); System.exit(1); } // 引数にユーザ名を追加 String username = args[0]; int param1 = 0; int param2 = 0; try { param1 = Integer.parseInt(args[1]); param2 = Integer.parseInt(args[2]); } catch (NumberFormatException e) { System.err.println("整数を指定してください"); System.exit(1); } // アプリケーションサービスを生成・実行 UserRepository userRepository = new UserRepositoryOnFile(); // 履歴をファイル出力するリポジトリ実装を使用 AdditionService service = new AdditionService(userRepository); // リポジトリ実装をDI int result = 0; try { result = service.execute(username, param1, param2); } catch (AdditionServiceException e) { System.err.println("足し算サービスでエラーが発生しました"); e.printStackTrace(); System.exit(1); } // 結果を表示 System.out.printf("%s + %s = %s%n", param1, param2, result); } } ここでポイントとなるのはリポジトリ実装のDI(Dependency Injection)です。 今回は UserRepository インターフェースの実装クラスとして、ファイル出力を行う UserRepositoryOnFile というクラスを実装し、アプリケーションサービスに渡していますが、例えばファイルの代わりにDBを使いたくなった場合は、DBアクセスする実装クラス UserRepositoryOnDB を別途用意して、 UserRepositoryOnFileの代わりに渡せばいいだけです。その際、アプリケーション層やドメイン層を変更する必要はありません。アプリケーション層やドメイン層はインフラストラクチャ層のクラスに依存していないからです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

単純すぎるドメイン駆動設計Javaサンプルコード (4) テスト

単純すぎるドメイン駆動設計Javaサンプルコード 単純すぎるドメイン駆動設計Javaサンプルコード (1) DDDらしいコード 単純すぎるドメイン駆動設計Javaサンプルコード (2) 仕様追加その1 単純すぎるドメイン駆動設計Javaサンプルコード (3) 仕様追加その2 単純すぎるドメイン駆動設計Javaサンプルコード (4) テスト <- ここ いまさらですが、ここでテストコードも見てみましょう。 DDDの場合 ドメイン層 ドメイン層のテストは比較的単純に書くことができます。 AdditionElement class AdditionElementTest { @Test @DisplayName("要素から値を取得できる") void testPositiveValue() { AdditionElement target = new AdditionElement(1); assertEquals(1, target.getValue()); } @Test @DisplayName("要素として0を扱える") void testZero() { AdditionElement target = new AdditionElement(0); assertEquals(0, target.getValue()); } @Test @DisplayName("要素として負の数は扱えない") void testNegativeValue() { assertThrows(IllegalArgumentException.class, () -> new AdditionElement(-1)); } @Test @DisplayName("要素どうしを足すと、足し算結果の値を持つ要素を返す") void plus() { AdditionElement element1 = new AdditionElement(1); AdditionElement element2 = new AdditionElement(2); AdditionElement expected = new AdditionElement(1 + 2); assertEquals(expected, element1.plus(element2)); } } AdditionFormula class AdditionFormulaTest { AdditionFormula target; @BeforeEach void setUp() { target = new AdditionFormula( new AdditionElement(1), new AdditionElement(2), new AdditionElement(3) ); } @AfterEach void tearDown() { target = null; } @Test @DisplayName("1つ目の要素の値を取得できる") void getElement1Value() { assertEquals(1, target.getElement1Value()); } @Test @DisplayName("2つ目の要素の値を取得できる") void getElement2Value() { assertEquals(2, target.getElement2Value()); } @Test @DisplayName("足し算結果の値を取得できる") void getResultValue() { assertEquals(3, target.getResultValue()); } } User class UserTest { User user; @BeforeEach void setUp() { user = new User("hoge"); } @AfterEach void tearDown() { user = null; } @Test @DisplayName("ユーザは名前で識別できる。名前が同じなら同じユーザとみなす") void testEquals1() { User sameUser = new User("hoge"); assertEquals(sameUser, user); } @Test @DisplayName("ユーザは名前で識別できる。名前が異なるなら別のユーザとみなす") void testEquals2() { User anotherUser = new User("fuga"); assertNotEquals(anotherUser, user); } @Test @DisplayName("ユーザから名前を取得できる") void getName() { assertEquals("hoge", user.getName()); } @Test @DisplayName("ユーザは複数の足し算の履歴を保持する") void testHistory() { AdditionElement e11 = new AdditionElement(1); AdditionElement e21 = new AdditionElement(2); AdditionElement result1 = new AdditionElement(3); user.addHistory(e11, e21, result1); AdditionElement e12 = new AdditionElement(3); AdditionElement e22 = new AdditionElement(4); AdditionElement result2 = new AdditionElement(7); user.addHistory(e12, e22, result2); List<AdditionFormula> expected = new ArrayList<>(Arrays.asList( new AdditionFormula(e11, e21, result1), new AdditionFormula(e12, e22, result2) )); assertEquals(expected, user.getHistory()); } } ユースケース層 ユースケース層では、UserRepositoryにモックを使用することにより、実際のファイル入出力を伴わずにテスト可能としています。 class AdditionServiceTest { UserRepository mockUserRepository; AdditionService service; @BeforeEach void setUp() { mockUserRepository = mock(UserRepository.class); service = new AdditionService(mockUserRepository); } @AfterEach void tearDown() { service = null; mockUserRepository = null; } @Test @DisplayName("足し算サービスを実行すると、足し算の結果を返却する") void execute() { try { when(mockUserRepository.find(anyString())).thenReturn(new User("hoge")); assertEquals(3, service.execute("hoge", 1, 2)); } catch (AdditionServiceException | UserRepositoryException e) { fail(); } } @Test @DisplayName("足し算サービスを実行すると、足し算の履歴を持つユーザを保存する") void executeSuccess() { try { when(mockUserRepository.find(anyString())).thenReturn(new User("hoge")); service.execute("hoge",1,2); User expectedUser = new User("hoge"); expectedUser.addHistory( new AdditionElement(1), new AdditionElement(2), new AdditionElement(3) ); verify(mockUserRepository).find("hoge"); verify(mockUserRepository).save(expectedUser); } catch (AdditionServiceException | UserRepositoryException e) { fail(); } } @Test @DisplayName("パラメータが不正だと、足し算サービスが失敗する") void executeFailure1() throws UserRepositoryException { when(mockUserRepository.find(anyString())).thenReturn(new User("hoge")); assertThrows(AdditionServiceException.class, () -> { service.execute("hoge",1, -2); }); } @Test @DisplayName("ユーザの保存が失敗すると、足し算サービスが失敗する") void executeFailure2() throws UserRepositoryException { when(mockUserRepository.find(anyString())).thenReturn(new User("hoge")); doThrow(new UserRepositoryException(new IOException())) .when(mockUserRepository).save(any()); assertThrows(AdditionServiceException.class, () -> { service.execute("hoge",1,2); }); } } UI層・インフラストラクチャ層 UI層とインフラストラクチャ層についてはユニットテストで対応するよりも実際にアプリを動作させてテストした方が効果的かつ効率的だと思います。 詳細なロジックはドメイン層とユースケース層のユニットテストでほぼ網羅できると思われるため、テストケースは下記観点でそれぞれ1ケースずつぐらいでよいかと思います。 正常系 アプリを起動して足し算を複数回実行し、標準出力された足し算の結果と、ファイル出力された内容が期待どおりであることを確認する。 終了コマンドを入力し、アプリが正常終了することを確認する。 アプリを再度起動し、同じユーザ名で足し算実行すると、同じファイルに履歴が追記されることを確認する。 異常系 引数の数が過不足の場合に、アプリが異常終了することを確認する。 2つ目および3つ目の引数に数値以外の値を入力し、アプリが異常終了することを確認する。 ※ファイル出力まわりの異常系は実際にIOExceptionを発生させるのは難しいと思われるので、FileWriter/FileReaderをモックに差し替えて動かすようなユニットテストを用意した方がよいかもしれませんが、ここでは割愛します。 手続き型設計の場合 正直どのようにテストするのがよいか困っています。DDDの場合よりも後回しにしたのはそのせいです。 1つのクラス、1つのメソッドにまとまってしまっているので、部分ごとにユニットテストをすることができません。 一部はprivateメソッドに分かれていますが、privateなのでクラスの外から見たら分かれていないのと同じです。部分ごとにテストできるように処理ごとに細かくprivateメソッドに切り出したとしても同様です。 ※privateメソッドのテストについては議論があるようですが、ググったらすぐに以下の記事が見つかったので、参考までに貼っておきます。 となると、テストコードによるユニットテスト自動化は諦めて、実際にアプリを動作させてテストすることになりそうですが、その場合、前述のDDDの場合のUI層、インフラストラクチャ層のテスト観点に加えて、足し算のロジック部分まで網羅的にテストする必要があります。自動化も不可ではないと思われますが、ツールを用意するのに一手間かかりそうです。申し訳ありませんが、ちょっと考えただけで心が折れてしまったので、ここまでとさせてください。 実際の開発では、だからと言ってテストしないというのはありえないと思いますが、excelテストケース表を用意して手動テストすることになるのもありがちかと思います。手動テストが面倒なので改修もなるべくしたくなくなりますね。 このあたりは、DDD以前に、オブジェクト指向設計を行うことによってテストを容易にできるようにしましょうという話かと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【実践!AWS Lambda】Javaで書いたLambdaの初回起動が遅い問題を解決する

■ 概要 AWS Lambdaの開発において、必ず出てくる初回起動が遅い問題、、、 NodejsやPythonなどのスクリプト言語のLambdaであれば対策は比較的簡単なのですが、Javaなどのコンパイル型の言語の場合は難易度が上がります JavaのLambdaの初回起動にかかる時間を8秒→1秒に短縮することに成功したので、ここに備忘録を残します ■ 目次 前提条件 設計要素の洗い出し コールドスタート対策 DBコネクション対策 クラスローディング対策 ■ 前提条件 ・ランタイムはJava 11を使用 ・以下のような構成とする ①API GatewayをトリガーにLambdaが起動 ②RDS Proxyを介してAuroraに接続 ③ClientにResponceを返す ■ 設計要素の洗い出し 時間がかかっている要因を洗い出していく 実行時間を計測 まずはPostmanからAPIを叩いて初回起動に要する時間を計測 実行時間 1回目 7245ミリ秒 2回目 683ミリ秒 3回目 364ミリ秒 4回目 537ミリ秒 5回目 366ミリ秒 明らかに初回が遅いことが分かる コールドスタート問題 Lambdaの実行時間を短縮したい場合にはまず、コールドスタートの回避を図る必要がある コールドスタートを理解するために、どのような仕組みでLambdaが実行されているかを整理してみる Lambda実行時、裏側では以下のような処理が走っている ➀ 実行環境の作成(コンテナ立ち上げ) ↓ ➁ デプロイパッケージのロード ↓ ➂ デプロイパッケージの展開 ↓ ➃ ランタイム起動・初期化 ↓ ➄ 関数/メソッドの実行 ↓ 10分~30分くらい(どのタイミングでコンテナが破棄されるかは決まっていない) ↓  ➅ コンテナの破棄 この➀~➄の全てを実行するのが、いわゆるコールドスタート コンテナが破棄される前に再度実行すると、➀~➃を省けるウォームスタートとなる 初回起動がおそいのは、コールドスタートになっている為である DBのConnection問題 今回の設計上、コールドスタートを回避できたとしても、DBとのConnectionが切れていればConnection作成に時間がかかってしまう DB処理をさせるLambdaの場合は、この罠にも引っかからないように設計する必要がある クラスローディング問題 ここはかなりはまりがち、、、Javaのようなコンパイル型言語ならではの罠 JavaのLambdaの場合コンテナ起動後に、 JVM(Javaを実行する仮想マシン)がクラスローディングを行いながら処理が走る仕様となっている 起動と実行の2段構えで時間がかかることになる(JavaのLambdaが遅いといわれる原因) ちなみにJVMも同じコンテナで使いまわされるので、2回目以降はクラスローディング済みの為早くなる 回避するには、コンテナ起動と同時にクラスローディングを済ませておく必要がある 設計要素まとめ 設計要素は以下のようになる ①コールドスタートの回避 ②DBのConnection維持 ③クラスローディング対策 ここから一つずつ解決していく ■ コールドスタート対策 コールドスタートを回避するには、主に以下の2つの手段がある 1. Amazon EventBridgeでスケジュール実行させる 2. Provisioned Concurrencyで事前プロビジョニングしておく それぞれ一長一短あるので、簡単に解説 「Amazon EventBridge」 メリット  ・コンテナを起動→実行まで定期実行できる  ・Lambdaの実行の料金しかかからない デメリット  ・スケジュール実行の合間にコンテナが破棄される可能性が否定できない(五分間隔でも稀に破棄される) 「Provisioned Concurrency」 メリット  ・設定が簡単  ・コンテナ数をプロビジョニングできるため、大量のリクエストに対応できる デメリット  ・Lambdaの実行とは別に料金がかかる  ・コンテナを事前に立てておくだけの機能の為、DBのCnnectionが維持できない → DBのConnectionを維持する必要があるので、今回はAmazon EventBridgeでスケジュール実行させる方法を採用 構築のPoint ・今回は複数のAPIを作成するため、一つずつEventBridgeの設定をしていくのは大変  → 全APIをたたくLambdaを新規作成し、そのLambdaのみスケジュール実行させる ・毎回ソース内の全ての処理が実行されると困る(毎回DBのデータが書き換えられたりする)  → スケジュール実行のLambdaによって呼び出された場合は、通常実行時の処理を行う前にレスポンスを返すようにする 構成図は以下のようになる 実行時間を計測 「schedued-execution-lambda」をスケジュール実行させた状態で動作確認 実行時間 1回目 3625ミリ秒 2回目 473ミリ秒 3回目 633ミリ秒 4回目 376ミリ秒 5回目 574ミリ秒 3~5秒ほど短縮されていて、コールドスタートは回避できていそう それでもまだ目に見えて初回起動に時間がかかっているので、DB接続とクラスローディング対策を進めていく ■ DBコネクション対策 スケジュール実行時にはDB処理を行っていない為、DBのConnectionが維持できていない 単純だが、スケジュール実行された場合の処理に、DB接続して閉じるだけの処理を追加する(現在はスケジュール実行によるHTTPリクエストの場合、通常の処理を行わずにすぐReturnさせている) 実行時間を計測 「schedued-execution-lambda」をスケジュール実行させた状態で動作確認 実行時間 1回目 1846ミリ秒 2回目 634ミリ秒 3回目 356ミリ秒 4回目 472ミリ秒 5回目 533ミリ秒 さらに2秒程度短縮されていることが確認できた ただ、これでもまだ実用できるほどの短縮にはなっていない為、クラスローディング対策も行う ■ クラスローディング対策 時間がかかっている処理を特定 ミリ秒単位で計測したいので、java.lang.SystemクラスのcurrentTimeMillis()メソッドを使用し、処理時間をログに出力させていく おそらく、ハンドラメソッドのあるクラス内で他のクラスを呼び出したりインスタンス化する処理に時間がかかっている Staticイニシャライザを使用 Staticイニシャライザとは、 クラスのロード時に一度だけ実行されるstaticで宣言されたコードブロックのこと つまりここで一度読み込んでクラスをロード済みにしてしまえば、実行時のクラスローディングにかかる時間を短縮できる 初回起動で時間がかかっている処理をここにガシガシ書いていく ここにDB接続処理も書いておけば、「Provisioned Concurrency」も使えそう 実行時間を計測 「schedued-execution-lambda」をスケジュール実行させた状態で動作確認 実行時間 1回目 749ミリ秒 2回目 637ミリ秒 3回目 745ミリ秒 4回目 465ミリ秒 5回目 585ミリ秒 ついに1秒を切った、、、! ■ まとめ JavaのLambdaの初回起動が遅い問題ですが、 ・コールドスタート対策 ・DBコネクションの維持 ・クラスローディング対策 を行うことで解消することができました 他にもできることがあれば教えていただきたいです。 こういったチューニングを行う際には裏側の理解が必要なので、歯ごたえがあって楽しいです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【22章】Javaを学ぼう

今回の記事では、「【21章】Javaを学ぼう」までのプログラムの内容に、「【18章】Javaを学ぼう」で完成させた自己紹介プログラムのPersonクラスと組み合わせて、乗り物をある人間が所有している状況を表現してみたいと思います。 私自身のアウトプットの場となりますので、よろしくお願いいたします! 前提条件(これまでの復習) これまで学習してきた内容を載せておきます。今回はJavaを学ぼうの【1章〜21章】の知識が必要になります。 Main.java import java.util.Scanner; class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); Car car = new Car("フェラーリ", "赤"); Bicycle bicycle = new Bicycle("ジオス", "青"); System.out.println("【車の情報】"); car.printData(); System.out.println("-----------------"); System.out.print("走る距離を入力してください:"); int carDistance = scanner.nextInt(); car.run(carDistance); System.out.println("-----------------"); System.out.print("給油する量を入力してください:"); int litre = scanner.nextInt(); car.charge(litre); System.out.println("================="); System.out.println("【自転車の情報】"); bicycle.printData(); System.out.println("-----------------"); System.out.print("走る距離を入力してください:"); int bicycleDistance = scanner.nextInt(); bicycle.run(bicycleDistance); } } Vehicle.java abstract class Vehicle { private String name; private String color; protected int distance = 0; Vehicle(String name, String color) { this.name = name; this.color = color; public String getName() { return this.name; } public String getColor() { return this.color; } public int getDistance() { return this.distance; } public void setName(String name) { this.name = name; } public void setColor(String color) { this.color = color; } public void printData() { System.out.println("名前:" + this.name); System.out.println("色:" + this.color); System.out.println("走行距離:" + this.distance + "km"); } abstract public void run(int distance); } Car.java class Car extends Vehicle { private int fuel = 50; Car(String name, String color) { super(name, color); } public int getFuel() { return this.fuel; } public void printData() { super.printData(); System.out.println("ガソリン量:" + this.fuel + "L"); } public void run(int distance) { System.out.println(distance + "km走ります"); if (distance <= this.fuel) { this.distance += distance; this.fuel -= distance; } else { System.out.println("ガソリンが足りません"); } System.out.println("走行距離:" + this.distance + "km"); System.out.println("ガソリン量:" + this.fuel + "L"); } public void charge(int litre) { System.out.println(litre + "L給油します"); if(litre <= 0) { System.out.println("給油できません"); } if else(litre + this.fuel >= 100) { System.out.println("満タンまで給油します"); this.fuel = 100; } else { this.fuel += litre; } System.out.println("ガソリン量:" + this.fuel + "L"); } } Bicycle.java class Bicycle extends Vehicle { Bicycle(String name, String color) { super(name, color); } public void run(int distance) { System.out.println(distance + "km走ります"); this.distance += distance; System.out.println("走行距離:" + this.distance + "km"); } } 今回組み合わせるPersonクラスは下図の通りです。 Person.java class Person { private String firstName; private String middleName; private String lastName; private int age; private double height; private double weight; Person(String firstName, String lastName, int age, double height, double weight) { this.firstName = firstName; this.lastName = lastName; this.age = age; this.height = height; this.weight = weight; } Person(String firstName, String middleName, String lastName, int age, double height, double weight) { this(firstName, lastName, age, height, weight); this.middleName = middleName; } public String fullName() { if (this.middleName == null) { return this.firstName + " " + this.lastName; } else { return this.firstName + " " + this.middleName + " " + this.lastName; } } public void printData() { System.out.println("名前は" + this.fullName() + "です"); System.out.println("年齢は" + this.age + "歳です"); System.out.println("身長は" + this.height + "mです"); System.out.println("体重は" + this.weight + "kgです"); System.out.println("BMIは" + Math.round(this.bmi()) + "です"); } public double bmi() { return this.weight / this.height / this.height; } } クラス型のフィールド Personクラスと組み合わせて、乗り物をある人間が所有している状況をプログラムで表現できるように定義していきます。 インスタンスフィールドにクラス型の変数を定義することで、フィールドにインスタンスを持つことが可能です。 Person.java class Person { //クラス名 public static int count = 0; public String firstName; public String middleName; public String lastName; . . . } Vehicle.java abstract class Vehicle { private String name; private String color; protected int distance = 0; private Person owner; //Person型のownerフィールドを追加 //クラス名 . . . } 上図のようにPerson型のownerというインスタンスフィールドを持つようしました。 さらにprivateで定義しているため、ゲッターとセッターを定義しておきます。ゲッターの戻り値の型と、セッターの仮引数の型がクラス型になることに注意してください。 Vehicle.java abstract class Vehicle { . . private Person owner; . . public Person getOwner() { return this.owner; } public void setOwner(Person person) { this.owner = person; } } インスタンスの戻り値 CarクラスのインスタンスやBicycleクラスのインスタンスに対してgetOwnerメソッドを呼び出すと、その戻り値はownerフィールドの値、すなわちPersonクラスのインスタンスとなっています。よって、getOwnerメソッドのあとにそのまま続けてPersonクラスのインスタンスメソッドprintDataを呼びだすことが可能です。 Main.java import java.util.Scanner; class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); Person person1 = new Person("Kate", "Jones", 27, 1.6, 50.0); Person person2 = new Person("John", "Christopher", "Smith", 65, 1.75, 80.0); Car car = new Car("フェラーリ", "赤"); //setOwnerを用いて、carの所有者をperson1に car.setOwner(person1); Bicycle bicycle = new Bicycle("ジオス", "青"); //setOwnerを用いて、bicycleの所有者をperson2に bicycle.setOwner(person2); System.out.println("【車の情報】"); car.printData(); System.out.println("-----------------"); System.out.println("【車の所有者の情報】"); // getOwnerメソッドを用いてcarのownerを取得し、 // さらにprintDataメソッドを用いてownerの情報を出力してください car.getOwner().printData(); System.out.println("-----------------"); System.out.print("走る距離を入力してください:"); int carDistance = scanner.nextInt(); car.run(carDistance); System.out.println("-----------------"); System.out.print("給油する量を入力してください:"); int litre = scanner.nextInt(); car.charge(litre); System.out.println("================="); System.out.println("【自転車の情報】"); bicycle.printData(); System.out.println("-----------------"); System.out.println("【自転車の所有者の情報】"); // getOwnerメソッドを用いてbicycleのownerを取得し、 // さらにprintDataメソッドを用いてownerの情報を出力してください bicycle.getOwner().printData(); System.out.println("-----------------"); System.out.print("走る距離を入力してください:"); int bicycleDistance = scanner.nextInt(); bicycle.run(bicycleDistance); } } Person.java class Person { private String firstName; private String middleName; private String lastName; . . public void printData() { System.out.println("名前は" + this.fullName() + "です"); System.out.println("年齢は" + this.age + "歳です"); System.out.println("身長は" + this.height + "mです"); System.out.println("体重は" + this.weight + "kgです"); System.out.println("BMIは" + Math.round(this.bmi()) + "です"); } . . } buyメソッドを定義 Personクラスのインスタンスが乗り物を購入できるように、buyメソッドを定義しましょう。 購入する乗り物はCarクラスのインスタンスかBicycleクラスのインスタンスです。buyメソッドの引数はどちらの型のインスタンスも受け取る可能性がありますので、下図のようにオーバーロードする必要があります。 Person.java class Person { . . public void buy(Car car) { car.setOwner(this); //thisはbuyメソッドを呼び出している、Personクラスのインスタンスを指す } public void buy(Bicycle bicycle) { bicycle.setOwner(this); //thisはbuyメソッドを呼び出している、Personクラスのインスタンスを指す . . } Vehicle.java abstract class Vehicle { . . private Person owner; . . public void setOwner(Person person) { this.owner = person; } } buyメソッドの中で、引数に受け取ったインスタンスのセッターを用いて所有者を変更してあげます。 Main.java import java.util.Scanner; class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); Person person1 = new Person("Kate", "Jones", 27, 1.6, 50.0); Person person2 = new Person("John", "Christopher", "Smith", 65, 1.75, 80.0); Car car = new Car("フェラーリ", "赤"); // buyメソッドを用いて、person1にcarを購入させる person1.buy(car); Bicycle bicycle = new Bicycle("ジオス", "青"); // buyメソッドを用いて、person2にbicycleを購入させる person2.buy(bicycle); System.out.println("【車の情報】"); car.printData(); System.out.println("-----------------"); System.out.println("【車の所有者の情報】"); car.getOwner().printData(); System.out.println("-----------------"); System.out.print("走る距離を入力してください:"); int carDistance = scanner.nextInt(); car.run(carDistance); System.out.println("-----------------"); System.out.print("給油する量を入力してください:"); int litre = scanner.nextInt(); car.charge(litre); System.out.println("================="); System.out.println("【自転車の情報】"); bicycle.printData(); System.out.println("-----------------"); System.out.println("【自転車の所有者の情報】"); bicycle.getOwner().printData(); System.out.println("-----------------"); System.out.print("走る距離を入力してください:"); int bicycleDistance = scanner.nextInt(); bicycle.run(bicycleDistance); } } buyメソッドの重複 もし、Vehicleクラスのサブクラスが今後増えた場合、毎回引数の型が違う同名のメソッドを定義していかなければなりません。この問題を解決するため、引数としてVehicle型のインスタンスを受け取るようにすることで、Carクラスのインスタンスも、Bicycleクラスのインスタンスも受け取ることができるようになります。 Person.java class Person { . . public void Owner(Vehicle vehicle) { vehicle.setOwner(this); //public void buy(Car car) { //car.setOwner(this); //} //public void buy(Bicycle bicycle) { //bicycle.setOwner(this); . . } これでプログラムが完成しました! 最後までご覧いただきまして、ありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gsonでパースするときに DateTimeParseException になったメモ

gson で 日付をパースするときに原因不明のエラーになってハマりました java.time.format.DateTimeParseException: Text '2021-01-11T09:59:01' could not be parsed at index 19 もともと swagger から自動生成したコードで、 org.threeten.bp.OffsetDateTime が使われていたので、 java.time.OffsetDateTime に変更してもエラーは変わらず。 java.time.LocalDateTime に変更してもエラーは変わらず。 散々ハマってしまったけど、解決方法は下記でした gson に設定する LocalDateTimeTypeAdapter の formatter の指定が下記になっていた DateTimeFormatter.ISO_OFFSET_DATE_TIME これを下記にしたら無事にパースできました DateTimeFormatter.ISO_LOCAL_DATE_TIME 上記は、java.time.OffsetDateTime に変更している前提です。 OffsetDateTime のままだとやはりパースできず下記エラーになります。(オフセット部分がないので) Unable to obtain ZoneOffset from TemporalAccessor: {},ISO resolved to 2021-08-27T16:10:23 of type java.time.format.Parsed threeten.bp パッケージ (ThreeTen Backport) は DateTime の機能をJava 6でも使えるように移植したものなので、今となっては使う必要性はないと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む