- 投稿日:2020-08-10T23:39:27+09:00
[AWS] CodeStarを使うと、Lambdaで動作するSpring(Java)のプロジェクトをたった3分で構築できる!!
CodeStar
AWS CodeStarは、アプリケーションを迅速に開発・デプロイすることができるための環境を構築することができます。
いくつかのテンプレートが用意されており、マネジメントコンソール上からポチポチと選択していくだけで、開発プロジェクトが構築できてしまいます。構築される環境
- コードリポジトリ(CodeCommitと、GitHubを選択可能)
- CodeBuilde環境
- CodePipeline環境
- アプリケーション
- IDE(Cloud9選択時)
料金
CodeStar自体は料金は発生しません。
CodeStarによって構築されるコードリポジトリや、パイプライン、デプロイされたアプリケーションに係るリソース使用料のみです。注意事項
リージョンに東京リージョンを選択すると、IDEにCloud9を選択することができません(2020年8月現在)。
今回はCloud9で環境を構築したいので、リージョンにバージニア北部(us-east-1)を選択することとします。環境構築手順
まずは、マネジメントコンソールで、サービスよりCodeStarを選択します。
今回はIDEに
Cloud9を使用したいので、リージョンをバージニア北部にします。アプリケーションの種類や、言語を選択できます。
今回は、Lambda + Java Springのテンプレートでプロジェクトを作成してみようと思います。続いてコードリポジトリを選択します。
今回は、CodeCommitを選択してみますね。内容を確認して、プロジェクトを作成します。
最後にIDEを選択します。
今回は、AWS Cloud9です。リージョンによっては、Cloud9を選択できないのでご注意ください。
Cloud9を選択すると、インスタンスタイプを聞かれますので、今回はデフォルトのt2.microのまま進めます。プロジェクトとIDEの作成が始まります(裏でCloudFormationが動作します)。
プロジェクトが大体3分で環境構築が完了します。
IDEはEC2インスタンスの作成があるので、こちらも大体3分くらいかかります。
両方とも準備が完了すると、下記のような画面になります。リポジトリの状態
左側にあるメニューより「コード」を選択すると、CodeCommitの画面が表示されます。
Springのテンプレート構造が作成されており、buildspec.yml
まで作成されています。なお、
buildspec.yml
の中身はこんな感じです。codespec.ymlversion: 0.2 phases: install: runtime-versions: java: openjdk8 commands: # Upgrade AWS CLI to the latest version - pip install --upgrade awscli pre_build: commands: - echo Test started on `date` - mvn clean compile test build: commands: - echo Build started on `date` - mvn package shade:shade - mv target/HelloWorld-1.0.jar . - unzip HelloWorld-1.0.jar - rm -rf target tst src buildspec.yml pom.xml HelloWorld-1.0.jar - aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template template-export.yml post_build: commands: # Do not remove this statement. This command is required for AWS CodeStar projects. # Update the AWS Partition, AWS Region, account ID and project ID in the project ARN on template-configuration.json file so AWS CloudFormation can tag project resources. - sed -i.bak 's/\$PARTITION\$/'${PARTITION}'/g;s/\$AWS_REGION\$/'${AWS_REGION}'/g;s/\$ACCOUNT_ID\$/'${ACCOUNT_ID}'/g;s/\$PROJECT_ID\$/'${PROJECT_ID}'/g' template-configuration.json artifacts: files: - template-export.yml - template-configuration.jsonOpenJDK8で、Mavenでコンパイルされていることがわかりますね。
IDE
では、メニューからIDEを選択してみます。
すると、CLoud9が開始され、CodeCommitのリポジトリから、コードをgit clone
で自動的にコードを取得してくれます。あお、チームの設定を行うと、クロスアカウントでのコードリポジトリの共有や、モブプログラミングなんかも行うことができます。
[AWS] Cloud9でモブプログラミングの環境を作ってみる
アプリ実行
今回は、LambdaによるAPIです。
ダッシュボードにある「アプリの表示」ボタンを押すと、プロジェクトのテンプレートで最初に入っているAPIが呼び出されます。{"Output":"Hello World!"}具体的なAPI GatewayやLambdaはどうなっているのでしょう?
API Gateway
REST APIが一つできていることがわかりますね。
これ自体は、コード上の
tenpkate.yml
に定義されています。template.ymlResources: GetHelloWorld: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'awscodestar-${ProjectId}-lambda-GetHelloWorld' Handler: com.aws.codestar.projecttemplates.handler.HelloWorldHandler Runtime: java8 Role: Fn::GetAtt: - LambdaExecutionRole - Arn Events: GetEvent: Type: Api Properties: Path: / Method: get PostHelloWorld: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'awscodestar-${ProjectId}-lambda-PostHelloWorld' Handler: com.aws.codestar.projecttemplates.handler.HelloWorldHandler Runtime: java8 Role: Fn::GetAtt: - LambdaExecutionRole - Arn Events: PostEvent: Type: Api Properties: Path: / Method: postLambda
こちらは、Get用とPost用の2つが存在します。これも、API Gatewayで記載した
template.yml
で定義さている内容ですね。Pipelineを動かしてみる
Cloud9上で、
springproject/src/main/java/com/aws/codestar/project/handler/HelloWorldHandler.java
を編集してみます。HelloWorldHandler.java変更前return new GatewayResponse(new JSONObject().put("Output", "Hello World!").toString(), headers, 200);HelloWorldHandler.java変更後return new GatewayResponse(new JSONObject().put("Output", "Hello World Update!").toString(), headers, 200);ファイルを保存したら、Cloud9のターミナルから、CodeCommitにPushしてみましょう。
$ git add . $ git commit -m "update response." [master f0ba0d6] update response. Committer: EC2 Default User <ec2-user@ip-172-31-2-200.ec2.internal> Your name and email address were configured automatically based on your username and hostname. Please check that they are accurate. You can suppress this message by setting them explicitly: git config --global user.name "Your Name" git config --global user.email you@example.com After doing this, you may fix the identity used for this commit with: git commit --amend --reset-author 1 file changed, 1 insertion(+), 1 deletion(-) $ git push Enumerating objects: 21, done. Counting objects: 100% (21/21), done. Compressing objects: 100% (6/6), done. Writing objects: 100% (11/11), 719 bytes | 239.00 KiB/s, done. Total 11 (delta 3), reused 0 (delta 0) To https://git-codecommit.us-east-1.amazonaws.com/v1/repos/SpringProject 182e230..f0ba0d6 master -> masterパイプラインを確認すると、ビルドが開始されたことがわかります。
実は、ここでエラーになります。
確認してみると、どうやらテストで失敗しているようですね。[ERROR] Failures: [ERROR] HelloWorldHandlerTest.testHandleRequest:59 expected: <Hello World!> but was: <Hello World Update!> [INFO] [ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 10.048 s [INFO] Finished at: 2020-08-10T14:16:40Z [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.22.0:test (default-test) on project HelloWorld: There are test failures. [ERROR] [ERROR] Please refer to /codebuild/output/src139859990/src/target/surefire-reports for the individual test results. [ERROR] Please refer to dump files (if any exist) [date]-jvmRun[N].dump, [date].dumpstream and [date]-jvmRun[N].dumpstream. [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException [Container] 2020/08/10 14:16:40 Command did not exit successfully mvn clean compile test exit status 1 [Container] 2020/08/10 14:16:40 Phase complete: PRE_BUILD State: FAILED [Container] 2020/08/10 14:16:40 Phase context status code: COMMAND_EXECUTION_ERROR Message: Error while executing command: mvn clean compile test. Reason: exit status 1どうやら、テストコードが期待しているレスポンスと一致していないことが原因のようです。
というわけで、springproject/test/java/com/aws/codestar/project/handler/HelloWorldTest.java
を編集します。HelloWorldTest.java変更前private static final String EXPECTED_RESPONSE_VALUE = "Hello World!";HelloWorldTest.java変更後private static final String EXPECTED_RESPONSE_VALUE = "Hello World Update!";$ git add. $ git commit -m "update test code." [master f003591] update test code. Committer: EC2 Default User <ec2-user@ip-172-31-2-200.ec2.internal> Your name and email address were configured automatically based on your username and hostname. Please check that they are accurate. You can suppress this message by setting them explicitly: git config --global user.name "Your Name" git config --global user.email you@example.com After doing this, you may fix the identity used for this commit with: git commit --amend --reset-author 1 file changed, 1 insertion(+), 1 deletion(-) $ git push Enumerating objects: 21, done. Counting objects: 100% (21/21), done. Compressing objects: 100% (5/5), done. Writing objects: 100% (11/11), 715 bytes | 238.00 KiB/s, done. Total 11 (delta 2), reused 0 (delta 0) To https://git-codecommit.us-east-1.amazonaws.com/v1/repos/SpringProject f0ba0d6..f003591 master -> master次はBuildもDeployも成功しました!
しばらくアプリケーションにアクセスすると、結果が変わり、最新のアプリケーションがデプロイされたことが確認できます。{"Output":"Hello World Update!"}まとめ
今回は、IDEにCLoud9を使用するためにリージョンに東京リージョンを使用しませんでしたが、コードリポジトリから普通にGitコマンドを使えば、EclipseやVSSなどのIDEでも開発することはできます。
SAMコマンドなどは、ローカルに環境を作成してからDployする、というアプローチですが、CodeStarの場合は、まずAWS側に雛形を用意して、そこからアプリ〜ケーションを構築していく流れになります。
より簡単に開始するには、コードリポジトリの管理からパイプラインまでオールインワンになっているCodeStar の利用もありかと思います。
できれば、Cloud9が早いうちに東京リージョン対応になることを望みますが、それを差し引いても利用価値はありそうですね。
- 投稿日:2020-08-10T23:39:27+09:00
[AWS] CodeStarを使うと、Lambdaで動作するSpring(Java)のプロジェクトをたった3分で構築できるのです!!
CodeStar
AWS CodeStarは、アプリケーションを迅速に開発・デプロイすることができるための環境を構築することができます。
いくつかのテンプレートが用意されており、マネジメントコンソール上からポチポチと選択していくだけで、開発プロジェクトが構築できてしまいます。構築される環境
- コードリポジトリ(CodeCommitと、GitHubを選択可能)
- CodeBuilde環境
- CodePipeline環境
- アプリケーション
- IDE(Cloud9選択時)
料金
CodeStar自体は料金は発生しません。
CodeStarによって構築されるコードリポジトリや、パイプライン、デプロイされたアプリケーションに係るリソース使用料のみです。注意事項
リージョンに東京リージョンを選択すると、IDEにCloud9を選択することができません(2020年8月現在)。
今回はCloud9で環境を構築したいので、リージョンにバージニア北部(us-east-1)を選択することとします。環境構築手順
まずは、マネジメントコンソールで、サービスよりCodeStarを選択します。
今回はIDEに
Cloud9を使用したいので、リージョンをバージニア北部にします。アプリケーションの種類や、言語を選択できます。
今回は、Lambda + Java Springのテンプレートでプロジェクトを作成してみようと思います。続いてコードリポジトリを選択します。
今回は、CodeCommitを選択してみますね。内容を確認して、プロジェクトを作成します。
最後にIDEを選択します。
今回は、AWS Cloud9です。リージョンによっては、Cloud9を選択できないのでご注意ください。
Cloud9を選択すると、インスタンスタイプを聞かれますので、今回はデフォルトのt2.microのまま進めます。プロジェクトとIDEの作成が始まります(裏でCloudFormationが動作します)。
プロジェクトが大体3分で環境構築が完了します。
IDEはEC2インスタンスの作成があるので、こちらも大体3分くらいかかります。
両方とも準備が完了すると、下記のような画面になります。リポジトリの状態
左側にあるメニューより「コード」を選択すると、CodeCommitの画面が表示されます。
Springのテンプレート構造が作成されており、buildspec.yml
まで作成されています。なお、
buildspec.yml
の中身はこんな感じです。codespec.ymlversion: 0.2 phases: install: runtime-versions: java: openjdk8 commands: # Upgrade AWS CLI to the latest version - pip install --upgrade awscli pre_build: commands: - echo Test started on `date` - mvn clean compile test build: commands: - echo Build started on `date` - mvn package shade:shade - mv target/HelloWorld-1.0.jar . - unzip HelloWorld-1.0.jar - rm -rf target tst src buildspec.yml pom.xml HelloWorld-1.0.jar - aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template template-export.yml post_build: commands: # Do not remove this statement. This command is required for AWS CodeStar projects. # Update the AWS Partition, AWS Region, account ID and project ID in the project ARN on template-configuration.json file so AWS CloudFormation can tag project resources. - sed -i.bak 's/\$PARTITION\$/'${PARTITION}'/g;s/\$AWS_REGION\$/'${AWS_REGION}'/g;s/\$ACCOUNT_ID\$/'${ACCOUNT_ID}'/g;s/\$PROJECT_ID\$/'${PROJECT_ID}'/g' template-configuration.json artifacts: files: - template-export.yml - template-configuration.jsonOpenJDK8で、Mavenでコンパイルされていることがわかりますね。
IDE
では、メニューからIDEを選択してみます。
すると、CLoud9が開始され、CodeCommitのリポジトリから、コードをgit clone
で自動的にコードを取得してくれます。なお、チームの設定を行うと、クロスアカウントでのコードリポジトリの共有や、モブプログラミングなんかも行うことができます。
[AWS] Cloud9でモブプログラミングの環境を作ってみる
アプリ実行
今回は、LambdaによるAPIです。
ダッシュボードにある「アプリの表示」ボタンを押すと、プロジェクトのテンプレートで最初に入っているAPIが呼び出されます。{"Output":"Hello World!"}具体的なAPI GatewayやLambdaはどうなっているのでしょう?
API Gateway
REST APIが一つできていることがわかりますね。
これ自体は、コード上の
tenpkate.yml
に定義されています。template.ymlResources: GetHelloWorld: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'awscodestar-${ProjectId}-lambda-GetHelloWorld' Handler: com.aws.codestar.projecttemplates.handler.HelloWorldHandler Runtime: java8 Role: Fn::GetAtt: - LambdaExecutionRole - Arn Events: GetEvent: Type: Api Properties: Path: / Method: get PostHelloWorld: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'awscodestar-${ProjectId}-lambda-PostHelloWorld' Handler: com.aws.codestar.projecttemplates.handler.HelloWorldHandler Runtime: java8 Role: Fn::GetAtt: - LambdaExecutionRole - Arn Events: PostEvent: Type: Api Properties: Path: / Method: postLambda
こちらは、Get用とPost用の2つが存在します。これも、API Gatewayで記載した
template.yml
で定義さている内容ですね。Pipelineを動かしてみる
Cloud9上で、
springproject/src/main/java/com/aws/codestar/project/handler/HelloWorldHandler.java
を編集してみます。HelloWorldHandler.java変更前return new GatewayResponse(new JSONObject().put("Output", "Hello World!").toString(), headers, 200);HelloWorldHandler.java変更後return new GatewayResponse(new JSONObject().put("Output", "Hello World Update!").toString(), headers, 200);ファイルを保存したら、Cloud9のターミナルから、CodeCommitにPushしてみましょう。
$ git add . $ git commit -m "update response." [master f0ba0d6] update response. Committer: EC2 Default User <ec2-user@ip-172-31-2-200.ec2.internal> Your name and email address were configured automatically based on your username and hostname. Please check that they are accurate. You can suppress this message by setting them explicitly: git config --global user.name "Your Name" git config --global user.email you@example.com After doing this, you may fix the identity used for this commit with: git commit --amend --reset-author 1 file changed, 1 insertion(+), 1 deletion(-) $ git push Enumerating objects: 21, done. Counting objects: 100% (21/21), done. Compressing objects: 100% (6/6), done. Writing objects: 100% (11/11), 719 bytes | 239.00 KiB/s, done. Total 11 (delta 3), reused 0 (delta 0) To https://git-codecommit.us-east-1.amazonaws.com/v1/repos/SpringProject 182e230..f0ba0d6 master -> masterパイプラインを確認すると、ビルドが開始されたことがわかります。
実は、ここでエラーになります。
確認してみると、どうやらテストで失敗しているようですね。[ERROR] Failures: [ERROR] HelloWorldHandlerTest.testHandleRequest:59 expected: <Hello World!> but was: <Hello World Update!> [INFO] [ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 10.048 s [INFO] Finished at: 2020-08-10T14:16:40Z [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.22.0:test (default-test) on project HelloWorld: There are test failures. [ERROR] [ERROR] Please refer to /codebuild/output/src139859990/src/target/surefire-reports for the individual test results. [ERROR] Please refer to dump files (if any exist) [date]-jvmRun[N].dump, [date].dumpstream and [date]-jvmRun[N].dumpstream. [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException [Container] 2020/08/10 14:16:40 Command did not exit successfully mvn clean compile test exit status 1 [Container] 2020/08/10 14:16:40 Phase complete: PRE_BUILD State: FAILED [Container] 2020/08/10 14:16:40 Phase context status code: COMMAND_EXECUTION_ERROR Message: Error while executing command: mvn clean compile test. Reason: exit status 1どうやら、テストコードが期待しているレスポンスと一致していないことが原因のようです。
というわけで、springproject/test/java/com/aws/codestar/project/handler/HelloWorldTest.java
を編集します。HelloWorldTest.java変更前private static final String EXPECTED_RESPONSE_VALUE = "Hello World!";HelloWorldTest.java変更後private static final String EXPECTED_RESPONSE_VALUE = "Hello World Update!";$ git add. $ git commit -m "update test code." [master f003591] update test code. Committer: EC2 Default User <ec2-user@ip-172-31-2-200.ec2.internal> Your name and email address were configured automatically based on your username and hostname. Please check that they are accurate. You can suppress this message by setting them explicitly: git config --global user.name "Your Name" git config --global user.email you@example.com After doing this, you may fix the identity used for this commit with: git commit --amend --reset-author 1 file changed, 1 insertion(+), 1 deletion(-) $ git push Enumerating objects: 21, done. Counting objects: 100% (21/21), done. Compressing objects: 100% (5/5), done. Writing objects: 100% (11/11), 715 bytes | 238.00 KiB/s, done. Total 11 (delta 2), reused 0 (delta 0) To https://git-codecommit.us-east-1.amazonaws.com/v1/repos/SpringProject f0ba0d6..f003591 master -> master次はBuildもDeployも成功しました!
しばらくアプリケーションにアクセスすると、結果が変わり、最新のアプリケーションがデプロイされたことが確認できます。{"Output":"Hello World Update!"}まとめ
今回は、IDEにCloud9を使用するためにリージョンに東京リージョンを使用しませんでしたが、コードリポジトリから普通にGitコマンドを使えば、EclipseやVSSなどのIDEでも開発することはできます。
SAMコマンドなどは、ローカルに環境を作成してからDployする、というアプローチですが、CodeStarの場合は、まずAWS側に雛形を用意して、そこからアプリ〜ケーションを構築していく流れになります。
より簡単に開始するには、コードリポジトリの管理からパイプラインまでオールインワンになっているCodeStarの利用もありかと思います。
今回は、Lambda + Springの組み合わせでしたが、他にもたくさんのテンプレートがあるので、アプリケーションの要件や、開発メンバーのスキルセットにあわせて、プロジェクト構成を選択できるのもうれしいです。
できれば、Cloud9が早いうちに東京リージョン対応になることを望みますが、それを差し引いても利用価値はありそうですね。
- 投稿日:2020-08-10T23:39:27+09:00
[AWS] CodeStarで、Lambda上でSpringのプロジェクトをたった3分で構築する
CodeStar
AWS CodeStarは、アプリケーションを迅速に開発・デプロイすることができるための環境を構築することができます。
いくつかのテンプレートが用意されており、マネジメントコンソール上からポチポチと選択していくだけで、開発プロジェクトが構築できてしまいます。構築される環境
- コードリポジトリ(CodeCommitと、GitHubを選択可能)
- CodeBuilde環境
- CodePipeline環境
- アプリケーション
- IDE(Cloud9選択時)
料金
CodeStar自体は料金は発生しません。
CodeStarによって構築されるコードリポジトリや、パイプライン、デプロイされたアプリケーションに係るリソース使用料のみです。注意事項
リージョンに東京リージョンを選択すると、IDEにCloud9を選択することができません(2020年8月現在)。
今回はCloud9で環境を構築したいので、リージョンにバージニア北部(us-east-1)を選択することとします。環境構築手順
まずは、マネジメントコンソールで、サービスよりCodeStarを選択します。
今回はIDEに
Cloud9を使用したいので、リージョンをバージニア北部にします。アプリケーションの種類や、言語を選択できます。
今回は、Lambda + Java Springのテンプレートでプロジェクトを作成してみようと思います。続いてコードリポジトリを選択します。
今回は、CodeCommitを選択してみますね。内容を確認して、プロジェクトを作成します。
最後にIDEを選択します。
今回は、AWS Cloud9です。リージョンによっては、Cloud9を選択できないのでご注意ください。
Cloud9を選択すると、インスタンスタイプを聞かれますので、今回はデフォルトのt2.microのまま進めます。プロジェクトとIDEの作成が始まります(裏でCloudFormationが動作します)。
プロジェクトが大体3分で環境構築が完了します。
IDEはEC2インスタンスの作成があるので、こちらも大体3分くらいかかります。
両方とも準備が完了すると、下記のような画面になります。リポジトリの状態
左側にあるメニューより「コード」を選択すると、CodeCommitの画面が表示されます。
Springのテンプレート構造が作成されており、buildspec.yml
まで作成されています。なお、
buildspec.yml
の中身はこんな感じです。codespec.ymlversion: 0.2 phases: install: runtime-versions: java: openjdk8 commands: # Upgrade AWS CLI to the latest version - pip install --upgrade awscli pre_build: commands: - echo Test started on `date` - mvn clean compile test build: commands: - echo Build started on `date` - mvn package shade:shade - mv target/HelloWorld-1.0.jar . - unzip HelloWorld-1.0.jar - rm -rf target tst src buildspec.yml pom.xml HelloWorld-1.0.jar - aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template template-export.yml post_build: commands: # Do not remove this statement. This command is required for AWS CodeStar projects. # Update the AWS Partition, AWS Region, account ID and project ID in the project ARN on template-configuration.json file so AWS CloudFormation can tag project resources. - sed -i.bak 's/\$PARTITION\$/'${PARTITION}'/g;s/\$AWS_REGION\$/'${AWS_REGION}'/g;s/\$ACCOUNT_ID\$/'${ACCOUNT_ID}'/g;s/\$PROJECT_ID\$/'${PROJECT_ID}'/g' template-configuration.json artifacts: files: - template-export.yml - template-configuration.jsonOpenJDK8で、Mavenでコンパイルされていることがわかりますね。
IDE
では、メニューからIDEを選択してみます。
すると、CLoud9が開始され、CodeCommitのリポジトリから、コードをgit clone
で自動的にコードを取得してくれます。あお、チームの設定を行うと、クロスアカウントでのコードリポジトリの共有や、モブプログラミングなんかも行うことができます。
アプリ実行
今回は、LambdaによるAPIです。
ダッシュボードにある「アプリの表示」ボタンを押すと、プロジェクトのテンプレートで最初に入っているAPIが呼び出されます。{"Output":"Hello World!"}具体的なAPI GatewayやLambdaはどうなっているのでしょう?
API Gateway
REST APIが一つできていることがわかりますね。
これ自体は、コード上の
tenpkate.yml
に定義されています。template.ymlResources: GetHelloWorld: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'awscodestar-${ProjectId}-lambda-GetHelloWorld' Handler: com.aws.codestar.projecttemplates.handler.HelloWorldHandler Runtime: java8 Role: Fn::GetAtt: - LambdaExecutionRole - Arn Events: GetEvent: Type: Api Properties: Path: / Method: get PostHelloWorld: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'awscodestar-${ProjectId}-lambda-PostHelloWorld' Handler: com.aws.codestar.projecttemplates.handler.HelloWorldHandler Runtime: java8 Role: Fn::GetAtt: - LambdaExecutionRole - Arn Events: PostEvent: Type: Api Properties: Path: / Method: postLambda
こちらは、Get用とPost用の2つが存在します。これも、API Gatewayで記載した
template.yml
で定義さている内容ですね。Pipelineを動かしてみる
Cloud9上で、
springproject/src/main/java/com/aws/codestar/project/handler/HelloWorldHandler.java
を編集してみます。HelloWorldHandler.java変更前return new GatewayResponse(new JSONObject().put("Output", "Hello World!").toString(), headers, 200);HelloWorldHandler.java変更後return new GatewayResponse(new JSONObject().put("Output", "Hello World Update!").toString(), headers, 200);ファイルを保存したら、Cloud9のターミナルから、CodeCommitにPushしてみましょう。
$ git add . $ git commit -m "update response." [master f0ba0d6] update response. Committer: EC2 Default User <ec2-user@ip-172-31-2-200.ec2.internal> Your name and email address were configured automatically based on your username and hostname. Please check that they are accurate. You can suppress this message by setting them explicitly: git config --global user.name "Your Name" git config --global user.email you@example.com After doing this, you may fix the identity used for this commit with: git commit --amend --reset-author 1 file changed, 1 insertion(+), 1 deletion(-) $ git push Enumerating objects: 21, done. Counting objects: 100% (21/21), done. Compressing objects: 100% (6/6), done. Writing objects: 100% (11/11), 719 bytes | 239.00 KiB/s, done. Total 11 (delta 3), reused 0 (delta 0) To https://git-codecommit.us-east-1.amazonaws.com/v1/repos/SpringProject 182e230..f0ba0d6 master -> masterパイプラインを確認すると、ビルドが開始されたことがわかります。
実は、ここでエラーになります。
確認してみると、どうやらテストで失敗しているようですね。[ERROR] Failures: [ERROR] HelloWorldHandlerTest.testHandleRequest:59 expected: <Hello World!> but was: <Hello World Update!> [INFO] [ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 10.048 s [INFO] Finished at: 2020-08-10T14:16:40Z [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.22.0:test (default-test) on project HelloWorld: There are test failures. [ERROR] [ERROR] Please refer to /codebuild/output/src139859990/src/target/surefire-reports for the individual test results. [ERROR] Please refer to dump files (if any exist) [date]-jvmRun[N].dump, [date].dumpstream and [date]-jvmRun[N].dumpstream. [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException [Container] 2020/08/10 14:16:40 Command did not exit successfully mvn clean compile test exit status 1 [Container] 2020/08/10 14:16:40 Phase complete: PRE_BUILD State: FAILED [Container] 2020/08/10 14:16:40 Phase context status code: COMMAND_EXECUTION_ERROR Message: Error while executing command: mvn clean compile test. Reason: exit status 1どうやら、テストコードが期待しているレスポンスと一致していないことが原因のようです。
というわけで、springproject/test/java/com/aws/codestar/project/handler/HelloWorldTest.java
を編集します。HelloWorldTest.java変更前private static final String EXPECTED_RESPONSE_VALUE = "Hello World!";HelloWorldTest.java変更後private static final String EXPECTED_RESPONSE_VALUE = "Hello World Update!";$ git add. $ git commit -m "update test code." [master f003591] update test code. Committer: EC2 Default User <ec2-user@ip-172-31-2-200.ec2.internal> Your name and email address were configured automatically based on your username and hostname. Please check that they are accurate. You can suppress this message by setting them explicitly: git config --global user.name "Your Name" git config --global user.email you@example.com After doing this, you may fix the identity used for this commit with: git commit --amend --reset-author 1 file changed, 1 insertion(+), 1 deletion(-) $ git push Enumerating objects: 21, done. Counting objects: 100% (21/21), done. Compressing objects: 100% (5/5), done. Writing objects: 100% (11/11), 715 bytes | 238.00 KiB/s, done. Total 11 (delta 2), reused 0 (delta 0) To https://git-codecommit.us-east-1.amazonaws.com/v1/repos/SpringProject f0ba0d6..f003591 master -> master次はBuildもDeployも成功しました!
しばらくアプリケーションにアクセスすると、結果が変わり、最新のアプリケーションがデプロイされたことが確認できます。{"Output":"Hello World Update!"}まとめ
今回は、IDEにCLoud9を使用するためにリージョンに東京リージョンを使用しませんでしたが、コードリポジトリから普通にGitコマンドを使えば、EclipseやVSSなどのIDEでも開発することはできます。
SAMコマンドなどは、ローカルに環境を作成してからDployする、というアプローチですが、CodeStarの場合は、まずAWS側に雛形を用意して、そこからアプリ〜ケーションを構築していく流れになります。
より簡単に開始するには、コードリポジトリの管理からパイプラインまでオールインワンになっているCodeStar の利用もありかと思います。
できれば、Cloud9が早いうちに東京リージョン対応になることを望みますが、それを差し引いても利用価値はありそうですね。
- 投稿日:2020-08-10T22:38:52+09:00
RXTXってもう使えない?代替方法は?
久し振りにシリアル通信を
久し振りにJavaでシリアル通信をしようと思っていろいろ調べてみた。Javaでプログラム書くのも久し振りだし、昔はJavaシリアル通信ならRXTXしかなかったんだが、今はその後継というか代替というか、そういう記事が検索で挙がっていた。以下は「Java シリアル通信」でGoogle検索した時の上位3つの記事だ。
1. Javaでシリアル通信を使ってみよう http://web.sfc.wide.ad.jp/~tinaba/tutorials/serial-j/index.html
2. Javaのシリアル通信ライブラリRXTXの課題と代替調査 https://qiita.com/kyosho/items/fc0c40458385c7d98232
3. ソフトウェアエンジニアリング/Java シリアル通信https://www.torutk.com/projects/swe/wiki/Java%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%AB%E9%80%9A%E4%BF%A1
最初のやつはRXTXの使い方、次とその次が代替ライブラリの提案になっている。準備作業としてcom0comを
モニタ用のCOMポートとしてNull-modem emulator(https://sourceforge.net/projects/com0com/)をインストールしておく。Windows10(64bit)にはインストールできないような場合もあるという記事も多くヒットするが、こちら(Windows10 Home 64bit)では何の問題もなくインストールできた。ちなみに開発環境はPleiades All In Oneの「Eclipse 2020 (Windows 64bit版)」「Platform」の「Full Edition」でJDTだけ追加インストールした。COMモニタ用ターミナルにはお馴染みの「Tera Term」を使っている。
代替できるかな
ここでは以下の4つのテストをしてみたが、自分の環境(Windows10 Home)で「PrintWriterなどで文字出力できる(送信)」「ターミナルからの文字入力をEventで受け取れる(エベント受信)」が出来なかったのはRXTXだけだった。がどれも完全ではない。なお、ポーリング受信は試してもいない。
1. RXTX [http://fizzed.com/oss/rxtx-for-java]
2. jSerialComm [http://fazecast.github.io/jSerialComm/]
3. jpurejavacomm [http://www.sparetimelabs.com/purejavacomm/purejavacomm.php]
4. jssc [https://github.com/scream3r/java-simple-serial-connector]RXTXを久し振りに試す
上に示したサイトから「mfz-rxtx-2.2-20081207-win-x64.zip」を持ってきた。binの下にdllを入れろとかlib\extにjarを入れろと言われるが、openjdk11だとlib/extというフォルダーが無いので、jarはEclipseのビルドパスで「外部アーカイブの追加」でライブラリに追加した。dllはEclipseフォルダの下にjre/binがあるので、そこに突っ込んだ。
import文は省略するが、クラス宣言、インタフェース、main関数(try/catchが面倒なのでthrows文を追加)は以下のようにした。他を使うときもだいたい同じである。public class RXTXtest implements SerialPortEventListener { public static void main(String[] args) throws Exception { new RXTXtest(); } ...(中略) }コンストラクタと最初の処理は以下の通り。EnumerationでアクセスできるCOMポート一覧を表示させている。特に問題なく表示できる。
String name; SerialPort port; RXTXtest() throws Exception { name=this.getClass().getSimpleName(); Enumeration<CommPortIdentifier> ids=CommPortIdentifier.getPortIdentifiers(); while(ids.hasMoreElements()) { CommPortIdentifier id=(CommPortIdentifier)ids.nextElement(); System.out.println(id.getName()); } ...中略 }実際にCOMポートをオープンしてPrintWriterで書き込んでみる(送信機能)と、Fatal Errorが出た。com0comで「COM7&COM8」がペアになっており、Tera Termで「COM8」側に接続して「いない」とエラーは出ないが、接続してモニタされているとFATAL ERRORになる。flush()手前までは大丈夫だったが、flush()するとFATALになる。
CommPortIdentifier id=CommPortIdentifier.getPortIdentifier("COM7"); SerialPort port=(SerialPort) id.open(name, 2000); port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); port.addEventListener(this); PrintWriter pw=new PrintWriter(port.getOutputStream()); pw.println(name+": "+"Tested!"); pw.flush();参考のためFATAL ERROR全文を載せておく。こんなんどうしろと。
# # A fatal error has been detected by the Java Runtime Environment: # # EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000180004465, pid=4480, tid=4852 # # JRE version: OpenJDK Runtime Environment (11.0.7+10) (build 11.0.7+10) # Java VM: OpenJDK 64-Bit Server VM (11.0.7+10, mixed mode, tiered, compressed oops, g1 gc, windows-amd64) # Problematic frame: # C [rxtxSerial.dll+0x4465] # # No core dump will be written. Minidumps are not enabled by default on client versions of Windows # # An error report file with more information is saved as: # C:\Users\akira\Documents\workspace\SerialTesting1\hs_err_pid4480.log # # If you would like to submit a bug report, please visit: # https://github.com/AdoptOpenJDK/openjdk-support/issues # The crash happened outside the Java Virtual Machine in native code. # See problematic frame for where to report the bug. #jSerialCommを試す
http://fazecast.github.io/jSerialComm/の上の方の「jar」ボタンから「jSerialComm-2.6.2.jar」を持ってきた。これも「外部アーカイブの追加」でビルドパスに入れておく。
コンストラクタ周辺は以下の通り。インタフェースはサンプルソースに倣って処理中に書いたので、冒頭では宣言しない。public class JSerialCommTest { public static void main(String[] args) throws Exception { new JSerialCommTest(); } ...中略 }使用できるポートの列挙は配列で示される。直感的だがスマートじゃない。また、隠蔽されているはずのcom0comの特殊ペアポート「CNCA0」「CNCB0」も列挙される(RXTXでは出て来なかった)。
SerialPort port; String name; JSerialCommTest() throws Exception { name=this.getClass().getSimpleName(); SerialPort[] ports=SerialPort.getCommPorts(); for(int i=0;i<ports.length;i++) System.out.println(ports[i].getSystemPortName()); ...中略 }オープンして出力するところは特に問題なく実行できた。少なくとも一方的に送信はできる(RXTXではpw.flush()でFATALを吐いた)。
port=SerialPort.getCommPort("COM7"); port.setComPortParameters(9600, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY); port.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED); port.openPort(); PrintWriter pw=new PrintWriter(port.getOutputStream()); pw.println(name+": Tested!"); pw.flush();問題は受信側である。以下の様に無限ループで入力を待ちながら、イベント監視すると例外が発生したり無反応だったりする。例外はTimeOut関係だったが、実力不足でこれもうまく解決できなかった。
port.addDataListener(new SerialPortDataListener() { public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; } public void serialEvent(SerialPortEvent event) { if (event.getEventType() != SerialPort.LISTENING_EVENT_DATA_AVAILABLE) return; byte[] newData = new byte[port.bytesAvailable()]; int numRead = port.readBytes(newData, newData.length); pw.println("Read " + numRead + " bytes."); } }); while(true) {}jpurejavacommを試す
オジサンはもう疲れたよ。次はjpurejavacommを使ってみよう。https://github.com/nyholku/purejavacommのbinフォルダから、「purejavacomm-1.0.3.jar」を持ってきた。これだけでは動かないらしく「jna.jar」も必要だった。ここhttps://github.com/java-native-access/jnaのdistフォルダの下にある。
クラス定義は他とだいたい同じ。ここでもインタフェースは匿名で処理中に書くようにしたので、implementsは無い。public class JPureJavaCommTest { public static void main(String[] args) throws Exception { new JPureJavaCommTest(); } ...中略 }コンストラクタ~使えるポートの列挙は普通にできた。ここまではどのライブラリも問題ないのだが。
SerialPort port; String name; JPureJavaCommTest() throws Exception { name=this.getClass().getSimpleName(); Enumeration<CommPortIdentifier> e=CommPortIdentifier.getPortIdentifiers(); while(e.hasMoreElements()) { CommPortIdentifier id=(CommPortIdentifier)e.nextElement(); System.out.println(id.getName()); } ...中略 }ポートのオープンから送信機能の確認までも何の問題もなく終了。RXTXはダメだったが、jSerialCommもここまでは良かった。
CommPortIdentifier id=CommPortIdentifier.getPortIdentifier("COM7"); port=(SerialPort)id.open(name,2000); port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,SerialPort.PARITY_NONE); port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); PrintWriter pw=new PrintWriter(port.getOutputStream()); pw.println(name+": Tested!"); pw.flush();さてイベント受信はどうかというと反応なし。エラーも例外も吐かない。どうやら使い方がおかしいのか?
port.notifyOnDataAvailable(true); port.addEventListener(new SerialPortEventListener() { public void serialEvent(SerialPortEvent arg0) { pw.println(name+": SerialEvent!"); }}); while(true) {}jsscを試すぜ
もう駄目だ。javaではシリアル送信はできても受信はできないんだ!!でも最後にjsscを試してみるか!ここhttps://github.com/java-native/jssc/releasesから「jssc-2.9.2.jar」を落としたのだが、ちょっと使ってみたらNGっぽかったので、一つ前の「jssc-2.9.1.jar」を使った(外国人が前のバージョンじゃ出なかったエラーだよ、バグじゃね?って書いてたので)。
もうこの時点で半ば諦めムードだ。しかも「native-lib-loader-2.3.4.jar」「slf4j-api-1.7.30.jar」「slf4j-simple-1.7.30.jar」も必要らしい。なんだよMavenって。知らないよ…
クラス定義は以下の通り。インタフェースはなぜか処理中に「new SerialPortEventListener()」が出来ない(コンストラクタが無い?)ので、implements文で追加してある。public class JSSCtest implements SerialPortEventListener { public static void main(String[] args) throws Exception { new JSSCtest(); } ...中略 }使えるポートの列挙も普通にできる。まぁRXTXだって出来たからな。ここはEnumurationじゃなくString[]配列を返してくる。趣味には合わないがシンプルでいい。だが、ここでも隠蔽されるべき(これらはデバイスマネージャに出て来ない)「CNCA0」「CNCB0」も列挙される。
なお、ここで文字lsにline.separatorを設定しているのには意味がある。SerialPort port; String ls; JSSCtest() throws Exception { ls=System.getProperty("line.separator"); String[] ports = SerialPortList.getPortNames(); for(int i = 0; i < ports.length; i++) { System.out.println(ports[i]); } ...中略 }ポートを開けたり送信したりするのも特に問題なくできるが、何故かOutputStreamを取得できず、writeStringなどというメソッドを使うしかないらしい。上手く改行できなかったので文字lsを使った(中身はSystem.geyProperty("line.separator"))が、Tera Term側で\nを受け取れるように設定してもよい。
port=new SerialPort("COM7"); port.openPort(); port.setParams(SerialPort.BAUDRATE_9600,SerialPort.DATABITS_8, SerialPort.STOPBITS_1,SerialPort.PARITY_NONE); port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); port.writeString("JSSC Test!"+ls); port.addEventListener(this); while(true) {}さて問題の受信だが、結論としてはイベントは発生できたが、受信される中身がよくわからない。
public void serialEvent(SerialPortEvent arg0) { try { byte[] bs=port.readBytes(); System.out.println(bs); } catch (SerialPortException e) { e.printStackTrace(); } }Tera Termから「aaabb」と手入力したら以下の様に返ってくる。1文字1イベントで分かりやすいのだがこりゃなんだ?
[B@1df7e046 [B@19c87720 [B@5ccaea7d [B@4f8bbf28 [B@6d5c934f結論
- RXTXはFATAL ERRORを吐く。お手上げ。もうメンテされていないしお蔵入り!
- jSerialCommは不安定。無反応だったり例外を吐く。もうちょっと頑張るか?
- jpurejavacommはエベント受信が無反応。使い方が悪いのか?
- jsscはもしかしたら何とかなるかもしれない。受信内容の精査が必要。
- 投稿日:2020-08-10T21:41:16+09:00
【チュートリアル】Spring Batch
springのチュートリアルバッチサービスの作成をやってみた。
適当なディレクトリでgit clone
$ git clone https://github.com/spring-guides/gs-batch-processing.git$ mba-2:target #{myname}$ java -jar /Users/#{myname}/projects/gs-batch-processing/complete/target/batch-processing-0.0.1-SNAPSHOT.jar. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.2.RELEASE) 2020-08-10 21:19:13.517 INFO 50375 --- [ main] c.e.b.BatchProcessingApplication : Starting BatchProcessingApplication v0.0.1-SNAPSHOT on mba-2 with PID 50375 (/Users/#{myname}/projects/gs-batch-processing/complete/target/batch-processing-0.0.1-SNAPSHOT.jar started by #{myname} in /Users/#{myname}/projects/gs-batch-processing/complete/target) 2020-08-10 21:19:13.520 INFO 50375 --- [ main] c.e.b.BatchProcessingApplication : No active profile set, falling back to default profiles: default 2020-08-10 21:19:14.662 INFO 50375 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2020-08-10 21:19:14.671 WARN 50375 --- [ main] com.zaxxer.hikari.util.DriverDataSource : Registered driver with driverClassName=org.hsqldb.jdbcDriver was not found, trying direct instantiation. 2020-08-10 21:19:15.098 INFO 50375 --- [ main] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Driver does not support get/set network timeout for connections. (feature not supported) 2020-08-10 21:19:15.103 INFO 50375 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2020-08-10 21:19:15.430 INFO 50375 --- [ main] o.s.b.c.r.s.JobRepositoryFactoryBean : No database type set, using meta data indicating: HSQL 2020-08-10 21:19:15.716 INFO 50375 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : No TaskExecutor has been set, defaulting to synchronous executor. 2020-08-10 21:19:15.825 INFO 50375 --- [ main] c.e.b.BatchProcessingApplication : Started BatchProcessingApplication in 2.835 seconds (JVM running for 3.415) 2020-08-10 21:19:15.826 INFO 50375 --- [ main] o.s.b.a.b.JobLauncherCommandLineRunner : Running default command line with: [] 2020-08-10 21:19:15.912 INFO 50375 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [FlowJob: [name=importUserJob]] launched with the following parameters: [{run.id=1}] 2020-08-10 21:19:15.992 INFO 50375 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step1] 2020-08-10 21:19:16.062 INFO 50375 --- [ main] c.e.batchprocessing.PersonItemProcessor : Converting (firstName: Jill, lastName: Doe) into (firstName: JILL, lastName: DOE) 2020-08-10 21:19:16.062 INFO 50375 --- [ main] c.e.batchprocessing.PersonItemProcessor : Converting (firstName: Joe, lastName: Doe) into (firstName: JOE, lastName: DOE) 2020-08-10 21:19:16.062 INFO 50375 --- [ main] c.e.batchprocessing.PersonItemProcessor : Converting (firstName: Justin, lastName: Doe) into (firstName: JUSTIN, lastName: DOE) 2020-08-10 21:19:16.063 INFO 50375 --- [ main] c.e.batchprocessing.PersonItemProcessor : Converting (firstName: Jane, lastName: Doe) into (firstName: JANE, lastName: DOE) 2020-08-10 21:19:16.063 INFO 50375 --- [ main] c.e.batchprocessing.PersonItemProcessor : Converting (firstName: John, lastName: Doe) into (firstName: JOHN, lastName: DOE) 2020-08-10 21:19:16.074 INFO 50375 --- [ main] o.s.batch.core.step.AbstractStep : Step: [step1] executed in 82ms 2020-08-10 21:19:16.080 INFO 50375 --- [ main] c.e.b.JobCompletionNotificationListener : !!! JOB FINISHED! Time to verify the results 2020-08-10 21:19:16.083 INFO 50375 --- [ main] c.e.b.JobCompletionNotificationListener : Found <firstName: JILL, lastName: DOE> in the database. 2020-08-10 21:19:16.083 INFO 50375 --- [ main] c.e.b.JobCompletionNotificationListener : Found <firstName: JOE, lastName: DOE> in the database. 2020-08-10 21:19:16.083 INFO 50375 --- [ main] c.e.b.JobCompletionNotificationListener : Found <firstName: JUSTIN, lastName: DOE> in the database. 2020-08-10 21:19:16.083 INFO 50375 --- [ main] c.e.b.JobCompletionNotificationListener : Found <firstName: JANE, lastName: DOE> in the database. 2020-08-10 21:19:16.083 INFO 50375 --- [ main] c.e.b.JobCompletionNotificationListener : Found <firstName: JOHN, lastName: DOE> in the database. 2020-08-10 21:19:16.088 INFO 50375 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [FlowJob: [name=importUserJob]] completed with the following parameters: [{run.id=1}] and the following status: [COMPLETED] in 107ms 2020-08-10 21:19:16.092 INFO 50375 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 2020-08-10 21:19:16.102 INFO 50375 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
- 投稿日:2020-08-10T20:54:39+09:00
@Builderと@NoArgsConstructor一緒に使える方法
課題
以下のように@Builderと@NoArgsConstructorを一緒に使うとエラーが発生する。
@Builder @NoArgsConstructor public class MyName { private String first; private String last; }解決
@AllArgsConstructorアノテーションも入れるかすべてのフィールドを持つコンストラクタを追加すればできる。
@Builder @NoArgsConstructor @AllArgsConstructor public class MyName { private String first; private String last; } .. または .. @Builder @NoArgsConstructor public class MyName { private String first; private String last; MyName(String first, String last) { ... } }
- 投稿日:2020-08-10T19:36:31+09:00
JAVASCRIPT 開発者を採用する方法
JavaScript言語は、JavaおよびC言語に基づいた構成を持っている、強力なクライアント側のマルチパラダイムの動的言語です。それに多数のタイプとオペレーター、組み込みオブジェクトやメソッドが含まれています。JavaScriptはオブジェクト指向プログラミングと関数型プログラミングの両方をサポートしていますので、1つの言語内でほぼどんなオブジェクトまたは機能でも作成できます。
JavaScriptの起源と最新のトレンドに詳しいJavaScript プログラマーを採用する方法
JavaScriptは単独で実行不可能であり、JavaScriptコードを実行するブラウザーが存在します。 ユーザーがJavaScriptの有効なHTMLページを開こうとすると、スクリプトがブラウザーに送信され、ブラウザーはスクリプトの下で動作します。ブラウザー以外に、JavaScriptはAdobeサービス、サーバーサイド環境、データベース、SVG画像などで表示することもできます。JavaScript言語は幅広いタイプのアプリケーションに使用できます。
JavaScript開発の基本、利点やトラップ
2018年にリリースされたState of the Developer Ecosystemのレポートによると、JavaScriptは3年連続で、世界で最も使用されているプログラミング言語であると認識されました。この調査は、世界中の17か国の6千人のプログラマーを対象に実施されました。
JavaScriptプログラミング言語を使用し、他の言語の専門家の代わりにJavaScript プログラマーを採用する利点と欠点を説明します。間違いなく、利点の方が多いです。
Javascript プログラマー コスト
PayScaleによるトップ5か国のフリーランスベースでのJavaScript エンジニアの平均年間報酬をご覧ください。
その国の一般的なJavaScript プログラマーの時給を詳しく見てみると、1時間あたりの報酬はイギリスが一番高いことが明らかになります。それでも、ウクライナのフリーランスJavaScript 開発者の時給は、JavaScriptプログラマーが勤務ないしは居住する上位国の中で最低です。ウクライナからのJavaScriptプログラマーが技術的知識と創造力において、世界中で非常に人気であるということはよく知られています。したがって、トップクラスのフリーランスJavaScriptエンジニアを採用する予定がある場合は、ウクライナのコーダーの採用をご検討ください。
ウクライナの開発者にご興味のある方は、以下のリンクをご参照ください。
https://jp.mobilunity.com/blog/hire-javascript-developer-jp/
- 投稿日:2020-08-10T18:22:24+09:00
Eclipseプラグイン・プロジェクトでJavaのクラスのメソッド・パラメータの名前をリフレクションで取得できない時の暫定対応メモ
Java8以降、クラスのコンストラクタを含むメソッドのパラメータ名をリフレクションを通して取得できるようになりました。
javac
に-parameters
オプションを追加してコンパイルすることで、これに必要な情報がクラスファイルに格納されます。Eclipseにおいては、例えばJavaプロジェクトの場合、プロジェクト共通あるいはプロジェクト毎の設定において、以下のチェックを入れてエクスポートすることで、対応したjarを作成することができます。恐らく、チェックすることで、
-parameters
オプションが内部で設定されてコンパイルされるのだろうと思います。
簡略ですが、以下のような感じでコンストラクタやメソッドオブジェクトへの参照からリフレクションを使って、パラメータ名を確認することができます。for (Constructor<?> constructor : clazz.getDeclaredConstructors()) { System.out.println("constructor: " + constructor.getName()); for (Parameter param : constructor.getParameters()) { System.out.println(" param: " + param.getName() + ", type: " + param.getType().getSimpleName()); } }for (Method method : clazz.getDeclaredMethods()) { System.out.println("method: " + method.getName()); for (Parameter param : method.getParameters()) { System.out.println(" param: " + param.getName() + ", type: " + param.getType().getSimpleName()); } }問題の症状
ところが、プラグイン・プロジェクトの場合、何故か、この設定が機能していないようで、エクスポートして作成した
jar
(というかバンドル)では、メソッドのパラメータ名は、arg0, arg1, arg2…
となります。このような名前は、-parameters
オプションを指定しない場合に、自動的に付けられるものです。なお、使用しているEclipse IDEは、Version: 2020-06 (4.16.0) Build id: 20200615-1200
です。(多分、最新)このことから、プラグイン・プロジェクトの場合、チェックを入れても、実際には
-paramters
オプションが使用されずにコンパイルされているように見えます。なお、チェックを入れると、以下のファイルに次の一行が追加されます。<プロジェクト>/.settings/org.eclipse.jdt.core.prefs
org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
この設定が入ったプロジェクトでエクスポートすることで、メソッドのパラメータ名をリフレクションで取得可能なjar
が作成されることが期待されます。プラグイン・プロジェクトの場合、バンドルの依存関係を明示した
MANIFEST.MF
を作成して、手軽にOSGiバンドルを作成できて重宝なのですが、今回の件は、かなり不便に感じました。暫定対応
そこで、一旦、プラグイン・プロジェクトで作成した、バンドルの依存関係を解決した
MANIFEST.MF
の雛形を、別途、そのプラグイン・プロジェクトと同一ソースのJavaプロジェクトを起こして、そこから参照させてOSGiバンドルを作成することにしました。(安易ですが)以上、備忘録でした。(何か誤解しているかも?)
- 投稿日:2020-08-10T17:10:13+09:00
メルカリで検索した一覧から金額の簡単な統計分析 Java開発
今回作成したシステムについて
今回はJava言語を使って、ECサイト上の商品から価格の相場などを提供するためのシステムを作りました。
個人開発のため、少しの空き時間で作成したプロトタイプになります。
いろんなレビューをいただけたら幸いです。
込み入った技術の話は割愛します。もしかしたら、他の投稿で書くかも…使用した主な技術
- [Java] Web Scraping (Webスクレイピング)
- [Java] GUI 操作
- [Java] グラフ、チャートのプロット
- [sh script(シェル)] コマンドラインからの設定と起動
全体像(イメージ)
デモンストレーション
追記
本当は技術的な話を書きたかったけど、友達がみて簡単にわかる程度で。
友達が興味とか示してくれればシリーズ化して深く書き上げたい!
ちなみにソースコードはGitHubにアップロードしています。
- 投稿日:2020-08-10T17:10:00+09:00
[Java]スライドマスターを作成して適用する
PPTマスターには強力なテンプレート機能があり、ユーザーは必要に応じてスライドのタイトルテキスト、背景画像、テーマの色などをデザインできます。PPTマスターデザインが成功したら、このテンプレートを直接呼び出して他のスライドに適用し、編集の繰り返しを回避できます。
この記事では、Javaアプリケーションでコードを使用してスライドマスタースタイルを作成し、それを他のスライドに適用する方法を紹介します。JARパッケージのインポート
方法1:Free Spire.Presentation for Javaをダウンロードして解凍し、libフォルダーのSpire.Presentation.jarパッケージを依存関係としてJavaアプリケーションにインポートします。
方法2:Mavenリポジトリから直接JARパッケージをインストールし、pom.xmlファイルを次のように構成します。<repositories> <repository> <id>com.e-iceblue</id> <name>e-iceblue</name> <url>http://repo.e-iceblue.com/nexus/content/groups/public/</url> </repository> </repositories> <dependencies> <dependency> <groupId>e-iceblue</groupId> <artifactId>spire.presentation.free</artifactId> <version>2.6.1</version> </dependency> </dependencies>ユニークなマスターを作成
import com.spire.presentation.*; import com.spire.presentation.drawing.BackgroundType; import com.spire.presentation.drawing.FillFormatType; import com.spire.presentation.drawing.IImageData; import com.spire.presentation.drawing.PictureFillType; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.FileInputStream; public class CreateSlideMaster { public static void main(String[] args) throws Exception { //PPTドキュメントを作成し、スライドサイズを指定する Presentation presentation = new Presentation(); presentation.getSlideSize().setType(SlideSizeType.SCREEN_16_X_9); //最初のスライドマスターを取得する IMasterSlide masterSlide = presentation.getMasters().get(0); //画像アドレスを取得 String backgroundPic = "pic.jpg"; String logo = "logo.jpg"; //スライドマスターの背景を設定する BufferedImage image = ImageIO.read(new FileInputStream(backgroundPic)); IImageData imageData = presentation.getImages().append(image); masterSlide.getSlideBackground().setType(BackgroundType.CUSTOM); masterSlide.getSlideBackground().getFill().setFillType(FillFormatType.PICTURE); masterSlide.getSlideBackground().getFill().getPictureFill().setFillType(PictureFillType.STRETCH); masterSlide.getSlideBackground().getFill().getPictureFill().getPicture().setEmbedImage(imageData); //スライドマスターに画像を追加する image = ImageIO.read(new FileInputStream(logo)); imageData = presentation.getImages().append(image); IEmbedImage imageShape = masterSlide.getShapes().appendEmbedImage(ShapeType.RECTANGLE,imageData,new Rectangle2D.Float(60,60,220,80)); imageShape.getLine().setFillType(FillFormatType.NONE); //スライドマスターにテキストを追加する IAutoShape textShape = masterSlide.getShapes().appendShape(ShapeType.RECTANGLE, new Rectangle2D.Float((float) presentation.getSlideSize().getSize().getWidth()-200,(float) presentation.getSlideSize().getSize().getHeight()-50,200,30));//Shapes.AppendShape(ShapeType.Rectangle, new RectangleF(ppt.SlideSize.Size.Width-200, ppt.SlideSize.Size.Height-60, 200, 30)); textShape.getTextFrame().setText("作業概要レポート"); textShape.getTextFrame().getTextRange().setFontHeight(20f); textShape.getTextFrame().getTextRange().getFill().setFillType(FillFormatType.SOLID); textShape.getTextFrame().getTextRange().getFill().getSolidColor().setColor(Color.BLUE); textShape.getTextFrame().getTextRange().getParagraph().setAlignment(TextAlignmentType.CENTER); textShape.getFill().setFillType(FillFormatType.NONE); textShape.getLine().setFillType(FillFormatType.NONE); //スライドを追加 presentation.getSlides().append(); //ドキュメントを保存します presentation.saveToFile("output/SlideMaster.pptx", FileFormat.PPTX_2013); presentation.dispose(); } }複数のマスターを作成し、スライドに個別に適用する
import com.spire.presentation.*; import com.spire.presentation.drawing.BackgroundType; import com.spire.presentation.drawing.FillFormatType; import com.spire.presentation.drawing.IImageData; import com.spire.presentation.drawing.PictureFillType; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.FileInputStream; public class CreateMultiSlideMasters { public static void main(String[] args) throws Exception { //新しいPPTドキュメントを作成する Presentation presentation = new Presentation(); presentation.getSlideSize().setType(SlideSizeType.SCREEN_16_X_9); //4枚のスライドを挿入する(デフォルトのスライドと合わせて、ドキュメントには5ページあります) for (int i = 0; i < 4; i++) { presentation.getSlides().append(); } //デフォルトのスライドマスターを取得する IMasterSlide first_master = presentation.getMasters().get(0); //2番目のスライドマスターを作成して取得する presentation.getMasters().appendSlide(first_master); IMasterSlide second_master = presentation.getMasters().get(1); //2つのマスターに異なる背景画像を設定する String pic1 = "image1.jpg"; String pic2 = "image2.jpg"; BufferedImage image = ImageIO.read(new FileInputStream(pic1)); IImageData imageData = presentation.getImages().append(image); first_master.getSlideBackground().setType(BackgroundType.CUSTOM); first_master.getSlideBackground().getFill().setFillType(FillFormatType.PICTURE); first_master.getSlideBackground().getFill().getPictureFill().setFillType(PictureFillType.STRETCH); first_master.getSlideBackground().getFill().getPictureFill().getPicture().setEmbedImage(imageData); image = ImageIO.read(new FileInputStream(pic2)); imageData = presentation.getImages().append(image); second_master.getSlideBackground().setType(BackgroundType.CUSTOM); second_master.getSlideBackground().getFill().setFillType(FillFormatType.PICTURE); second_master.getSlideBackground().getFill().getPictureFill().setFillType(PictureFillType.STRETCH); second_master.getSlideBackground().getFill().getPictureFill().getPicture().setEmbedImage(imageData); //最初のスライドマスターとレイアウトを最初のページに適用します(プレート6は空です) presentation.getSlides().get(0).setLayout(first_master.getLayouts().get(6)); //2番目のスライドマスターとレイアウトを残りのスライドに適用する for (int i = 1; i < presentation.getSlides().getCount(); i++) { presentation.getSlides().get(i).setLayout(second_master.getLayouts().get(6)); } //ドキュメントを保存します presentation.saveToFile("MultiSlideMaters.pptx", FileFormat.PPTX_2013); presentation.dispose(); } }
- 投稿日:2020-08-10T16:55:06+09:00
アップロードされたファイルの拡張子とサイズをチェックする方法
- 環境
- CentOS Linux release 7.8.2003 (Core)
- Eclipse IDE for Enterprise Java Developers.Version: 2020-03 (4.15.0)
- openjdk version "11.0.7" 2020-04-14 LTS
- JSF 2.3.9
やりたいこと
- アップロードされたファイルの拡張子が指定のもの以外の場合はエラーにしたい
- アップロードされたファイルのサイズが指定より大きかった場合はエラーにしたい
- エラーメッセージは親画面で指定したい
Fileインターフェースから名前やサイズを取り出してチェック
File オブジェクトは特別な種類の Blob オブジェクトであり、 Blob が利用できる場面ではどこでも利用できます。
File - Web API | MDNファイルの拡張子が指定のもの以外の場合はエラーにしたい
upload.js/** * 拡張子が正しいか判定する. * @param {string} ファイル名. * @return {Boolean} true:正しい. */ function isCorrectExtension(name) { // スペース以外の文字で始まって「.jpg」「.png」「.gif」「.psf」で終わる文字(大文字・小文字を区別しない[i]) var format = new RegExp('([^\s]+(\\.(jpg|png|gif|pdf))$)', 'i'); return format.test(name); }
特殊文字 意味 ^ 入力の先頭にマッチ $ 入力の末尾にマッチ \s スペース、タブ、改ページ、改行を含むホワイトスペース文字にマッチ
- 参考
ファイルのサイズが指定より大きかった場合はエラーにしたい
upload.js/** * ファイルサイズが正しいかを判定する. * @param {number} ファイルサイズ(バイト単位). * @return {Boolean} true:正しい. */ function isCorrectSize(size) { /** @type {number} 許容する最大サイズ(1MB). */ var maxSize = 1024 * 1024; return size <= maxSize; }
- 参考 : バイト換算 - 高精度計算サイト
エラーメッセージは親画面で指定したい
状況に合わせて使えるように思い付いた方法3つ
方法1. JavaScriptのwindow.openerで親画面から取得する
- エラーメッセージを親画面の隠し項目で設定しておく
- 子画面のJavaScript処理で
window.opener
を使って取得する親画面...省略... <h:inputHidden id="extErrMessage" value="拡張子が対象外だよ。" /> <h:inputHidden id="sizeErrMessage" value="ファイルサイズが大きすぎるよ。" /> ...省略...upload.js...省略... if (!isCorrectExtension(file.name)) { errMessage += window.opener.$('#extErrMessage').text(); } if (!isCorrectSize(file.size)) { if (errMessage != '') { errMessage += '<br />'; } errMessage += window.opener.$('#sizeErrMessage').text(); } ...省略...方法2. 子画面を表示するときにパラメータでメッセージを渡す
- 親画面で子画面を表示するJavaSctiptを生成するときにエラーメッセージをGETのパラメータで設定する
- 子画面を開いたらパラメータを
f:viewParam
で受け取ってバッキングビーンに設定する- バッキングビーンのエラーメッセージをJSON形式で置いておく
- JavaScriptで
parseJSON
を使ってエラーメッセージを取得する親画面...省略... <input type="button" value="アップロード" onclick="#{uploadBean.onClick}" /> ...省略...UploadBean.java/** * onClick属性用に出力するJavaScriptコードを取得する. * @return JavaScriptコード. */ public String getOnClick() { StringBuilder builder = new StringBuilder(); builder.append("window.open('upload.jsf"); builder.append("?key="); builder.append(urlEncode("formId:file")); builder.append("&extErrMessage="); builder.append(urlEncode("拡張子が対象外だよ。")); builder.append("&sizeErrMessage="); builder.append(urlEncode("ファイルサイズが大きすぎるよ。")); builder.append("', '', 'width=500,height=100'); return false;"); return builder.toString(); } /** * orgをURLエンコードして返す. * @param org * @return */ private String urlEncode(String org) { try { return URLEncoder.encode(org, "utf-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } }upload.xml(子画面)...省略... <f:metadata> <ui:remove>GETのパラメータを受け取る</ui:remove> <f:viewParam name="key" value="#{uploadBean.key}"/> <f:viewParam name="extErrMessage" value="#{uploadBean.extErrMessage}" /> <f:viewParam name="sizeErrMessage" value="#{uploadBean.sizeErrMessage}" /> </f:metadata> <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <h:outputScript library="js" name="upload.js"/> <ui:remove>エラーメッセージをJSON形式で置いておく</ui:remove> <script id="errMessage" type="application/json"> {"ext" : "#{uploadBean.extErrMessage}", "size" : "#{uploadBean.sizeErrMessage}"} </script> ...省略...upload.js...省略... /** @type {array} headタグ内に置いておいたエラーメッセージ. */ var message = $.parseJSON($('#errMessage').html()); /** @type {object} 選択されたファイル. */ var file = inputFile.files[0]; if (!isCorrectExtension(file.name)) { errMessage += message.ext; } if (!isCorrectSize(file.size)) { if (errMessage != '') { errMessage += '<br />'; } errMessage += message.size; } ...省略...方法3. 親子画面で同じバッキングビーンを使う
- 親子画面で共通のバッキングビーンにエラーメッセージ取得処理を実装する
- あとは「方法2. 子画面を表示するときにパラメータでメッセージを渡す」の「バッキングビーンのエラーメッセージをJSON形式で置いておく」以降と同じ
UploadBean.java...省略... /** * 拡張しでエラーになった時のエラーメッセージを取得する. * @return エラーメッセージ. */ public String getExtErrMessage() { return "拡張子が対象外だよ。"; } /** * サイズでエラーになった時のエラーメッセージを取得する. * @return エラーメッセージ. */ public String getSizeErrMessage() { return "ファイルサイズが大きすぎるよ。"; } ...省略...upload.xml(子画面)<ui:remove>エラーメッセージをJSON形式で置いておく</ui:remove> <script id="errMessage" type="application/json"> {"ext" : "#{uploadBean.extErrMessage}", "size" : "#{uploadBean.sizeErrMessage}"} </script>実装全体
親画面<?xml version='1.0' encoding='UTF-8' ?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core"> <ui:composition template="template.xhtml"> <ui:define name="js"> <h:outputScript library="js" name="upload.js"/> </ui:define> <ui:define name="content"> <h3>ファイルの入力チェックをしてみる</h3> <h:form id="formId"> <div id="uploadArea"> <ui:fragment rendered="#{!uploadBean.upload}"> <h:button value="アップロード" onclick="showPopup();"/> <h:inputText id="file" style="display:none;"> <f:ajax event="change" execute="@form" render="@form" listener="#{uploadBean.uploadFile}" /> </h:inputText> </ui:fragment> <ui:fragment rendered="#{uploadBean.upload}"> <h:outputText value="#{uploadBean.file.name}" /> <h:commandButton value="削除"> <f:ajax execute="@form" render="@form" listener="#{uploadBean.deleteFile}" /> </h:commandButton> </ui:fragment> <div><h:message for="uploadArea" errorClass="error" warnClass="warn" infoClass="info" /></div> </div> </h:form> </ui:define> </ui:composition> </html>upload.xhtml(子画面)<?xml version='1.0' encoding='UTF-8' ?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core"> <h:head> <title>アップロードするファイル</title> <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <h:outputScript library="js" name="upload.js"/> <ui:remove>エラーメッセージをJSON形式で置いておく</ui:remove> <script id="errMessage" type="application/json"> {"ext" : "#{uploadBean.extErrMessage}", "size" : "#{uploadBean.sizeErrMessage}"} </script> </h:head> <body> <div> <h:inputFile id="inputFile" onchange="checkFile(this)" value="uploadBean.file" /> </div> <div> <h:button value="OK" onclick="submit('#{uploadBean.key}');" /> <h:button value="閉じる" onclick="window.close();" /> </div> </body> </html>upload.js/** ポップアップ画面を表示する. */ function showPopup() { window.open('upload.jsf', '', 'width=500,height=100'); } /** * アップロードされたファイルをチェックする. * @param {Object} Fileオブジェクト. */ function checkFile(inputFile) { // エラーメッセージを削除する. $('.errMessage').remove(); /** @type {String} 表示するエラーメッセージ. */ var errMessage = ''; if (inputFile.files && inputFile.files[0]) { /** @type {array} headタグ内に置いておいたエラーメッセージ. */ var message = $.parseJSON($('#errMessage').html()); /** @type {object} 選択されたファイル. */ var file = inputFile.files[0]; if (!isCorrectExtension(file.name)) { errMessage += message.ext; } if (!isCorrectSize(file.size)) { if (errMessage != '') { errMessage += '<br />'; } errMessage += message.size; } } if (errMessage != '') { // エラーメッセージを追加する. $('#inputFile').after('<br /><span class="errMessage" style="color: red;">' + errMessage + '</span>'); // ファイルを削除する. inputFile.value = null; } } /** * 拡張子が正しいか判定する. * @param {string} ファイル名. * @return {Boolean} true:正しい. */ function isCorrectExtension(name) { var format = new RegExp('([^\s]+(\\.(jpg|png|gif|pdf))$)', 'i'); return format.test(name); } /** * ファイルサイズが正しいかを判定する. * @param {number} ファイルサイズ(バイト単位). * @return {Boolean} true:正しい. */ function isCorrectSize(size) { /** @type {number} 許容する最大サイズ(1MB). */ var maxSize = 1024 * 1024; return size <= maxSize; } /** * 親画面の要素を更新して画面を閉じる. * @param {string} key 更新する親画面要素のid. */ function submit(key) { window.opener.$('#'+key.replace(/:/g,"\\:")).change(); window.close(); }UploadBean.javapackage brans; import java.io.IOException; import java.io.Serializable; import javax.faces.view.ViewScoped; import javax.inject.Named; import javax.servlet.http.Part; import lombok.Data; @Named @ViewScoped @Data public class UploadBean implements Serializable { /** serialVersionUID. */ private static final long serialVersionUID = -355651229394801584L; /** ファイルデータ. */ private Part file; /** * ファイルがアップロードされているかを判定する. * @return true:アップロードされている. */ public boolean isUpload() { return this.file != null; } /** * 拡張しでエラーになった時のエラーメッセージを取得する. * @return エラーメッセージ. */ public String getExtErrMessage() { return "拡張子が対象外だよ。"; } /** * サイズでエラーになった時のエラーメッセージを取得する. * @return エラーメッセージ. */ public String getSizeErrMessage() { return "ファイルサイズが大きすぎるよ。"; } public String getKey() { return "formId:file"; } public void uploadFile() throws IOException { if (!isUpload()) { return; } if (!isCorrectExtension(this.file.getName())) { deleteFile(); } if (!isCorrectSize(this.file.getSize())) { deleteFile(); } } /** * アップロードしたファイルを削除する. * @throws IOException エラーが起きた. */ public void deleteFile() throws IOException { this.file.delete(); } /** * 拡張子が正しいか判定する. * @param name ファイル名. * @return true:正しい. */ private boolean isCorrectExtension(String name) { if (name != null) { return name.matches("([^\\s]+(\\.(?i)(jpg|png|gif|pdf))$)"); } return true; } /** * ファイルサイズが正しいかを判定する. * @param size ファイルサイズ(バイト). * @return true:正しい. */ private boolean isCorrectSize(long size) { long maxSize = 1024 * 1024; return size <= maxSize; } }
- 投稿日:2020-08-10T16:23:58+09:00
Mockitoを使ったJavaの単体テスト
Mockitoを使ったJavaの単体テスト
目的
本投稿ではMockito,JUnitを使用した単体テストの実施方法について解説します.
なお,サンプルはSpring Bootで作成し,コンストラクタインジェクションを使用しています.
そのため,@Mock
,@InjectMock
などのアノテーションは出てきません.Mockitoとは
Mockitoは、MITライセンスの下でリリースされたJava用のオープンソーステストフレームワーク.(by Wikipedia)
モック伊藤と読みます.モキートと読みます.
これを使うことでテスト用のモックオブジェクトを簡単に生成することができます.モックを使うと何が嬉しいの?
以下のようなテストはテストを書くことが厄介です.
- DBの値によって動作を分岐させる → テストでDBを書き換えるの?テスト用のDBはどうやって準備するの?毎回同じ値で初期化されてる?
- 意図的にエラーを発生させる → ディスクフルが発生した時のテストってどうやるの?
- 日付に関するテスト → システム時刻を変更するの?
テストしたいクラスが依存するクラスと同じように振る舞うクラス(モック)に差し替えることでこれらの解決します.
メリットとして以下が挙げられます.
- テスト対象が限定的になりテストしやすい
- 実際にDBにアクセスしないので爆速でテストの実行が完了する
- 意図的に例外を発生させることができる
- 依存クラスの実装が完了していなくてもテストできる
材料
- Java
- JUnit5
- Mockito
- Gradle or Maven (今回はGradleを利用)
- Spring Boot
インストール
plugins { id 'org.springframework.boot' version '2.3.2.RELEASE' id 'io.spring.dependency-management' version '1.0.9.RELEASE' id 'java' } group = 'com.mockito' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' } test { useJUnitPlatform() }MockitoはSpring Boot sterter testに含まれていますので,Spring Bootのデフォルトから特別追加する必要はありません.
Lombokだけ使いたかったので追加しています.テスト対象
概要
商品の合計額を計算するPOSシステムを例として取り扱います.
商品の種類ごとに設定された税率を加味して合計金額を算出します.
税率は今後変更される可能性もあるので,DBから取得するものとします.※ この章はUnitテストで意味のわからないメソッドが出てきてから読んでも問題ありません.
商品オブジェクトの定義package com.example.mockito; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; /** * 商品の区分 * * @author rcftdbeu * */ enum ItemType { Food, Other } /** * 商品 * * @author rcftdbeu * */ @Getter @Setter @AllArgsConstructor public class Item { /** 名称 */ String name; /** 価格 */ int price; /** タイプ */ ItemType itemType; }合計額を計算するサービスpackage com.example.mockito; import java.util.List; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class POS { private final TaxRateMaster taxRateMaster; /** * 商品の合計額を計算する * * @param itemList * @return */ public int culculateTotal(List<Item> itemList) { // 合計金額 int sum = 0; // 商品を1点ずつ税率計算して加算 for (Item item : itemList) { sum += item.getPrice() * (1 + taxRateMaster.getTaxRate(item.getItemType())); } return sum; } }モック対象のDBアクセスが含まれるクラスpackage com.example.mockito; import java.text.SimpleDateFormat; import java.util.Date; import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; /** * 税率を取得する * * @author rcftdbeu * */ @RequiredArgsConstructor @Component public class TaxRateMaster { private final TaxRepository ripository; /** * 本日日付 * * @return */ public Date getSysDate() { return new Date(); } /** * 軽減税率適用日以降か * * @return */ private boolean isAfterApplyDate() { String strDate = "2019/10/1 00:00:00"; SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); try { Date applyDate = dateFormat.parse(strDate); return getSysDate().after(applyDate); } catch (Exception e) { } return false; } /** * 税率を取得する * * @param type * @return */ public Double getTaxRate(ItemType type) { // 軽減税率適用後 if (isAfterApplyDate()) { if (type == ItemType.Food) { return ripository.getFoodTaxRate(); } else { return ripository.getOtherTaxRate(); } } // 軽減税率適用前 return ripository.getOldTaxRate(); } }リポジトリpackage com.example.mockito; import org.springframework.stereotype.Repository; /** * DBから税率を取得する処理<br> * DBは使用しないので未実装<br> * コンパイルエラーを回避するためだけに存在 * * @author rcftdbeu * */ @Repository public class TaxRepository { public Double getFoodTaxRate() { return null; } public Double getOtherTaxRate() { return null; } public Double getOldTaxRate() { return null; } }Unit Test
やっと本題です.
Mockitoで利用できるものに
mock
とspy
の2種類があります.mock : 対象のクラスの全てのメソッドが
return null
で置き換えられたモック.初期化も不要.
spy : 対象のクラスと同じオブジェクトで初期化が必要.必要に応じて特定のメソッドだけ変更できる.Mock
mockを利用したテストを実施していきます.
package com.example.mockito; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.Matchers.*; import static org.mockito.Mockito.*; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; public class MockTest { private static POS pos; private static TaxRateMaster master; private static TaxRepository repository; @Nested static class Mockを利用したテスト { // テストの前に1度だけ実行 @BeforeAll static void init() { master = mock(TaxRateMaster.class); pos = new POS(master); } // 各テストの前に実行 @BeforeEach void setup() { // 食料品の税率は8% when(master.getTaxRate(ItemType.Food)).thenReturn(0.08); // その他の税率は10% when(master.getTaxRate(ItemType.Other)).thenReturn(0.10); } @Test void 食料品の軽減税率計算_8パーセントで計算() { // 100円のおにぎりを追加 List<Item> itemList = new ArrayList<Item>(); Item onigiri = new Item("おにぎり", 100, ItemType.Food); itemList.add(onigiri); // 計算 int sum = pos.culculateTotal(itemList); // 結果確認 assertThat(sum, is(108)); } @Test void その他の軽減税率計算_10パーセントで計算() { // 500円の雑誌を追加 List<Item> itemList = new ArrayList<Item>(); Item onigiri = new Item("雑誌", 500, ItemType.Other); itemList.add(onigiri); // 計算 int sum = pos.culculateTotal(itemList); // 結果確認 assertThat(sum, is(550)); } } }init()
モックを初期化しています.
POSが依存しているTaxRateMasterをモックに差し替えています.
構造としては以下のようになっています.
POSの実体 ー TaxRateMasterのモックsetup()
モックの動作を定義しています.
when()
でどのメソッドが呼ばれたに,thenReturn()
で何を返すかを定義します.特定の引数で実行された時の動作を定義する場合は,その値を直接指定します.
任意の引数で実行された時の動作を定義する場合は,anyString()やany(ItemType.class)などを指定します.記法について
when().thenReturn()
で記述する方法とdoReturn().when()
で記述する方法の2種類があります.
setupの例を書き換えるとdoReturn(0.08).when(master).getTaxRate(ItemType.Food);
となります.どちらが良いかは議論が分かれるところです.
when().thenReturn()派
- ⭕️ if文の同じ順序で読みやすい
- ❌ spyで利用できない場合がある
- ❌ voidを返すメソッドでthenThrowが利用できない
→ 正しく設計していればdoReturnを使う機会はない.doReturn().when()派
- ⭕️ 気にせずいつでも利用できる
- ❌ doReturn()に記載する値がObject型になりコンパイラでエラーを検出できない
→ 微妙な違いを覚えるくらいならdoReturnで統一した方が楽だし,型チェックは実行すればわかる.個人的には読みやすいwhen().thenReturn()が好きなので,本投稿でもこちらの記法としています.
テストの実行
特別なことはなく,テスト用のItemインスタンスを作成し,テスト対象のculculateTotalを実行するだけです.
Spy
@Nested static class Spyを利用したテスト { // テストの前に1度だけ実行 @BeforeAll static void init() { repository = mock(TaxRepository.class); master = spy(new TaxRateMaster(repository)); pos = new POS(master); } // 各テストの前に実行 @BeforeEach void setup() { // 軽減税率適用以前の税率は8% when(repository.getOldTaxRate()).thenReturn(0.08); // 食料品の税率は8% when(repository.getFoodTaxRate()).thenReturn(0.08); // その他の税率は10% when(repository.getOtherTaxRate()).thenReturn(0.10); } @Nested class 軽減税率適用前 { @BeforeEach void setup() throws ParseException { // 現在の日付を取得したときに軽減税率適用前の日を返すように設定する String strDate = "2019/9/30"; SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd"); Date applyDate = dateFormat.parse(strDate); when(master.getSysDate()).thenReturn(applyDate); } @Test void 食料品の軽減税率計算_8パーセントで計算() { // 100円のおにぎりを追加 List<Item> itemList = new ArrayList<Item>(); Item onigiri = new Item("おにぎり", 100, ItemType.Food); itemList.add(onigiri); // 計算 int sum = pos.culculateTotal(itemList); // 結果確認 assertThat(sum, is(108)); } @Test void その他の軽減税率計算_8パーセントで計算() { // 500円の雑誌を追加 List<Item> itemList = new ArrayList<Item>(); Item onigiri = new Item("雑誌", 500, ItemType.Other); itemList.add(onigiri); // 計算 int sum = pos.culculateTotal(itemList); // 結果確認 assertThat(sum, is(540)); } } @Nested class 軽減税率適用後 { @BeforeEach void setup() throws ParseException { // 現在の日付を取得したときに軽減税率適用前の日を返すように設定する String strDate = "2019/10/1 00:00:01"; SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); Date applyDate = dateFormat.parse(strDate); when(master.getSysDate()).thenReturn(applyDate); } @Test void 食料品の軽減税率計算_8パーセントで計算() { // 100円のおにぎりを追加 List<Item> itemList = new ArrayList<Item>(); Item onigiri = new Item("おにぎり", 100, ItemType.Food); itemList.add(onigiri); // 計算 int sum = pos.culculateTotal(itemList); // 結果確認 assertThat(sum, is(108)); } @Test void その他の軽減税率計算_10パーセントで計算() { // 500円の雑誌を追加 List<Item> itemList = new ArrayList<Item>(); Item onigiri = new Item("雑誌", 500, ItemType.Other); itemList.add(onigiri); // 計算 int sum = pos.culculateTotal(itemList); // 結果確認 assertThat(sum, is(550)); } } }init()
Spyを初期化しています.
POSが依存しているTaxRateMasterをSpyに設定し,
構造としては以下のようになっています.
POS実体 ー TaxRateMasterのSpy ー TaxRepositoryのモックsetup()
TaxRateMasterのSpyで現在の日付を取得する部分だけ定義した日付に置き換えています.
テストの実行
mockの時と同じで,特に解説する箇所はありません.
その他いろいろな使い方
1度目と2度目で返す値を変更する
when(master.getTaxRate(ItemType.Food)).thenReturn(0.08) .thenReturn(0.10);
thenReturn
をつなげて書くことで,1回目,2回目の呼び出しで異なる値を返すことができます.
1回目は登録,2回目は更新などの処理の時に利用できます.メソッドが呼び出されたことを検証する
verify(master).getTaxRate(ItemType.Food);
verify
を使うことでモックのメソッドが呼び出されたことを検証できます.
また,メソッド実行時の引数が,指定の引数で実行されているかも検証できます.メソッドの呼び出し回数をクリアする
@BeforeEach void setup() { clearInvocations(master); }モックのインスタンスは1つで,複数のテストから実行されると
verify
で検証する時に他のテストでの呼び出し回数もカウントしてしまうので,テスト実行前にclearInvocations
でモックの呼び出し回数をリセットする必要があります.メソッドが複数回呼び出されたことを検証する
verify(master, times(2)).getTaxRate(ItemType.Food);
verify
の引数にtimes
を追加することで指定した回数だけメソッドが呼び出されたことを検証できます.メソッドが呼び出されていないことを検証する
verify(master, never()).getTaxRate(ItemType.Food);
verify
の引数にnever
を追加することでメソッドが呼び出されていないことを検証できます.
times(0)
でも同じです.引数を検証する
検証したいメソッドの引数がStringやBooleanなどのプリミティブ型であれば,verifyの後のメソッドの引数で検証できましたが,その他のオブジェクトの場合は,
ArgumentCaptorを利用することで,引数を取得することができます.@Test void 引数を取得() { // 引数を取得するためのArgumentCaptorを生成 ArgumentCaptor<ItemType> argCaptor = ArgumentCaptor.forClass(ItemType.class); // 100円のおにぎりを追加 List<Item> itemList = new ArrayList<Item>(); itemList.add(new Item("おにぎり", 100, ItemType.Food)); // 計算実行 pos.culculateTotal(itemList); // 引数を取得する verify(master).getTaxRate(argCaptor.capture()); ItemType executedItemType = argCaptor.getValue(); // 実行時の引数を検証 assertThat(executedItemType, is(ItemType.Food)); }例外発生時の動作を検証する
@Test void 例外発生() { when(master.getTaxRate(ItemType.Food)).thenThrow(new RuntimeException("DB接続失敗")); // 100円のおにぎりを追加 List<Item> itemList = new ArrayList<Item>(); itemList.add(new Item("おにぎり", 100, ItemType.Food)); // 計算 assertThrows(RuntimeException.class, () -> pos.culculateTotal(itemList)); // thenThrowの設定をクリア reset(master); }
thenReturn
の変わりにthenThrow
を設定することで例外を発生するさせることができます.
例外は該当のメソッドがThrowする例外だけ設定することができます.今回のメソッドでは特に例外を設定していないのでRuntimeException
を投げています.
また,他のテストでは例外を投げて欲しくないので,reset
でクリアしています.その他テスト関連
staticメソッドをmockにしたいのですが
デフォルトではmock化できません.
staticメソッドはmockと相性が悪く,可能であれば非staticにする方法をご検討ください.
Power mockを利用するとmock化できるようですが,JUnit5での動かし方が分かりませんでした.privateメソッドをテストしたいのですが
こちらもmock化できません.
privateメソッドはpublicメソッドを通じてテストできるはずですので,publicメソッドのテストを実装しましょう.
どうしてもという場合は,Power mockを利用する,または,リフレクションなどの黒魔術に手を出すことになります.
- 投稿日:2020-08-10T15:45:53+09:00
h:buttonタグのonclick属性にEL式を書いて失敗した
- 環境
- CentOS Linux release 7.8.2003 (Core)
- Eclipse IDE for Enterprise Java Developers.Version: 2020-03 (4.15.0)
- openjdk version "11.0.7" 2020-04-14 LTS
- JSF 2.3.9
事象 : ページを表示したらHTTPSステータス500になった
Eclipseのコンソールのログ2020-08-10T15:17:17.299+0900|警告: StandardWrapperValve[Faces Servlet]: Servlet.service() for servlet Faces Servlet threw exception javax.faces.application.ViewExpiredException: viewId:/base.jsf - ビュー /base.jsf を復元できませんでした。 at com.sun.faces.lifecycle.RestoreViewPhase.execute(RestoreViewPhase.java:194) at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:76) at com.sun.faces.lifecycle.RestoreViewPhase.doPhase(RestoreViewPhase.java:109) at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:177) at javax.faces.webapp.FacesServlet.executeLifecyle(FacesServlet.java:707) ...省略...原因 : h:buttonタグのonclick属性にEL式を書いているから
base.xml...省略... <h:button value="アップロード" onclick="#{uploadBean.onClick}"/> ...省略...UploadBean.java...省略... /** * onClick属性用に出力するJavaScriptコードを取得する. * @return JavaScriptコード. */ public String getOnClick() { StringBuilder builder = new StringBuilder(); builder.append("window.open('upload.jsf"); // ...省略... return builder.toString(); } ...省略...EL式を書くとうまくいかない理由がよくわからない・・・・ドキュメントに書いてあることが何を言いたのかよくわからないからだろうか?
誰かわかりやすく説明してほしい・・・。The entire target URL string must be processed by a call to the encodeResourceURL() method of the ExternalContext. The name of the UIParameter goes on the left hand side, and the value of the UIParameter on the right hand side. The name and the value must be URLEncoded. Each UIParameter instance is separeted by an ampersand, as dictated in the URL spec. The final encoded result will be written out to the onclick attribute of the button as "window.location.href = ''". If the developer has specified a custom onlclick the window.location.href name/value pair will be appended at the end of the developer specified script.
button(JSF 2.1 View Declaration Language: Facelets Variant)(ざっくり訳)
ターゲットURLは、ExternalContextのencodeResourceURLメソッドへの呼び出しによって処理される必要があります。
UIParameterの名前は左側に表示され、UIParameterの値は右側に表示されます。名前と値はURLEEncodedでなければなりません。各UIParameterインスタンスは、URL仕様の指示に従ってアンパサンドで区切られます。
エンコードされた最終結果は、ボタンのonclick属性に"window.location.href = ''"として書き出されます。
開発者がonlclickを指定した場合は、window.location.hrefの名前と値のペアがスクリプトの最後に追加されます。対応 : inputタグのtype="button"に変える
base.xml...省略... <input type="button" value="アップロード" onclick="#{uploadBean.onClick}" /> ...省略...疑問 : h:buttonタグのonclick属性にEL式を書いてうまくいくこともある
upload.xml<h:button value="OK" onclick="submit('#{uploadBean.key}');" />UploadBean.java...省略... public String getKey() { return "formId:file"; } ...省略...出力<input type="button" onclick="submit('formId:file');window.location.href='/tryJsf/upload.jsf'; return false;" value="OK">
- 投稿日:2020-08-10T14:49:51+09:00
【新人向け】JUnitの紹介
これは、前々から新人教育用にJUnitを紹介したいなぁと思っていたのですが、草稿を書いていたらちょっとしたボリュームになってしまったために断念した残念な記事です。このまま捨てるのももったいないので会社も夏休みですし、新人向けに。
JUnitとは
JUnitは一般的にユニットテストと呼ばれているところで使われるツールです。
JUnitは使い方と検証を書いたメソッドを持ったクラスを作っておくことで、そのメソッドを好きなタイミングで実行出来るソフトです。
JUnitを実行する際はJUnitの流儀に従ったクラスやメソッドの書き方をします。JUnitの流れ
JUnitの流れ(概論)
JUnitの基本的な流れは
- 「その機能の使い方」を基に実行
- 「その機能を使った結果」を検証
になります。
JUnitの流れ(簡単なサンプル)
例えば、買い物かごクラス(
Cart
)があったとします。この中に、買い物かごの中に入っている商品全ての価格と個数を反映した合計金額を取得するgetSum()
というメソッドがあったとします。
使い方はこんな感じです。Cart cart = new Cart(); cart.add(商品, 個数); int result = cart.getSum(); // cartの中の現在の合計金額が取得できる assertEquals(なっているはずの金額, result); // 取得した金額が想定した金額かどうか確認
assertEquals
が検証の部分になります。
ここで引数同士が違っていればテスト処理はそこで終了します。
テストもfailed
となります。assertEquals
をパスし、最後までいくとテストはsuccessful
になります。なお、
assertEquals
は複数書くことができます。例えば、コンストラクタのテストで複数のパラメータの設定をして正しく設定できたか確認などする場合は次のようになります。Product apple = new Product("リンゴ", 400); // appleという変数にリンゴという商品名と400円を設定している assertEquals("リンゴ", apple.getName()); // 名前が正しく設定出来たか確認 assertEquals(400, apple.getPrice()); // 価格が正しく設定できたか確認みたいな感じです。2つの
assertEquals
をパスすればテストは成功となります。JUnitでのテスト
テスト対象のクラス
下記のクラスはテスト対象となるクラスです。
Product.javapublic class Product { private String productId; // 商品番号 private String productName; // 商品名 private Integer price; // 価格 // getter public String getProductId() { return productId; } public String getProductName() { return productName; } public Integer getPrice() { return price; } // コンストラクタ public Product(String 商品番号, String 商品名, Integer 価格) { /* 上記のプロパティを設定 */ } }Cart.Javapublic class Cart { // プロパティ private List<Product> list; // 入っている商品とその個数 // メソッド /** * 商品を追加する処理を行う */ public void add(Product 商品) { /* 略 */ } /** * 入っている商品とその個数を返す処理 */ public List<Product> getList() { /* 略 */ } /** * 入っている商品の合計を返す処理 */ public int getSum() { /* 略 */ } }
ちなみに全体像は次の通りになります。
Product.javapublic class Product { private String productId; // 商品番号 private String productName; // 商品名 private Integer price; // 価格 public Product(String productId, String productName, Integer price) { this.productId = productId; this.productName = productName; this.price = price; } public String getProductId() { return productId; } public String getProductName() { return productName; } public Integer getPrice() { return price; } }Cart.javaimport java.util.List; import java.util.ArrayList; public class Cart { // プロパティ private List<Product> list; // 入っている商品とその個数 public Cart() { this.list = new ArrayList<Product>(); } // メソッド /** * 商品を追加する処理を行う */ public void add(Product product) { this.list.add(product); } /** * 入っている商品とその個数を返す処理 */ public List<Product> getList() { return this.list; } /** * 入っている商品の合計を返す処理 */ public int getSum() { int sum = 0; for (Product p: this.list) { sum += p.getPrice(); } return sum; } }テストの方法(概要編)
ここではaddのテストをしたいと思います。その場合、次の流れを考えました
使い方
0. カートに入れる商品を作成する
1. カートを作成する
2. カートに商品を追加する上記の使い方でaddが正しいかどうかの検証は次のように考えました( 注:これは一例です。必ずしも同じようなテストで下記のようなテストを行う必要はありません )
0. カートから一覧を取り出す(これは前準備でテストに入りません)
1. カートに入っている一覧の個数が追加した個数と同じか確認する
2. カートに入っている商品が正しいものか順に確認するテストの方法(擬似コード編)
上記のテストコードは次のようなものでしょう
// 0. カートに入れる商品を作成する Product apple = new Product("A001", "リンゴ", 158); Product orange = new Product("A002", "オレンジ", 258); // 1. カートを作成する Cart cart = new Cart(); // カートに商品(リンゴとオレンジ)を追加する cart.add(apple); cart.add(orange); // ここから検証 // 0. カートから一覧を取り出す List<Product> list = cart.getList(); // カートに入っているリストを取得する // 1. カートに入っている一覧の個数が追加した個数(2個)と同じか確認する assertEquals(2, list.size()); // 2. カートに入っている商品が正しいものか順に確認する Product item; // 2.1 1つめの商品がappleで定義したものと同じか確認 item= list.get(0); // カートの1つ目の商品を取り出し assertEquals("A001", item.getProductId()); // 取り出した商品の商品IDの検証 assertEquals("リンゴ", item.getProductName()); // 取り出した商品の商品名の検証 assertEquals((Integer)158, item.getPrice()); // 取り出した商品の商品名の検証 // 2.1 2つめの商品がorangeで定義したものと同じか確認 item = list.get(1); // カートの2つ目の商品を取り出し検証を開始する assertEquals("A002", item.getProductId()); // 取り出した商品の商品IDの検証 assertEquals("オレンジ", item.getProductName()); // 取り出した商品の商品名の検証 assertEquals((Integer)258, item.getPrice()); // 取り出した商品の商品名の検証ちなみに、
"A001"
などで書いた部分はapple.getProductName()
としても問題ないです。テストの方法(擬似コード補足編)
最初に書きました通り、JUnitの流れは
- 「その機能の使い方」を基に実行
- 「その機能を使った結果」を検証
が基本です。その検証は
assertEquals
などのassert
で始まるメソットで検証されます。
そして、assertEqualsは両方の引数が同じでない場合、そこでテスト失敗(failure)となり、以降の処理は行われません。例えば
assertEquals("リンゴ", item.getProductName());
の"リンゴ"
が"りんご"
になっていれば、failureとなり、以降の
assertEquals((Integer)158, item.getPrice());
などは実施されません。そしてassertEqualsを全て抜けて最後まで処理が進むと成功(success)となります。
なお、
assertXXX(expect, actual)
は正しい値、テストする値の順 に書きましょう。JUnitでのテストの方法(Eclipse編)
Eclipse編としていますが、書いている元はpleiadesです。
ちなみにこれだけでで力尽きました・・・。もし気力があればいつかVSCodeなどは追加します。
EclipseでJUnitを行う場合、次のようにします。
先ほど確認または作成したtestフォルダを右クリックし、「新規」→「その他」を選択します。
新規の画面が表示されますので「Java」→「JUnit」→「JUnit テスト・ケース」を選択し、「次へ」を選択します。
もしくは新規の画面の上部のウィザードフィールドに「junit」と入力します。残ったものの中から「JUnit テスト・ケース」を選択し、「次へ」を選択します。次はJUnit3か、4か5によって変わります。新人教育でなんのしがらみもない場合は4か5を使いましょう。配属先が3という残念な状況ならば3を選択してください。なお、残念ながら5については4で包括させていただきます。ちょっと、時間が足りなかったので・・・
JUnit3のテストケースを作成する
JUnit3のテストケースを作成します
1.「新規 JUnit 3テスト」を選択します
2. 名前にテストケースを書くクラスの名前を入力します(赤線部)。「テスト対象Test」が良いと思われます。
3. パッケージ名(青線部)はテストのクラスの対象と同じパッケージ(テスト対象Javaプログラムの最初の方に書いてある「package XXX;」の部分)を指定します。
4. 「完了」をクリックします。JUnit3用のライブラリが最初はありませんので、追加するかどうか聞かれますので「次のアクションを実行」を選択し、「JUnit 3 ライブラリーをビルド・パスに追加」を選択し、OKを押します。
作成されたJUnitテストケースの実体は単なるJavaのプログラムファイルです。今回は次のように記述します。
CartTest.javapackage myStore; import junit.framework.TestCase; import java.util.List; public class CartTest extends TestCase { public void testカートに追加できたか確認する() { // テストに必要な投入する商品は先に作っておく Product apple = new Product("A001", "リンゴ", 158); Product orange = new Product("A002", "オレンジ", 258); //カートを作成 Cart cart = new Cart(); // リンゴとオレンジを追加する cart.add(apple); cart.add(orange); // ここから検証 List<Product> list = cart.getList(); // カートに入っているリストを取得する // カートの中身が2個か検証する assertEquals(2, list.size()); // カートの中身が正しいか確認する Product item; item= list.get(0); // カートの1つ目の商品を取り出し検証を開始する assertEquals("カートの1つ目の商品IDがA001であるかの検証", "A001", item.getProductId()); assertEquals("カートの1つ目の商品名がリンゴであるかの検証", "リンゴ", item.getProductName()); assertEquals("カートの1つ目の価格が158であるかの検証", (Integer)158, item.getPrice()); item = list.get(1); // カートの2つ目の商品を取り出し検証を開始する assertEquals("カートの1つ目の商品IDがA002であるかの検証", "A002", item.getProductId()); assertEquals("カートの1つ目の商品名がオレンジであるかの検証", "オレンジ", item.getProductName()); assertEquals("カートの1つ目の価格が258であるかの検証", (Integer)258, item.getPrice()); } }
JUnit3のお約束は次の通りです。
-import junit.framework.TestCase;
を書く
- テスト用のクラスはextends TestCaseでTestCaseクラスを継承する
- テスト用のメソッドの修飾子はpublic void
で名前はtest
で始めること以上のルールを守って作成します。テスト用のメソッドは
test
から始めれば何個でも作ることができますので、あとはいろんなパターンのテストを作っていってください。
また、メソッド名に日本語が入っていますが、これはJavaにとって問題はありません(仕様的に問題なし)。JUnitでは何のテストをしているか分かりやすいように日本語が使われることも多いです1。testNo1
と書くよりもこちらの方が分かりやすいのではないでしょうか。またassertEqualsの引数の1つ目に文字列が入っているのに気がついたと思いますが、これはコメントだと思ってください。これを残しておくとJUnitでテストしたときに、この第一引数がメッセージとして表示され、何を意図したテストで失敗したのかが分かります。
JUnit4のテストケースを作成する
JUnit4のテストケースを作成します
JUnit4のテストケースを作成します
作成されたJUnitテストケースの実体は単なるJavaのプログラムファイルです。今回は次のように記述します。
CartTest2.javapackage myStore; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import java.util.List; import org.junit.Test; public class CartTest { @Test public void カートに追加できたか確認する() { // テストに必要な投入する商品は先に作っておく Product apple = new Product("A001", "リンゴ", 158); Product orange = new Product("A002", "オレンジ", 258); //カートを作成 Cart cart = new Cart(); // リンゴとオレンジを追加する cart.add(apple); cart.add(orange); // ここから検証 List<Product> list = cart.getList(); // カートに入っているリストを取得する // カートの中身が2個か検証する assertEquals(2, list.size()); // カートの中身が正しいか確認する Product item; item= list.get(0); // カートの1つ目の商品を取り出し assertThat(item.getProductId(), is("A001")); // 取り出した商品の商品IDの検証 assertThat(item.getProductName(), is("リンゴ")); // 取り出した商品の商品名の検証 assertThat(item.getPrice(), is(158)); // 取り出した商品の商品名の検証 item = list.get(1); // カートの2つ目の商品を取り出し検証を開始する assertThat(item.getProductId(), is("A002")); // 取り出した商品の商品IDの検証 assertThat(item.getProductName(), is("オレンジ")); // 取り出した商品の商品名の検証 assertThat(item.getPrice(), is(258)); // 取り出した商品の商品名の検証 } }JUnit4のお約束は次の通りです。
- 次を最初に書く
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
- テスト用のメソッドの修飾子は
public void
- テスト用のメソッドの前に
@Test
を書くこと- 検証は
assertThat( 検証対象, is( 望む値 ) )
のように書くこと以上のルールを守って作成します。テスト用のメソッドは
@Test
を付ければ始めれば何個でも作ることができますので、あとはいろんなパターンのテストを作っていってください。
また、メソッド名に日本語が入っていますが、これはJavaにとって問題はありません(仕様的に問題なし)。JUnitでは何のテストをしているか分かりやすいように日本語が使われることも多いです1。testNo1
と書くよりもこちらの方が分かりやすいのではないでしょうか。またassertEqualsの引数の1つ目に文字列が入っているのに気がついたと思いますが、これはコメントだと思ってください。これを残しておくとJUnitでテストしたときに、この第一引数がメッセージとして表示され、何を意図したテストで失敗したのかが分かります。
蛇足ですがJUnit 3とほぼ同様のコードも書けます
CartTest.javapackage myStore; import static org.junit.Assert.*; import org.junit.Test; import java.util.List; public class CartTest { @Test public void カートに追加できたか確認する() { // テストに必要な投入する商品は先に作っておく Product apple = new Product("A001", "リンゴ", 158); Product orange = new Product("A002", "オレンジ", 258); //カートを作成 Cart cart = new Cart(); // リンゴとオレンジを追加する cart.add(apple); cart.add(orange); // ここから検証 List<Product> list = cart.getList(); // カートに入っているリストを取得する // カートの中身が2個か検証する assertEquals(2, list.size()); // カートの中身が正しいか確認する Product item; item= list.get(0); // カートの1つ目の商品を取り出し検証を開始する assertEquals("カートの1つ目の商品IDがA001であるかの検証", "A001", item.getProductId()); assertEquals("カートの1つ目の商品名がリンゴであるかの検証", "リンゴ", item.getProductName()); assertEquals("カートの1つ目の価格が158であるかの検証", (Integer)158, item.getPrice()); item = list.get(1); // カートの2つ目の商品を取り出し検証を開始する assertEquals("カートの2つ目の商品IDがA002であるかの検証", "A002", item.getProductId()); assertEquals("カートの2つ目の商品名がオレンジであるかの検証", "オレンジ", item.getProductName()); assertEquals("カートの2つ目の価格が 258であるかの検証", (Integer)258, item.getPrice()); } }
JUnit4の書き方について
序盤で
assertEquals
を使ってしまったため、ここで補講です。
JUnit4では次のような書き方ができるようになりました。これからはこれがメインになります。
assertThat( target, is("target") );
assertEquals
とほぼやっていることが変わらないのですが、左から右に読めるようになったという利点が大きいです。assert that target is "target"(targetは"target"であると検証する)って感じです。
他にもこんな感じで書けます。is
やnot
はMatcherと呼ばれるものです。
assertThat( target, is( not( "not target") ) )
JUnitの実行
作ったテストケースを実行します。「実行」メニュー→「実行」→「JUnitテスト」を選択します。
すると画面のどこかに下記のような画面が出てきます。
ここでmyStore.CartTestの左の三角形をクリックしてみましょう。下記のように何のテストをしたのかが分かります。なお、成功したテストは緑(Green)で表示され、失敗したテストは赤(Red)で表示されます。
ちなみに、ここで、テストプログラムの28行目の"A001"を"A003"に変更して再度実行してみましょう。先ほど同じように右クリックで実行しても良いですが、JUnitビューがある場合は「テストの再実行」(赤線部)を押すと再実行可能です。
なお、"A001"を"A003"にしたことでファイルが変更されましたので保存するか確認するダイアログが出てきますので問題なければ「OK」を押します。ファイル名の横のチェックを外すとそのファイルが保存前状態でのプログラムが実行されます。
すると、今度はアイコンの左下に群青色のバツが付きます。これが失敗の印です。
そして、失敗したテストをクリックすると、テストケースのどこで失敗したか表示されます。これをダブルクリックすると対象の行が表示されます。
また、障害トレースの
junit.framework.ComparisionFailure:
の後ろ側を見るとエラーの詳細に「カートの1つめの商品IDがA001であるかの確認があること」の記述が確認できると思います。
また「Expected: is」に正しい値、「but: was」に実際の値(実行の結果)が表示されます。
さて、ながったらしい話になりましたが以上になります。
コラム(蛇足) テストとは?品質とは?何をどう検査するか?
単なるポエムです
JUnitは実行と検証の二つが大事だと書きました。このうち、検証について触れておこうと思います。
ここは超ややこしいので読み飛ばしてもOKです。
検証は実行の結果の値が期待する結果と同じかどうかを確認することですが、「何を」検証するかについては意見の分かれるところです。明確な正解があるのかも知れませんが、私は知りません(@t_wada さん辺りは知ってそうですが・・・)。例えば、下記のようなクラスがあったとします。
SomeClass.javaclass SomeClass { private String somethingvalue = ""; public void setSomethingValue(String somethingvalue) { this.somethingvalue = somethingvalue; } }このクラスは将来の拡張のため、データを保持する機能だけしかありません。保持するデータ
somethingvalue
を設定するsetSomethingValue
があります。ただし、value
の値を取得するgetSomethingValue
はありません。このsetSomethingValue
のテストはどうするべきでしょう?
私的な回答としては「プロジェクトの指針や仕様書に拠る」です。
先に言っておくとprivateな変数(プロパティ)であろうと、Javaのリフレクションという機能を使えばvalueの値は分かります。 2
その上でプロジェクトや仕様書について
- プロジェクトや仕様書に「valueに設定する」と書かれるとテスト対象になる可能性が高いと思います
- 「privateな変数は確認しない」「クラスが公開しているプロパティやメソッドで確認する」という方針ならばテスト対象外となります(valueにアクセスする方法がリフレクション以外にないのですから)
私的には後者の視点を採用したいところですが、この場合、いささか厄介な問題があります。下記のような値を指定された回数分繰り返し表示するメソッドを追加した場合
public String repeatSpeak(int times) { String result = ""; for (int i=0; i < times; i++) { result += this.value; } return result; }この
repeatSpeak
は公開された(=誰かが使う前提の)メソッドですからテスト対象になります。このとき、value
によって値が変わります。なので、value
を変更するためにsetValue
を使うテストも書くことになります。
このように直接的ではなく、間接的にテストすることもあり得るのではないかと思います。こう言った「何を」「どのように」「誰が」と言った視点を元にプロジェクトで統一した視点を持ち、テストすることがチーム開発では必要ですので配属先などでどのようにテストするか聞きましょう。決まっていなければ開発の最初にチームのメンバーで統一した視点を共有できるようにしましょう。
と言っても、半角スペースが入っていればエラーになりますので、一言でテストが表せるような言葉を選びましょう ↩
リフレクションについては次などを参照 :Samurai blog - 【Java入門】リフレクションでメソッドの実行、フィールドの変更、ひしだま's 技術メモページ - リフレクション、 ↩
- 投稿日:2020-08-10T14:32:36+09:00
プロパティグラフに触れてみる
1. 初めに
- プロパティグラフに触れる機会があったので、備忘録として残す気持ちで書きます。
- グラフと聞くと、グラフ理論のようなアカデミックな数学を連想し易いですが、「グラフデータベース」を活用する場合はグラフ理論のような複雑な話はあまり必要ありません。
- ここでは「グラフデータベース」にフォーカスを当ててながら認めます。
2. グラフとは
グラフは一言で言うと、単なる頂点(Vertex)と辺(edge)の集合です。
グラフはエンティティをノードとして表し、エンティティが世界とどのように関連しているかを関係として表現します。
グラフのデータモデルは以下の3つがあります。プロパティグラフ
プロパティグラフの特徴として下記4つあります。
- プロパティグラフにはノードと関係が含まれている。
- ノードには、プロパティ(Key/Value)が含まれている。
- 関係には名前と方向(direction)があり、必ず開始ノードと終了ノードがあります。
- 関係にもプロパティを含めることができます。
例) 簡単なプロパティグラフ
ハイパーグラフ
ハイパーグラフは関係を任意の数のノードに接続できる汎用的なグラフモデルです。
前述のプロパティグラフでは関係は1つの開始ノードから終了ノードしか持てないが、ハイパーグラフでは任意の数のノードに接続できます。例) アリスとボブが車を3台所有していることを表現するハイパーグラフ
トリプル
トリプルは「主語- 述語- 目的語」というデータ構造になります。
トリプルはRDF(Resource Description Framework)のメタデータモデルに当たります。
逆に言うと、RDFはリソース(定義されていないこと)をトリプルで表現します。例) 「Ora Lassilaは、資源(http://www.w3.org/Home/Lassila) の作者である」 という自然言語をトリプルで表現するRDF
- 文の構成
主語 リソース http://www.w3.org/Home/Lassila 述語 プロパティ Creator 目的語 プロパティ値 "Ora Lassila"
- RDF/XML表現
<rdf:RDF> <rdf:Description about="http://www.w3.org/Home/Lassila"> <s:Creator>Ora Lassila</s:Creator> </rdf:Description> </rdf:RDF>※今回はプロパティグラフのお話なので上記ハイパーグラフとトリプルの話は関連しておりません。
3. 環境(Parallel Graph Analytics)
PGX
今回はParallel Graph Analytics(通称:PGX)と呼ばれるグラフツールキットを用います。
PGXには、グラフクエリ言語、多様な分析機能やMachine Learningのサポートが含まれています。
下記の絵はPGXの全体概要です。
Oracle Cloud Infrastructure
PGXをどのように用いるのか、簡単なクラウドアーキテクチャは下記の通りです。
簡素化のために全てPublic Subnetベースで環境を整えます。※
インストール手順は書くのがめんどくさいので省きますが、中身は下記の通りです。
参考資料のDocumentを読めば特に問題なく構築できると思います。クライアントサーバ
- oracle-instantclient19.6-basic
- oracle-graph-client-20.3.0
- jdk-11.0.3
グラフサーバ
- oracle-graph-20.3.0
- jdk-1.8.0
- ※今回は特に使用しておりませんが、Tomcatへデプロイすることも可能です
DBサーバ
- 19c
- OPG_APIS.CREATE_PG()プロシージャでグラフを作成する
SQL> Execute OPG_APIS.CREATE_PG('Graph',4,8,'USERS'); SQL> select table_name from user_tables; TABLE_NAME -------------------- MYGRAPHGT$ MYGRAPHIT$ MYGRAPHSS$ MYGRAPHVT$ MYGRAPHGE$ SQL> desc MYGRAPHVT$ Name Null? Type ------------------ ---------- --------------------- VID NOT NULL NUMBER VL NVARCHAR2(3100) K NVARCHAR2(3100) T NUMBER(38) V NVARCHAR2(15000) VN NUMBER VT TIMESTAMP(6) WITH TIME ZONE SL NUMBER VTS DATE VTE DATE FE NVARCHAR2(4000)4. グラフクエリ(PGQL)を投げるところまで
RDBデータ
作成するプロパティグラフ
Googleで検索するとサンプルデータが豊富に出てきますが、それらを使ってもつまらないので、ショボいグラフではありますが自分で作ってみます。素直にRDB側のGRAPHVT$表とGRAPHGE$表へInsertします。
ノード作成
insert into GRAPHVT$ (VID,VL,T,K,V) values (1,'person',1,'name','Sato'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(1,'person',2,'age',40); insert into GRAPHVT$ (VID,VL,T,K,V) values(2,'person',1,'name','Suzuki'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(2,'person',2,'age',20); insert into GRAPHVT$ (VID,VL,T,K,V) values(3,'person',1,'name','Yamamoto'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(3,'person',2,'age',35); insert into GRAPHVT$ (VID,VL,T,K,V) values(4,'person',1,'name','Tanaka'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(4,'person',2,'age',25);エッジ作成
create sequence graph_eid_seq; alter sequence graph_eid_seq restart; insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,1,2,'knows','weight',3,0.5); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,1,4,'knows','weight',3,0.5); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,4,2,'likes','weight',3,0.8); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,4,3,'knows','weight',3,0.7); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,3,1,'knows','weight',3,0.9);Gremlin Shellによる接続
Jshellを用いた場合の接続方法を記します。
[oracle@cli bin] curl -X POST -H 'Content-Type: application/json' -d '{"username": "***", "password": "***"}' http://10.0.0.3:7007/auth/token ->正しくコマンドを打つと、Access Tokenが返ってきます [oracle@cli bin] ./oracle-graph-client-20.3.0/bin/opg-jshell --base_url http://10.51.0.3:7007 enter authentication token (press Enter for no token): <-Curlコマンドで取得したTokenをコピペします For an introduction type: /help intro Oracle Graph Client Shell 20.3.0 PGX server version: 20.1.1 type: SM PGX server API version: 3.8.1 PGQL version: 1.3 Variables instance, session, and analyst ready to use. opg-jshell> GraphConfig cfg = GraphConfigBuilder.forPropertyGraphRdbms() .setName("Graph") .addVertexProperty("name",PropertyType.STRING) .addVertexProperty("age",PropertyType.INTEGER) .addEdgeProperty("weight",PropertyType.FLOAT) .setLoadVertexLabels(true) .setLoadEdgeLabel(true).build(); <-扱うグラフを定義する opg-jshell> PgxGraph graph = session.readGraphWithProperties(cfg); <-RDBからOn-Memoryへグラフをロードする graph ==> PgxGraph[name=Graph,N=4,E=5,created=1596986537591] opg-jshell> graph.queryPgql("SELECT count(v) FROM Graph MATCH (v)").print(10).close(); <-PGQL(1) +----------+ | count(v) | +----------+ | 4 | +----------+ opg-jshell> opg-jshell> graph.queryPgql("SELECT id(n), label(n),n.name as name1,n.age as age1,label(e), e.weight, id(m),label(m),m.name as name2,m.age as age2 FROM MATCH (n) -[e]-> (m)").print(10).close(); <- PGQL(2) +---------------------------------------------------------------------------------------------+ | id(n) | label(n) | name1 | age1 | label(e) | weight | id(m) | label(m) | name2 | age2 | +---------------------------------------------------------------------------------------------+ | 3 | person | Yamamoto | 35 | knows | 0.9 | 1 | person | Sato | 40 | | 4 | person | Tanaka | 25 | knows | 0.7 | 3 | person | Yamamoto | 35 | | 4 | person | Tanaka | 25 | likes | 0.8 | 2 | person | Suzuki | 20 | | 1 | person | Sato | 40 | knows | 0.5 | 4 | person | Tanaka | 25 | | 1 | person | Sato | 40 | knows | 0.5 | 2 | person | Suzuki | 20 | +---------------------------------------------------------------------------------------------+ opg-jshell>Javaによる接続
Javaでの接続を記します。JshellでしかPGXを触れない場合はこちらは必要ありません。
ただ、Javaの方がInteractiveに実行せずに済むのでラクです。TokenConnect.javaimport oracle.pgx.api.*; import oracle.pgx.config.*; import oracle.pg.rdbms.*; import oracle.pgx.common.types.*; import java.util.function.Supplier; public class TokenConnect{ public static void main(Srting[] args) throws Exception{ /*引数にはURLとCurlで取得したTokenを指定する仕様にしてあります*/ String baseUrl = args[0]; String token = args[1]; ServerInstance instance = Pgx.setInstance(baseUrl,token); try (PgxSession session = instance.createSession("my-session")){ Supplier<GraphConfig> cfg = () ->{return GraphConfigBuilder.forPropertyGraphRdbms() .forPropertyGraphRdbms() .setName("Graph") .addVertexProperty("name",PropertyType.STRING) .addVertexProperty("age",PropertyType.INTEGER) .addEdgeProperty("weight",PropertyType.FLOAT) .setLoadVertexLabels(true) .setLoadEdgeLabel(true) .build(); PgxGraph graph = session.readGraphWithProperties(cfg.get()); System.out.println("N = " + graph.getNumVertices()+ "E = " + graph.getNumEdges()); } } }[oracle@cli oracle-graph-client-20.3.0] javac -cp 'lib/*' TokenConnect.java warning: Supported source version 'RELEASE_8' from annotation processor 'org.apache.tinkerpop.gremlin.process.traversal.dsl.GremlinDslProcessor' less than -source '11' 1 warning [oracle@cli oracle-graph-client-20.3.0] java -cp '.:conf:lib/*' TokenConnect *baseUrl *Token WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by com.google.inject.internal.cglib.core.$ReflectUtils$1 (file:/home/opc/oracle-graph-client-20.3.0/lib/guice-4.2.2.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) WARNING: Please consider reporting this to the maintainers of com.google.inject.internal.cglib.core.$ReflectUtils$1 WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release N = 4 <-> E = 5 +---------------------------------------------------------------------------------------------+ | id(n) | label(n) | name1 | age1 | label(e) | weight | id(m) | label(m) | name2 | age2 | +---------------------------------------------------------------------------------------------+ | 3 | person | Yamamoto | 35 | knows | 0.9 | 1 | person | Sato | 40 | | 4 | person | Tanaka | 25 | knows | 0.7 | 3 | person | Yamamoto | 35 | | 4 | person | Tanaka | 25 | likes | 0.8 | 2 | person | Suzuki | 20 | | 1 | person | Sato | 40 | knows | 0.5 | 4 | person | Tanaka | 25 | | 1 | person | Sato | 40 | knows | 0.5 | 2 | person | Suzuki | 20 | +---------------------------------------------------------------------------------------------+ [oracle@cli oracle-graph-client-20.3.0]しっかりとPGQLクエリの結果が返ってきました。
5. まとめ
- PGXを用いて自作グラフからクエリ検索しました。
- サーバをそれぞれ3層に分けてリモートで接続できることを確認できました。
- JshellとJavaの両方で使い分けできることも確認しました。
参考
- インストール手順の参考 -> PGXのDocument
- 投稿日:2020-08-10T14:32:36+09:00
Java/JShellを用いて爆速でプロパティグラフのクエリを叩き込む
1. 初めに
- プロパティグラフに触れる機会があったので、備忘録として残す気持ちで書きます。
- グラフと聞くと、グラフ理論のようなアカデミックな数学を連想し易いですが、「グラフデータベース」を活用する場合はグラフ理論のような複雑な話はあまり必要ありません。
- ここでは「グラフデータベース」にフォーカスを当ててながら認めます。
2. グラフとは
グラフは一言で言うと、単なる頂点(Vertex)と辺(edge)の集合です。
グラフはエンティティをノードとして表し、エンティティが世界とどのように関連しているかを関係として表現します。
グラフのデータモデルは以下の3つがあります。プロパティグラフ
プロパティグラフの特徴として下記4つあります。
- プロパティグラフにはノードと関係が含まれている。
- ノードには、プロパティ(Key/Value)が含まれている。
- 関係には名前と方向(direction)があり、必ず開始ノードと終了ノードがあります。
- 関係にもプロパティを含めることができます。
例) 簡単なプロパティグラフ
ハイパーグラフ
ハイパーグラフは関係を任意の数のノードに接続できる汎用的なグラフモデルです。
前述のプロパティグラフでは関係は1つの開始ノードから終了ノードしか持てないが、ハイパーグラフでは任意の数のノードに接続できます。例) アリスとボブが車を3台所有していることを表現するハイパーグラフ
トリプル
トリプルは「主語- 述語- 目的語」というデータ構造になります。
トリプルはRDF(Resource Description Framework)のメタデータモデルに当たります。
逆に言うと、RDFはリソース(定義されていないこと)をトリプルで表現します。例) 「Ora Lassilaは、資源(http://www.w3.org/Home/Lassila) の作者である」 という自然言語をトリプルで表現するRDF
- 文の構成
主語 リソース http://www.w3.org/Home/Lassila 述語 プロパティ Creator 目的語 プロパティ値 "Ora Lassila"
- RDF/XML表現
<rdf:RDF> <rdf:Description about="http://www.w3.org/Home/Lassila"> <s:Creator>Ora Lassila</s:Creator> </rdf:Description> </rdf:RDF>※今回はプロパティグラフのお話なので上記ハイパーグラフとトリプルの話は関連しておりません。
3. 環境(Parallel Graph Analytics)
PGX
今回はParallel Graph Analytics(通称:PGX)と呼ばれるグラフツールキットを用います。
PGXには、グラフクエリ言語、多様な分析機能やMachine Learningのサポートが含まれています。
下記の絵はPGXの全体概要です。
Oracle Cloud Infrastructure
PGXをどのように用いるのか、簡単なクラウドアーキテクチャは下記の通りです。
簡素化のために全てPublic Subnetベースで環境を整えます。※
インストール手順は書くのがめんどくさいので省きますが、中身は下記の通りです。
参考資料のDocumentを読めば特に問題なく構築できると思います。クライアントサーバ
- oracle-instantclient19.6-basic
- oracle-graph-client-20.3.0
- jdk-11.0.3
グラフサーバ
- oracle-graph-20.3.0
- jdk-1.8.0
- ※今回は特に使用しておりませんが、Tomcatへデプロイすることも可能です
DBサーバ
- 19c
- OPG_APIS.CREATE_PG()プロシージャでグラフを作成する
SQL> Execute OPG_APIS.CREATE_PG('Graph',4,8,'USERS'); SQL> select table_name from user_tables; TABLE_NAME -------------------- MYGRAPHGT$ MYGRAPHIT$ MYGRAPHSS$ MYGRAPHVT$ MYGRAPHGE$ SQL> desc MYGRAPHVT$ Name Null? Type ------------------ ---------- --------------------- VID NOT NULL NUMBER VL NVARCHAR2(3100) K NVARCHAR2(3100) T NUMBER(38) V NVARCHAR2(15000) VN NUMBER VT TIMESTAMP(6) WITH TIME ZONE SL NUMBER VTS DATE VTE DATE FE NVARCHAR2(4000)4. グラフクエリ(PGQL)を投げるところまで
RDBデータ
作成するプロパティグラフ
Googleで検索するとサンプルデータが豊富に出てきますが、それらを使ってもつまらないので、ショボいグラフではありますが自分で作ってみます。素直にRDB側のGRAPHVT$表とGRAPHGE$表へInsertします。
ノード作成
insert into GRAPHVT$ (VID,VL,T,K,V) values (1,'person',1,'name','Sato'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(1,'person',2,'age',40); insert into GRAPHVT$ (VID,VL,T,K,V) values(2,'person',1,'name','Suzuki'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(2,'person',2,'age',20); insert into GRAPHVT$ (VID,VL,T,K,V) values(3,'person',1,'name','Yamamoto'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(3,'person',2,'age',35); insert into GRAPHVT$ (VID,VL,T,K,V) values(4,'person',1,'name','Tanaka'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(4,'person',2,'age',25);エッジ作成
create sequence graph_eid_seq; alter sequence graph_eid_seq restart; insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,1,2,'knows','weight',3,0.5); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,1,4,'knows','weight',3,0.5); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,4,2,'likes','weight',3,0.8); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,4,3,'knows','weight',3,0.7); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,3,1,'knows','weight',3,0.9);JShellによる接続
Jshellを用いた場合の接続方法を記します。
[oracle@cli bin] curl -X POST -H 'Content-Type: application/json' -d '{"username": "***", "password": "***"}' http://10.0.0.3:7007/auth/token ->正しくコマンドを打つと、Access Tokenが返ってきます [oracle@cli bin] ./oracle-graph-client-20.3.0/bin/opg-jshell --base_url http://10.51.0.3:7007 enter authentication token (press Enter for no token): <-Curlコマンドで取得したTokenをコピペします For an introduction type: /help intro Oracle Graph Client Shell 20.3.0 PGX server version: 20.1.1 type: SM PGX server API version: 3.8.1 PGQL version: 1.3 Variables instance, session, and analyst ready to use. opg-jshell> GraphConfig cfg = GraphConfigBuilder.forPropertyGraphRdbms() .setName("Graph") .addVertexProperty("name",PropertyType.STRING) .addVertexProperty("age",PropertyType.INTEGER) .addEdgeProperty("weight",PropertyType.FLOAT) .setLoadVertexLabels(true) .setLoadEdgeLabel(true).build(); <-扱うグラフを定義する opg-jshell> PgxGraph graph = session.readGraphWithProperties(cfg); <-RDBからOn-Memoryへグラフをロードする graph ==> PgxGraph[name=Graph,N=4,E=5,created=1596986537591] opg-jshell> graph.queryPgql("SELECT count(v) FROM Graph MATCH (v)").print(10).close(); <-PGQL(1) +----------+ | count(v) | +----------+ | 4 | +----------+ opg-jshell> opg-jshell> graph.queryPgql("SELECT id(n), label(n),n.name as name1,n.age as age1,label(e), e.weight, id(m),label(m),m.name as name2,m.age as age2 FROM MATCH (n) -[e]-> (m)").print(10).close(); <- PGQL(2) +---------------------------------------------------------------------------------------------+ | id(n) | label(n) | name1 | age1 | label(e) | weight | id(m) | label(m) | name2 | age2 | +---------------------------------------------------------------------------------------------+ | 3 | person | Yamamoto | 35 | knows | 0.9 | 1 | person | Sato | 40 | | 4 | person | Tanaka | 25 | knows | 0.7 | 3 | person | Yamamoto | 35 | | 4 | person | Tanaka | 25 | likes | 0.8 | 2 | person | Suzuki | 20 | | 1 | person | Sato | 40 | knows | 0.5 | 4 | person | Tanaka | 25 | | 1 | person | Sato | 40 | knows | 0.5 | 2 | person | Suzuki | 20 | +---------------------------------------------------------------------------------------------+ opg-jshell>Javaによる接続
Javaでの接続を記します。JshellでしかPGXを触れない場合はこちらは必要ありません。
ただ、Javaの方がInteractiveに実行せずに済むのでラクです。TokenConnect.javaimport oracle.pgx.api.*; import oracle.pgx.config.*; import oracle.pg.rdbms.*; import oracle.pgx.common.types.*; import java.util.function.Supplier; public class TokenConnect{ public static void main(Srting[] args) throws Exception{ /*引数にはURLとCurlで取得したTokenを指定する仕様にしてあります*/ String baseUrl = args[0]; String token = args[1]; ServerInstance instance = Pgx.setInstance(baseUrl,token); try (PgxSession session = instance.createSession("my-session")){ Supplier<GraphConfig> cfg = () ->{return GraphConfigBuilder.forPropertyGraphRdbms() .forPropertyGraphRdbms() .setName("Graph") .addVertexProperty("name",PropertyType.STRING) .addVertexProperty("age",PropertyType.INTEGER) .addEdgeProperty("weight",PropertyType.FLOAT) .setLoadVertexLabels(true) .setLoadEdgeLabel(true) .build(); PgxGraph graph = session.readGraphWithProperties(cfg.get()); System.out.println("N = " + graph.getNumVertices()+ " <-> E = " + graph.getNumEdges()); } } }[oracle@cli oracle-graph-client-20.3.0] javac -cp 'lib/*' TokenConnect.java warning: Supported source version 'RELEASE_8' from annotation processor 'org.apache.tinkerpop.gremlin.process.traversal.dsl.GremlinDslProcessor' less than -source '11' 1 warning [oracle@cli oracle-graph-client-20.3.0] java -cp '.:conf:lib/*' TokenConnect *baseUrl *Token WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by com.google.inject.internal.cglib.core.$ReflectUtils$1 (file:/home/opc/oracle-graph-client-20.3.0/lib/guice-4.2.2.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) WARNING: Please consider reporting this to the maintainers of com.google.inject.internal.cglib.core.$ReflectUtils$1 WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release N = 4 <-> E = 5 +---------------------------------------------------------------------------------------------+ | id(n) | label(n) | name1 | age1 | label(e) | weight | id(m) | label(m) | name2 | age2 | +---------------------------------------------------------------------------------------------+ | 3 | person | Yamamoto | 35 | knows | 0.9 | 1 | person | Sato | 40 | | 4 | person | Tanaka | 25 | knows | 0.7 | 3 | person | Yamamoto | 35 | | 4 | person | Tanaka | 25 | likes | 0.8 | 2 | person | Suzuki | 20 | | 1 | person | Sato | 40 | knows | 0.5 | 4 | person | Tanaka | 25 | | 1 | person | Sato | 40 | knows | 0.5 | 2 | person | Suzuki | 20 | +---------------------------------------------------------------------------------------------+ [oracle@cli oracle-graph-client-20.3.0]しっかりとPGQLクエリの結果が返ってきました。
5. まとめ
- PGXを用いて自作グラフからクエリ検索しました。
- サーバをそれぞれ3層に分けてリモートで接続できることを確認できました。
- JshellとJavaの両方で使い分けできることも確認しました。
参考
- インストール手順の参考 -> PGXのDocument
- 投稿日:2020-08-10T14:32:36+09:00
Java+PGXでグラフクエリを爆速で叩く
1. 初めに
- プロパティグラフに触れる機会があったので、備忘録として残す気持ちで書きます。
- グラフと聞くと、グラフ理論のようなアカデミックな数学を連想し易いですが、「グラフデータベース」を活用する場合はグラフ理論のような複雑な話はあまり必要ありません。
- ここでは「グラフデータベース」にフォーカスを当ててながら認めます。
2. グラフとは
グラフは一言で言うと、単なる頂点(Vertex)と辺(edge)の集合です。
グラフはエンティティをノードとして表し、エンティティが世界とどのように関連しているかを関係として表現します。
グラフのデータモデルは以下の3つがあります。プロパティグラフ
プロパティグラフの特徴として下記4つあります。
- プロパティグラフにはノードと関係が含まれている。
- ノードには、プロパティ(Key/Value)が含まれている。
- 関係には名前と方向(direction)があり、必ず開始ノードと終了ノードがあります。
- 関係にもプロパティを含めることができます。
例) 簡単なプロパティグラフ
ハイパーグラフ
ハイパーグラフは関係を任意の数のノードに接続できる汎用的なグラフモデルです。
前述のプロパティグラフでは関係は1つの開始ノードから終了ノードしか持てないが、ハイパーグラフでは任意の数のノードに接続できます。例) アリスとボブが車を3台所有していることを表現するハイパーグラフ
トリプル
トリプルは「主語- 述語- 目的語」というデータ構造になります。
トリプルはRDF(Resource Description Framework)のメタデータモデルに当たります。
逆に言うと、RDFはリソース(定義されていないこと)をトリプルで表現します。例) 「Ora Lassilaは、資源(http://www.w3.org/Home/Lassila) の作者である」 という自然言語をトリプルで表現するRDF
- 文の構成
主語 リソース http://www.w3.org/Home/Lassila 述語 プロパティ Creator 目的語 プロパティ値 "Ora Lassila"
- RDF/XML表現
<rdf:RDF> <rdf:Description about="http://www.w3.org/Home/Lassila"> <s:Creator>Ora Lassila</s:Creator> </rdf:Description> </rdf:RDF>※今回はプロパティグラフのお話なので上記ハイパーグラフとトリプルの話は関連しておりません。
3. 環境(Parallel Graph Analytix)
PGX
今回はParallel Graph Analytix(通称:PGX)と呼ばれるグラフツールキットを用います。
PGXには、グラフクエリ言語、多様な分析機能やMachine Learningのサポートが含まれています。
下記の絵はPGXの全体概要です。
Oracle Cloud Infrastructure
PGXをどのように用いるのか、簡単なクラウドアーキテクチャは下記の通りです。
簡素化のために全てPublic Subnetベースで環境を整えます。中身は下記の通りです。
クライアントサーバ
- oracle-instantclient19.6-basic
- oracle-graph-client-20.3.0
- jdk-11.0.3
グラフサーバ
- oracle-graph-20.3.0
- jdk-1.8.0
- ※今回は特に使用しておりませんが、Tomcatへデプロイすることも可能です
DBサーバ
- Autonomous Database(19c) <- DBCSでも構いません
- OPG_APIS.CREATE_PG()プロシージャでグラフを作成する
SQL> Execute OPG_APIS.CREATE_PG('Graph',4,8,'USERS'); SQL> select table_name from user_tables; TABLE_NAME -------------------- GRAPHGT$ GRAPHIT$ GRAPHSS$ GRAPHVT$ GRAPHGE$ SQL> desc GRAPHVT$ Name Null? Type ------------------ ---------- --------------------- VID NOT NULL NUMBER VL NVARCHAR2(3100) K NVARCHAR2(3100) T NUMBER(38) V NVARCHAR2(15000) VN NUMBER VT TIMESTAMP(6) WITH TIME ZONE SL NUMBER VTS DATE VTE DATE FE NVARCHAR2(4000)4. グラフクエリ(PGQL)を投げるところまで
RDBデータ
作成するプロパティグラフ
Googleで検索するとサンプルデータが豊富に出てきますが、それらを使ってもつまらないので、ショボいグラフではありますが自分で作ってみます。素直にRDB側のGRAPHVT$表とGRAPHGE$表へInsertします。
ノード作成
insert into GRAPHVT$ (VID,VL,T,K,V) values (1,'person',1,'name','Sato'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(1,'person',2,'age',40); insert into GRAPHVT$ (VID,VL,T,K,V) values(2,'person',1,'name','Suzuki'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(2,'person',2,'age',20); insert into GRAPHVT$ (VID,VL,T,K,V) values(3,'person',1,'name','Yamamoto'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(3,'person',2,'age',35); insert into GRAPHVT$ (VID,VL,T,K,V) values(4,'person',1,'name','Tanaka'); insert into GRAPHVT$ (VID,VL,T,K,VN) values(4,'person',2,'age',25);エッジ作成
create sequence graph_eid_seq; alter sequence graph_eid_seq restart; insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,1,2,'knows','weight',3,0.5); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,1,4,'knows','weight',3,0.5); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,4,2,'likes','weight',3,0.8); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,4,3,'knows','weight',3,0.7); insert into GRAPHGE$ (EID,SVID,DVID,EL,K,T,VN) values(graph_eid_seq.nextval,3,1,'knows','weight',3,0.9);JShellによる接続
Jshellを用いた場合の接続方法を記します。
[oracle@cli bin] curl -X POST -H 'Content-Type: application/json' -d '{"username": "***", "password": "***"}' http://10.51.0.2:7007/auth/token ->正しくコマンドを打つと、Access Tokenが返ってきます [oracle@cli bin] ./oracle-graph-client-20.3.0/bin/opg-jshell --base_url http://10.51.0.2:7007 enter authentication token (press Enter for no token): <-Curlコマンドで取得したTokenをコピペします For an introduction type: /help intro Oracle Graph Client Shell 20.3.0 PGX server version: 20.1.1 type: SM PGX server API version: 3.8.1 PGQL version: 1.3 Variables instance, session, and analyst ready to use. opg-jshell> GraphConfig cfg = GraphConfigBuilder.forPropertyGraphRdbms() .setName("Graph") .addVertexProperty("name",PropertyType.STRING) .addVertexProperty("age",PropertyType.INTEGER) .addEdgeProperty("weight",PropertyType.FLOAT) .setLoadVertexLabels(true) .setLoadEdgeLabel(true).build(); <-扱うグラフを定義する opg-jshell> PgxGraph graph = session.readGraphWithProperties(cfg); <-RDBからOn-Memoryへグラフをロードする graph ==> PgxGraph[name=Graph,N=4,E=5,created=1596986537591] opg-jshell> graph.queryPgql("SELECT count(v) FROM Graph MATCH (v)").print(10).close(); <-PGQL(1) +----------+ | count(v) | +----------+ | 4 | +----------+ opg-jshell> opg-jshell> graph.queryPgql("SELECT id(n), label(n),n.name as name1,n.age as age1,label(e), e.weight, id(m),label(m),m.name as name2,m.age as age2 FROM MATCH (n) -[e]-> (m)").print(10).close(); <- PGQL(2) +---------------------------------------------------------------------------------------------+ | id(n) | label(n) | name1 | age1 | label(e) | weight | id(m) | label(m) | name2 | age2 | +---------------------------------------------------------------------------------------------+ | 3 | person | Yamamoto | 35 | knows | 0.9 | 1 | person | Sato | 40 | | 4 | person | Tanaka | 25 | knows | 0.7 | 3 | person | Yamamoto | 35 | | 4 | person | Tanaka | 25 | likes | 0.8 | 2 | person | Suzuki | 20 | | 1 | person | Sato | 40 | knows | 0.5 | 4 | person | Tanaka | 25 | | 1 | person | Sato | 40 | knows | 0.5 | 2 | person | Suzuki | 20 | +---------------------------------------------------------------------------------------------+ opg-jshell>Javaによる接続
Javaでの接続を記します。JshellでしかPGXを触れない場合はこちらは必要ありません。
ただ、Javaの方がInteractiveに実行せずに済むのでラクです。TokenConnect.javaimport oracle.pgx.api.*; import oracle.pgx.config.*; import oracle.pg.rdbms.*; import oracle.pgx.common.types.*; import java.util.function.Supplier; public class TokenConnect{ public static void main(Srting[] args) throws Exception{ /*引数にはURLとCurlで取得したTokenを指定する仕様にしてあります*/ String baseUrl = args[0]; String token = args[1]; ServerInstance instance = Pgx.setInstance(baseUrl,token); try (PgxSession session = instance.createSession("my-session")){ Supplier<GraphConfig> cfg = () ->{return GraphConfigBuilder.forPropertyGraphRdbms() .forPropertyGraphRdbms() .setName("Graph") .addVertexProperty("name",PropertyType.STRING) .addVertexProperty("age",PropertyType.INTEGER) .addEdgeProperty("weight",PropertyType.FLOAT) .setLoadVertexLabels(true) .setLoadEdgeLabel(true) .build(); PgxGraph graph = session.readGraphWithProperties(cfg.get()); System.out.println("N = " + graph.getNumVertices()+ " <-> E = " + graph.getNumEdges()); } } }[oracle@cli oracle-graph-client-20.3.0] javac -cp 'lib/*' TokenConnect.java warning: Supported source version 'RELEASE_8' from annotation processor 'org.apache.tinkerpop.gremlin.process.traversal.dsl.GremlinDslProcessor' less than -source '11' 1 warning [oracle@cli oracle-graph-client-20.3.0] java -cp '.:conf:lib/*' TokenConnect *baseUrl *Token WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by com.google.inject.internal.cglib.core.$ReflectUtils$1 (file:/home/opc/oracle-graph-client-20.3.0/lib/guice-4.2.2.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) WARNING: Please consider reporting this to the maintainers of com.google.inject.internal.cglib.core.$ReflectUtils$1 WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release N = 4 <-> E = 5 +---------------------------------------------------------------------------------------------+ | id(n) | label(n) | name1 | age1 | label(e) | weight | id(m) | label(m) | name2 | age2 | +---------------------------------------------------------------------------------------------+ | 3 | person | Yamamoto | 35 | knows | 0.9 | 1 | person | Sato | 40 | | 4 | person | Tanaka | 25 | knows | 0.7 | 3 | person | Yamamoto | 35 | | 4 | person | Tanaka | 25 | likes | 0.8 | 2 | person | Suzuki | 20 | | 1 | person | Sato | 40 | knows | 0.5 | 4 | person | Tanaka | 25 | | 1 | person | Sato | 40 | knows | 0.5 | 2 | person | Suzuki | 20 | +---------------------------------------------------------------------------------------------+ [oracle@cli oracle-graph-client-20.3.0]しっかりとPGQLクエリの結果が返ってきました。
5. まとめ
- PGXを用いて自作グラフからクエリ検索しました。
- サーバをそれぞれ3層に分けてリモートで接続できることを確認できました。
- JshellとJavaの両方で使い分けできることも確認しました。
参考
- インストール手順の参考 -> PGXのDocument
- 投稿日:2020-08-10T11:27:58+09:00
Java + H2 Database で主要な JDBC 型を取得するサンプルコード
概要
- H2 Database にてテーブルに主要な JDBC 型のカラムを定義して、Java のプログラムからカラムの値を取得するサンプルコードを示す
- 今回の動作確認環境: H2 Database 1.4.200 + Java 14 (AdoptOpenJDK 14.0.2) + Gradle 6.5.1 + macOS Catalina
JDBC 型とは
JDBC 型は SQL 型と Java 言語の型の間を取り持つ型。SQL のデータを表す型 (CHAR や INTEGER など) に近いもの。
SQL 型 ←(マッピング)→ JDBC 型 ←(マッピング)→ Java 言語型
JDBC API 入門 - SQL と Java の型のマッピング
異なるデータベース製品がサポートする SQL の型の間には、相当な相違があります。異なるデータベースが同一の意味を持つ SQL の型をサポートしている場合でも、それらの型に異なる名前を与えていることがあります。たとえば、主要データベースのほとんどが大きなバイナリ値に対する SQL の型をサポートしていますが、Oracle ではこの型を LONG RAW、Sybase では IMAGE、Informix では BYTE、DB2 では LONG VARCHAR FOR BIT DATA とそれぞれ呼んでいます。
JDBC プログラマは、通常は、ターゲットのデータベースが使用している実際の SQL の型名に気を使う必要はありません。多くの場合、JDBC プログラマは、既存のデータベースのテーブルに対してプログラミングをし、そうしたテーブルを作成した正確な SQL の型名に注意を払う必要はありません。
JDBC は、クラス java.sql.Types で総称 SQL の型識別子のセットを定義しています。そのセットの型は、もっとも一般的に使用される SQL の型を表すように設計されています。JDBC API によるプログラミングでは、プログラマは通常、ターゲットのデータベースが使用している正確な SQL の型名を意識することなく、そのセットの JDBC 型を使用して総称 SQL の型を参照することができます。
サンプルコード
ファイル一覧
├── build.gradle └── src └── main └── java └── JdbcSample.javabuild.gradle
plugins { id 'application' id 'java' } sourceCompatibility = JavaVersion.VERSION_14 repositories { mavenCentral() } dependencies { // H2 Database 1.4.200 を導入 implementation 'com.h2database:h2:1.4.200' } tasks.withType(JavaCompile) { // Java 14 のプレビュー機能を使う options.compilerArgs += ['--enable-preview'] } application { // Java 14 のプレビュー機能を使う applicationDefaultJvmArgs = ['--enable-preview'] mainClassName = 'JdbcSample' }JdbcSample.java
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.Statement; import java.util.Arrays; class JdbcSample { public static void main(String[] args) throws Exception { // H2 Database に接続 // mem: インメモリデータベース化 // DB_CLOSE_DELAY=-1: コネクション切断時にDBコンテンツを削除しない String url = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; String user = "sa"; String password = ""; Connection con = DriverManager.getConnection(url, user, password); Statement stmt = con.createStatement(); // テーブルを作成 // いろいろな JDBC 型 (SQL データ型とは微妙にちがうもの) でカラムを定義 // (Java 14 プレビュー機能で使えるヒアドキュメントっぽく書けるテキストブロック機能を使う) stmt.execute(""" create table test ( -- 文字列型 my_char CHAR, -- 短い固定長文字列 my_varchar VARCHAR, -- 短い可変長文字列 my_longvarchar LONGVARCHAR, -- 長い可変長文字列 my_clob CLOB, -- Character Large Object -- バイナリ型 my_binary BINARY(4), -- 小さな固定長バイナリ (SQL データ型の BIT) my_varbinary VARBINARY(4), -- 小さな可変長バイナリ (SQL データ型の BIT VARYING) my_longvarbinary LONGVARBINARY(4), -- 大きな可変長バイナリ my_blob BLOB(4), -- Binary Large Object -- 真偽値型 my_boolean BOOLEAN, -- 真偽値 -- 整数型 my_smallint SMALLINT, -- short my_integer INTEGER, -- int my_bigint BIGINT, -- long -- 浮動小数点型 my_real REAL, -- 単精度浮動小数点数 float my_double DOUBLE, -- 倍精度浮動小数点数 double -- 固定小数点型 my_numeric NUMERIC, -- 固定小数点数 my_decimal DECIMAL, -- 固定小数点型 -- 時間型 my_date DATE, -- 年月日 my_time TIME, -- 時分秒 my_timestamp TIMESTAMP -- 年月日 + 時分秒 + ナノ秒 )"""); // レコードを追加 // (Java 14 プレビュー機能で使えるヒアドキュメントっぽく書けるテキストブロック機能を使う) stmt.execute(""" insert into test values ( -- 文字列型 'Hello', -- CHAR 'Hello', -- VARCHAR 'Hello', -- LONGVARCHAR 'Hello', -- CLOB -- バイナリ型 X'CAFEBABE', -- BINARY, X'CAFEBABE', -- VARBINARY, X'CAFEBABE', -- LONGVARBINARY, X'CAFEBABE', -- BLOB, -- 真偽値型 TRUE, -- BOOLEAN -- 整数型 32767 , -- SMALLINT 2147483647 , -- INTEGER 9223372036854775807, -- BIGINT -- 浮動小数点型 123.0001, -- REAL 123.0001, -- DOUBLE -- 固定小数点型 123.0001, -- NUMERIC 123.0001, -- DECIMAL -- 時間型 '2001-02-03', -- DATE '04:05:06', -- TIME '2001-02-03 04:05:06.123456789' -- TIMESTAMP )"""); // レコードを取得 ResultSet rs = stmt.executeQuery("select * from test"); while (rs.next()) { // カラムの JDBC 型に対する Java オブジェクトの型を取得 System.out.println("カラム名 - JDBC 型 - Java オブジェクトの型"); ResultSetMetaData rsmd = rs.getMetaData(); for (int i = 1; i <= rsmd.getColumnCount(); i++) { System.out.println( rsmd.getColumnName(i) + " - " + rsmd.getColumnTypeName(i) + " - " + rsmd.getColumnClassName(i)); } System.out.println(); // カラムの値を取得していく System.out.println("カラム名 - カラムの値"); // 文字列型 System.out.println("my_char=" + rs.getString("my_char")); System.out.println("my_varchar=" + rs.getString("my_varchar")); System.out.println("my_longvarchar=" + rs.getString("my_longvarchar")); System.out.println("my_clob=" + rs.getClob("my_clob")); // バイナリ型 System.out.println("my_binary=" + Arrays.toString(rs.getBytes("my_binary"))); System.out.println("my_varbinary=" + Arrays.toString(rs.getBytes("my_varbinary"))); System.out.println("my_longvarbinary=" + Arrays.toString(rs.getBytes("my_longvarbinary"))); System.out.println("my_blob=" + rs.getBlob("my_blob")); // 真偽値型 System.out.println("my_boolean=" + rs.getBoolean("my_boolean")); // 整数型 System.out.println("my_smallint=" + rs.getShort("my_smallint")); System.out.println("my_integer=" + rs.getInt("my_integer")); System.out.println("my_bigint=" + rs.getBigDecimal("my_bigint")); // 浮動小数点型 System.out.println("my_real=" + rs.getFloat("my_real")); System.out.println("my_double=" + rs.getDouble("my_double")); // 固定小数点型 System.out.println("my_numeric=" + rs.getBigDecimal("my_numeric")); System.out.println("my_decimal=" + rs.getBigDecimal("my_decimal")); // 時間型 System.out.println("my_date=" + rs.getDate("my_date")); System.out.println("my_time=" + rs.getTime("my_time")); System.out.println("my_timestamp=" + rs.getTimestamp("my_timestamp")); } stmt.close(); con.close(); } }実行結果
Gradle の run タスクで実行。
$ gradle run > Task :compileJava 注意:/Users/foo/bar/src/main/java/JdbcSample.javaはプレビュー言語機能を使用します。 注意:詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。 > Task :run カラム名 - JDBC 型 - Java オブジェクトの型 MY_CHAR - CHAR - java.lang.String MY_VARCHAR - VARCHAR - java.lang.String MY_LONGVARCHAR - VARCHAR - java.lang.String MY_CLOB - CLOB - java.sql.Clob MY_BINARY - VARBINARY - [B MY_VARBINARY - VARBINARY - [B MY_LONGVARBINARY - VARBINARY - [B MY_BLOB - BLOB - java.sql.Blob MY_BOOLEAN - BOOLEAN - java.lang.Boolean MY_SMALLINT - SMALLINT - java.lang.Short MY_INTEGER - INTEGER - java.lang.Integer MY_BIGINT - BIGINT - java.lang.Long MY_REAL - REAL - java.lang.Float MY_DOUBLE - DOUBLE - java.lang.Double MY_NUMERIC - DECIMAL - java.math.BigDecimal MY_DECIMAL - DECIMAL - java.math.BigDecimal MY_DATE - DATE - java.sql.Date MY_TIME - TIME - java.sql.Time MY_TIMESTAMP - TIMESTAMP - java.sql.Timestamp カラム名 - カラムの値 my_char=Hello my_varchar=Hello my_longvarchar=Hello my_clob=clob0: 'Hello' my_binary=[-54, -2, -70, -66] my_varbinary=[-54, -2, -70, -66] my_longvarbinary=[-54, -2, -70, -66] my_blob=blob0: X'cafebabe' my_boolean=true my_smallint=32767 my_integer=2147483647 my_bigint=9223372036854775807 my_real=123.0001 my_double=123.0001 my_numeric=123.0001 my_decimal=123.0001 my_date=2001-02-03 my_time=04:05:06 my_timestamp=2001-02-03 04:05:06.123457 BUILD SUCCESSFUL in 1s 2 actionable tasks: 2 executed参考資料
- 投稿日:2020-08-10T11:27:58+09:00
Java + H2 Database で主要な JDBC 型の値を取得するサンプルコード
概要
- H2 Database にてテーブルに主要な JDBC 型のカラムを定義して、Java のプログラムからカラムの値を取得するサンプルコードを示す
- 今回の動作確認環境: H2 Database 1.4.200 + Java 14 (AdoptOpenJDK 14.0.2) + Gradle 6.5.1 + macOS Catalina
JDBC 型とは
JDBC 型は SQL 型と Java 言語の型の間を取り持つ型。SQL のデータを表す型 (CHAR や INTEGER など) に近いもの。
SQL 型 ←(マッピング)→ JDBC 型 ←(マッピング)→ Java 言語型
JDBC API 入門 - SQL と Java の型のマッピング
異なるデータベース製品がサポートする SQL の型の間には、相当な相違があります。異なるデータベースが同一の意味を持つ SQL の型をサポートしている場合でも、それらの型に異なる名前を与えていることがあります。たとえば、主要データベースのほとんどが大きなバイナリ値に対する SQL の型をサポートしていますが、Oracle ではこの型を LONG RAW、Sybase では IMAGE、Informix では BYTE、DB2 では LONG VARCHAR FOR BIT DATA とそれぞれ呼んでいます。
JDBC プログラマは、通常は、ターゲットのデータベースが使用している実際の SQL の型名に気を使う必要はありません。多くの場合、JDBC プログラマは、既存のデータベースのテーブルに対してプログラミングをし、そうしたテーブルを作成した正確な SQL の型名に注意を払う必要はありません。
JDBC は、クラス java.sql.Types で総称 SQL の型識別子のセットを定義しています。そのセットの型は、もっとも一般的に使用される SQL の型を表すように設計されています。JDBC API によるプログラミングでは、プログラマは通常、ターゲットのデータベースが使用している正確な SQL の型名を意識することなく、そのセットの JDBC 型を使用して総称 SQL の型を参照することができます。
サンプルコード
ファイル一覧
├── build.gradle └── src └── main └── java └── JdbcSample.javabuild.gradle
plugins { id 'application' id 'java' } sourceCompatibility = JavaVersion.VERSION_14 repositories { mavenCentral() } dependencies { // H2 Database 1.4.200 を導入 implementation 'com.h2database:h2:1.4.200' } tasks.withType(JavaCompile) { // Java 14 のプレビュー機能を使う options.compilerArgs += ['--enable-preview'] } application { // Java 14 のプレビュー機能を使う applicationDefaultJvmArgs = ['--enable-preview'] mainClassName = 'JdbcSample' }JdbcSample.java
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.Statement; import java.util.Arrays; class JdbcSample { public static void main(String[] args) throws Exception { // H2 Database に接続 // mem: インメモリデータベース化 // DB_CLOSE_DELAY=-1: コネクション切断時にDBコンテンツを削除しない String url = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"; String user = "sa"; String password = ""; Connection con = DriverManager.getConnection(url, user, password); Statement stmt = con.createStatement(); // テーブルを作成 // いろいろな JDBC 型 (SQL データ型とは微妙にちがうもの) でカラムを定義 // (Java 14 プレビュー機能で使えるヒアドキュメントっぽく書けるテキストブロック機能を使う) stmt.execute(""" create table test ( -- 文字列型 my_char CHAR, -- 短い固定長文字列 my_varchar VARCHAR, -- 短い可変長文字列 my_longvarchar LONGVARCHAR, -- 長い可変長文字列 my_clob CLOB, -- Character Large Object -- バイナリ型 my_binary BINARY(4), -- 小さな固定長バイナリ (SQL データ型の BIT) my_varbinary VARBINARY(4), -- 小さな可変長バイナリ (SQL データ型の BIT VARYING) my_longvarbinary LONGVARBINARY(4), -- 大きな可変長バイナリ my_blob BLOB(4), -- Binary Large Object -- 真偽値型 my_boolean BOOLEAN, -- 真偽値 -- 整数型 my_smallint SMALLINT, -- short my_integer INTEGER, -- int my_bigint BIGINT, -- long -- 浮動小数点型 my_real REAL, -- 単精度浮動小数点数 float my_double DOUBLE, -- 倍精度浮動小数点数 double -- 固定小数点型 my_numeric NUMERIC, -- 固定小数点数 my_decimal DECIMAL, -- 固定小数点型 -- 時間型 my_date DATE, -- 年月日 my_time TIME, -- 時分秒 my_timestamp TIMESTAMP -- 年月日 + 時分秒 + ナノ秒 )"""); // レコードを追加 // (Java 14 プレビュー機能で使えるヒアドキュメントっぽく書けるテキストブロック機能を使う) stmt.execute(""" insert into test values ( -- 文字列型 'Hello', -- CHAR 'Hello', -- VARCHAR 'Hello', -- LONGVARCHAR 'Hello', -- CLOB -- バイナリ型 X'CAFEBABE', -- BINARY, X'CAFEBABE', -- VARBINARY, X'CAFEBABE', -- LONGVARBINARY, X'CAFEBABE', -- BLOB, -- 真偽値型 TRUE, -- BOOLEAN -- 整数型 32767 , -- SMALLINT 2147483647 , -- INTEGER 9223372036854775807, -- BIGINT -- 浮動小数点型 123.0001, -- REAL 123.0001, -- DOUBLE -- 固定小数点型 123.0001, -- NUMERIC 123.0001, -- DECIMAL -- 時間型 '2001-02-03', -- DATE '04:05:06', -- TIME '2001-02-03 04:05:06.123456789' -- TIMESTAMP )"""); // レコードを取得 ResultSet rs = stmt.executeQuery("select * from test"); while (rs.next()) { // カラムの JDBC 型に対する Java オブジェクトの型を取得 System.out.println("カラム名 - JDBC 型 - Java オブジェクトの型"); ResultSetMetaData rsmd = rs.getMetaData(); for (int i = 1; i <= rsmd.getColumnCount(); i++) { System.out.println( rsmd.getColumnName(i) + " - " + rsmd.getColumnTypeName(i) + " - " + rsmd.getColumnClassName(i)); } System.out.println(); // カラムの値を取得していく System.out.println("カラム名 - カラムの値"); // 文字列型 System.out.println("my_char=" + rs.getString("my_char")); System.out.println("my_varchar=" + rs.getString("my_varchar")); System.out.println("my_longvarchar=" + rs.getString("my_longvarchar")); System.out.println("my_clob=" + rs.getClob("my_clob")); // バイナリ型 System.out.println("my_binary=" + Arrays.toString(rs.getBytes("my_binary"))); System.out.println("my_varbinary=" + Arrays.toString(rs.getBytes("my_varbinary"))); System.out.println("my_longvarbinary=" + Arrays.toString(rs.getBytes("my_longvarbinary"))); System.out.println("my_blob=" + rs.getBlob("my_blob")); // 真偽値型 System.out.println("my_boolean=" + rs.getBoolean("my_boolean")); // 整数型 System.out.println("my_smallint=" + rs.getShort("my_smallint")); System.out.println("my_integer=" + rs.getInt("my_integer")); System.out.println("my_bigint=" + rs.getBigDecimal("my_bigint")); // 浮動小数点型 System.out.println("my_real=" + rs.getFloat("my_real")); System.out.println("my_double=" + rs.getDouble("my_double")); // 固定小数点型 System.out.println("my_numeric=" + rs.getBigDecimal("my_numeric")); System.out.println("my_decimal=" + rs.getBigDecimal("my_decimal")); // 時間型 System.out.println("my_date=" + rs.getDate("my_date")); System.out.println("my_time=" + rs.getTime("my_time")); System.out.println("my_timestamp=" + rs.getTimestamp("my_timestamp")); } stmt.close(); con.close(); } }実行結果
Gradle の run タスクで実行。
$ gradle run > Task :compileJava 注意:/Users/foo/bar/src/main/java/JdbcSample.javaはプレビュー言語機能を使用します。 注意:詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。 > Task :run カラム名 - JDBC 型 - Java オブジェクトの型 MY_CHAR - CHAR - java.lang.String MY_VARCHAR - VARCHAR - java.lang.String MY_LONGVARCHAR - VARCHAR - java.lang.String MY_CLOB - CLOB - java.sql.Clob MY_BINARY - VARBINARY - [B MY_VARBINARY - VARBINARY - [B MY_LONGVARBINARY - VARBINARY - [B MY_BLOB - BLOB - java.sql.Blob MY_BOOLEAN - BOOLEAN - java.lang.Boolean MY_SMALLINT - SMALLINT - java.lang.Short MY_INTEGER - INTEGER - java.lang.Integer MY_BIGINT - BIGINT - java.lang.Long MY_REAL - REAL - java.lang.Float MY_DOUBLE - DOUBLE - java.lang.Double MY_NUMERIC - DECIMAL - java.math.BigDecimal MY_DECIMAL - DECIMAL - java.math.BigDecimal MY_DATE - DATE - java.sql.Date MY_TIME - TIME - java.sql.Time MY_TIMESTAMP - TIMESTAMP - java.sql.Timestamp カラム名 - カラムの値 my_char=Hello my_varchar=Hello my_longvarchar=Hello my_clob=clob0: 'Hello' my_binary=[-54, -2, -70, -66] my_varbinary=[-54, -2, -70, -66] my_longvarbinary=[-54, -2, -70, -66] my_blob=blob0: X'cafebabe' my_boolean=true my_smallint=32767 my_integer=2147483647 my_bigint=9223372036854775807 my_real=123.0001 my_double=123.0001 my_numeric=123.0001 my_decimal=123.0001 my_date=2001-02-03 my_time=04:05:06 my_timestamp=2001-02-03 04:05:06.123457 BUILD SUCCESSFUL in 1s 2 actionable tasks: 2 executed参考資料
- 投稿日:2020-08-10T11:24:10+09:00
Mavenのコマンド集
はじめに
MavenはJava向けのビルドツール、pom.xmlを元にビルドを実行します。
コマンドについて記載します。eclipseへ変換
eclipseに変換
mvn eclipse:eclipseeclipse関連のファイルを作成する。
eclipseに変換
mvn eclipse:cleaneclipse関連のファイルを消去する。
IntelliJへ変換
IntelliJに変換
mvn idea:ideaIntellij関連のファイルを作成する。
intellijの消去
mvn idea:cleanIntellij関連のファイルを消去する。
Mavneプロジェクト作成
Mavneプロジェクト作成
mvn archetype:generatearchetypeの指定
mvn archetype:generate -DarchetypeGroupId=${groupId} -DarchetypeArtifactId=${ArtifactId}
-D
でプロパティを指定する。そのほかにもプロパティを指定するときに使用できます。ビルド関連
実際のプロジェクトではこの辺を使用する。
コンパイル
mvn compileユニットテスト
mvn test主にJunitでテストする。
Jar or War の作成
mvn packagejar か war はpom.xmlで指定する。
ローカルリポジトリにインストール
mvn installデフォルトだとmaven central repositoryからライブラリ(Jar)をローカルリポジトリにインストールします。
ローカルリポジトリは~/.m2/repository
か${Maven home}/conf/settings.xml
で設定したパスにあります。削除
targetディレクトリ以下を削除する。
mvn cleanまとめ
JavaのCICD環境を整えることがあります。内部的にはMavenコマンドを叩いてるだけだったりするので、今回まとめました。
参考文献
- 投稿日:2020-08-10T11:24:10+09:00
Mavenのゴールの種類
はじめに
MavenはJava向けのビルドツール、pom.xmlを元にビルドを実行します。
コマンドについて記載します。eclipseへ変換
eclipse関連のファイルを生成する
mvn eclipse:eclipseeclipse関連のファイルを消去する
mvn eclipse:cleanIntelliJへ
IntelliJ関連のファイルを生成する
mvn idea:ideaintellij関連のファイルを消去する
mvn idea:cleanMavneプロジェクトの作成
Mavneプロジェクトの作成
mvn archetype:generatearchetypeの指定
mvn archetype:generate -DarchetypeGroupId=${groupId} -DarchetypeArtifactId=${ArtifactId}
-D
でプロパティを指定する。そのほかにもプロパティを指定するときに使用できます。ビルド関連
実際のプロジェクトではこの辺を使用する。
コンパイル
mvn compileユニットテスト
mvn test主にJunitでテストする。
Jar or War の作成
mvn packagejar か war はpom.xmlで指定する。
ローカルリポジトリにプラグインやライブラリをインストール
mvn installデフォルトだとmaven central repositoryからライブラリ(Jar)をローカルリポジトリにインストールします。
ローカルリポジトリは~/.m2/repository
か${Maven home}/conf/settings.xml
で設定したパスにあります。成果物を消去
targetディレクトリ以下を削除する。
mvn cleanまとめ
JavaのCICD環境を整えることがあります。内部的にはMavenコマンドを叩いてるだけだったりするので、今回まとめました。
参考文献
- 投稿日:2020-08-10T09:28:19+09:00
ConcurrentHashMapはkey,valueにnullを許容しない
ConcurrentHashmapはHashMapをスレッドセーフにしたものだけどちょっと違うところもあるよというメモ。
基本:
Map
なんとかMapの違いHashMap
ConcurrentHashMap
排他制御を達成するために java1.5から追加された。
が、nullを許容しない。ConcurrentHashMap.java/** * Maps the specified key to the specified value in this table. * Neither the key nor the value can be null. * * <p> The value can be retrieved by calling the <tt>get</tt> method * with a key that is equal to the original key. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt> * @throws NullPointerException if the specified key or value is null */ public V put(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); return segmentFor(hash).put(key, hash, value, false); }例:
Sample1.javaConcurrentHashMap map = new ConcurrentHashMap(); map.put("test", null);Exception in thread "main" java.lang.NullPointerException at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:881) at playground.Sample1.main(Sample1.java:9)Sample2.javaConcurrentHashMap map = new ConcurrentHashMap(); map.put(null, "test");Exception in thread "main" java.lang.NullPointerException at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:882) at playground.Sample2.main(Sample2.java:12)結果
HashMap→ConcurrentHashmap置換時にこういうことをしたソースが存在する。
Realcode.javaif (inputvalue == null) { return; }
- 投稿日:2020-08-10T00:25:48+09:00
JavaのIntrinsic Locksの動きをbpftraceで確認してみる
概要
JavaにはIntrinsic Locksという仕組がある。オブジェクトごとにそれぞれの固有ロック(Monitor Lock)がつけられて、
synchronized
というキーワードだけで楽なマルチスレッド並行処理が実現できる。では、プログラムが実行しているとき、スレッドたちはどうのように協同しているのか?今回は bpftrace を利用して、これを確認してみる。作業環境
OSはUbuntu 20.04にします。
OpenJDK 14
JDKのDtrace機能必要ですから、ソースからビルトします。
$ apt-get update $ DEBIAN_FRONTEND=noninteractive ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && apt-get install -y tzdata && dpkg-reconfigure --frontend noninteractive tzdata $ apt-get install build-essential autoconf systemtap-sdt-dev libx11-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxt-dev libcups2-dev libfontconfig1-dev libasound2-dev unzip zip ccache -y ## clone source and boot jdk $ mkdir jdk && cd jdk $ git clone https://github.com/openjdk/jdk14u.git $ wget https://download.java.net/java/GA/jdk13.0.2/d4173c853231432d94f001e99d882ca7/8/GPL/openjdk-13.0.2_linux-x64_bin.tar.gz $ tar xvf openjdk-13.0.2_linux-x64_bin.tar.gz $ cd jdk14u $ bash configure --enable-debug --with-jvm-variants=server --enable-dtrace --with-boot-jdk=../jdk-13.0.2 --enable-ccache $ make images $ export JAVA_HOME=$PWD/build/linux-x86_64-server-fastdebug/jdkbcc と bpftrace
## bcc $ apt-get install -y linux-headers-$(uname -r) bison build-essential cmake flex g++ git libelf-dev zlib1g-dev libfl-dev systemtap-sdt-dev binutils-dev llvm-8-dev llvm-8-runtime libclang-8-dev clang-8 arping netperf iperf3 python3-distutils $ git clone --recurse-submodules https://github.com/iovisor/bcc.git $ mkdir bcc/build; cd bcc/build $ cmake -DPYTHON_CMD=python3 .. $ make -j8 && make install && ldconfig $ cd ../.. ## bpftrace $ git clone https://github.com/iovisor/bpftrace.git $ mkdir bpftrace/build; cd bpftrace/build $ cmake -DHAVE_BCC_PROG_LOAD=ON -DHAVE_BCC_CREATE_MAP=ON -DBUILD_TESTING=OFF .. $ make -j8 && make installサンプルプログラムを用意
さて、こちらのjavaプログラムを用意します。やっていることはただ二つの競合スレットを呼び出すことです。
Test.javapublic class Test { private static synchronized void enter(String name) { try { System.out.println("enter " + name); Thread.sleep(1000); System.out.println("exit " + name); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { new Thread(() -> enter("foo"), "foo").start(); new Thread(() -> enter("bar"), "bar").start(); } }コンパイルする
$ $JAVA_HOME/bin/javac Test.java
実行と結果
まずは別のターミナルでこのbpftraceコマンドを起動して、トレースを準備する
$ bpftrace - <<EOF BEGIN { printf("%-12s %-6s %-27s %s\n", "TIME", "TID", "ACTION", "DESC"); } usdt:$JAVA_HOME/lib/server/libjvm.so:hotspot:thread__start { printf("%-12ld %-6d %-27s", elapsed, arg2, "start");printf("name=%s\n", str(arg0)); } usdt:$JAVA_HOME/lib/server/libjvm.so:hotspot:thread__stop { printf("%-12ld %-6d %-27s", elapsed, arg2, "stop");printf("name=%s\n", str(arg0)); } usdt:$JAVA_HOME/lib/server/libjvm.so:hotspot:monitor__contended__enter { printf("%-12ld %-6d %-27s", elapsed, arg0, "monitor_contended_enter"); printf("monitor=0x%lx, class=%s\n", arg1, str(arg2)); } usdt:$JAVA_HOME/lib/server/libjvm.so:hotspot:monitor__contended__entered { printf("%-12ld %-6d %-27s", elapsed, arg0, "monitor_contended_entered"); printf("monitor=0x%lx, class=%s\n", arg1, str(arg2)); } usdt:$JAVA_HOME/lib/server/libjvm.so:hotspot:monitor__contended__exit { printf("%-12ld %-6d %-27s", elapsed, arg0, "monitor_contended_exit"); printf("monitor=0x%lx, class=%s\n", arg1, str(arg2)); } EOF Attaching 6 probes...次は先のjavaのプログラムを実行します
$ $JAVA_HOME/bin/java -XX:+ExtendedDTraceProbes Test enter bar exit bar enter foo exit foo二つのスレットは順番に実行しました。では、トレースの方はどうですか。
Attaching 6 probes... TIME TID ACTION DESC 3568183750 2 start name=Reference Handler 3568644123 3 start name=Finalizer 3581708591 1 monitor_contended_exit monitor=0x7f7314003100, class=java/lang/Object���� 3583984755 4 start name=Signal Dispatcher 3584404128 6 start name=C2 CompilerThread0 3584475441 5 start name=Service Thread 3584812927 7 start name=C1 CompilerThread0 3585382491 8 start name=Sweeper thread 3618900047 9 start name=Common-Cleaner 4170272484 2 monitor_contended_exit monitor=0x7f7314005100, class=java/lang/ref/ReferenceQueue$Lock���# 4176664886 10 start name=Notification Thread 4201863620 11 start name=foo 4202328663 12 start name=bar 4203255448 11 monitor_contended_enter monitor=0x7f7314007000, class=java/lang/Class����� 5229989467 12 monitor_contended_exit monitor=0x7f7314007000, class=java/lang/Class����� 5230226530 11 monitor_contended_entered monitor=0x7f7314007000, class=java/lang/Class����� 5230593438 12 stop name=bar 6242293321 11 stop name=foo 6247012424 4 stop name=Signal Dispatcherここでわかるのは、いくつのJVM Systemスレットが起動してから、
foo
とbar
のスレットも起動しました。そして、foo
がmonitor_contended_enter
の状態になりました、その原因はもちろんbar
先に critical region に入りましたから。その後、bar
がmonitor_contended_exit
になって、critical regionを退出。すぐにfoo
もmonitor_contended_entered
になって、実行が開始しました。まとめ
簡単な応用例ですが、bpftrace あるいは bcc はすごく強力なツールです、これから他の可能性をもっと発掘て行きたいと思います。
参考資料