20200810のJavaに関する記事は23件です。

[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を使用したいので、リージョンをバージニア北部にします。

CodeStar1.png

アプリケーションの種類や、言語を選択できます。
今回は、Lambda + Java Springのテンプレートでプロジェクトを作成してみようと思います。

CodeStar3.png

続いてコードリポジトリを選択します。
今回は、CodeCommitを選択してみますね。

CodeStart4.png

内容を確認して、プロジェクトを作成します。

最後にIDEを選択します。
今回は、AWS Cloud9です。リージョンによっては、Cloud9を選択できないのでご注意ください。
Cloud9を選択すると、インスタンスタイプを聞かれますので、今回はデフォルトのt2.microのまま進めます。

CodeStar5.png

プロジェクトとIDEの作成が始まります(裏でCloudFormationが動作します)。

CodeStar6.png

プロジェクトが大体3分で環境構築が完了します。
IDEはEC2インスタンスの作成があるので、こちらも大体3分くらいかかります。
両方とも準備が完了すると、下記のような画面になります。

CodeStar7.png

リポジトリの状態

左側にあるメニューより「コード」を選択すると、CodeCommitの画面が表示されます。
Springのテンプレート構造が作成されており、buildspec.ymlまで作成されています。

CodeCommit1.png

なお、buildspec.ymlの中身はこんな感じです。

codespec.yml
version: 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.json

OpenJDK8で、Mavenでコンパイルされていることがわかりますね。

IDE

では、メニューからIDEを選択してみます。
すると、CLoud9が開始され、CodeCommitのリポジトリから、コードをgit cloneで自動的にコードを取得してくれます。

CodeCommit1.png

 あお、チームの設定を行うと、クロスアカウントでのコードリポジトリの共有や、モブプログラミングなんかも行うことができます。

[AWS] Cloud9でモブプログラミングの環境を作ってみる

アプリ実行

今回は、LambdaによるAPIです。
ダッシュボードにある「アプリの表示」ボタンを押すと、プロジェクトのテンプレートで最初に入っているAPIが呼び出されます。

{"Output":"Hello World!"}

具体的なAPI GatewayやLambdaはどうなっているのでしょう?

API Gateway

REST APIが一つできていることがわかりますね。

APIGateway.png

これ自体は、コード上のtenpkate.ymlに定義されています。

template.yml
Resources:
  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: post

Lambda

こちらは、Get用とPost用の2つが存在します。これも、API Gatewayで記載したtemplate.ymlで定義さている内容ですね。

Lambda1.png

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

パイプラインを確認すると、ビルドが開始されたことがわかります。

build1.png

実は、ここでエラーになります。
確認してみると、どうやらテストで失敗しているようですね。

[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が早いうちに東京リージョン対応になることを望みますが、それを差し引いても利用価値はありそうですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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を使用したいので、リージョンをバージニア北部にします。

CodeStar1.png

アプリケーションの種類や、言語を選択できます。
今回は、Lambda + Java Springのテンプレートでプロジェクトを作成してみようと思います。

CodeStar3.png

続いてコードリポジトリを選択します。
今回は、CodeCommitを選択してみますね。

CodeStart4.png

内容を確認して、プロジェクトを作成します。

最後にIDEを選択します。
今回は、AWS Cloud9です。リージョンによっては、Cloud9を選択できないのでご注意ください。
Cloud9を選択すると、インスタンスタイプを聞かれますので、今回はデフォルトのt2.microのまま進めます。

CodeStar5.png

プロジェクトとIDEの作成が始まります(裏でCloudFormationが動作します)。

CodeStar6.png

プロジェクトが大体3分で環境構築が完了します。
IDEはEC2インスタンスの作成があるので、こちらも大体3分くらいかかります。
両方とも準備が完了すると、下記のような画面になります。

CodeStar7.png

リポジトリの状態

左側にあるメニューより「コード」を選択すると、CodeCommitの画面が表示されます。
Springのテンプレート構造が作成されており、buildspec.ymlまで作成されています。

CodeCommit1.png

なお、buildspec.ymlの中身はこんな感じです。

codespec.yml
version: 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.json

OpenJDK8で、Mavenでコンパイルされていることがわかりますね。

IDE

では、メニューからIDEを選択してみます。
すると、CLoud9が開始され、CodeCommitのリポジトリから、コードをgit cloneで自動的にコードを取得してくれます。

CodeCommit1.png

なお、チームの設定を行うと、クロスアカウントでのコードリポジトリの共有や、モブプログラミングなんかも行うことができます。

[AWS] Cloud9でモブプログラミングの環境を作ってみる

アプリ実行

今回は、LambdaによるAPIです。
ダッシュボードにある「アプリの表示」ボタンを押すと、プロジェクトのテンプレートで最初に入っているAPIが呼び出されます。

{"Output":"Hello World!"}

具体的なAPI GatewayやLambdaはどうなっているのでしょう?

API Gateway

REST APIが一つできていることがわかりますね。

APIGateway.png

これ自体は、コード上のtenpkate.ymlに定義されています。

template.yml
Resources:
  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: post

Lambda

こちらは、Get用とPost用の2つが存在します。これも、API Gatewayで記載したtemplate.ymlで定義さている内容ですね。

Lambda1.png

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

パイプラインを確認すると、ビルドが開始されたことがわかります。

build1.png

実は、ここでエラーになります。
確認してみると、どうやらテストで失敗しているようですね。

[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が早いうちに東京リージョン対応になることを望みますが、それを差し引いても利用価値はありそうですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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を使用したいので、リージョンをバージニア北部にします。

CodeStar1.png

アプリケーションの種類や、言語を選択できます。
今回は、Lambda + Java Springのテンプレートでプロジェクトを作成してみようと思います。

CodeStar3.png

続いてコードリポジトリを選択します。
今回は、CodeCommitを選択してみますね。

CodeStart4.png

内容を確認して、プロジェクトを作成します。

最後にIDEを選択します。
今回は、AWS Cloud9です。リージョンによっては、Cloud9を選択できないのでご注意ください。
Cloud9を選択すると、インスタンスタイプを聞かれますので、今回はデフォルトのt2.microのまま進めます。

CodeStar5.png

プロジェクトとIDEの作成が始まります(裏でCloudFormationが動作します)。

CodeStar6.png

プロジェクトが大体3分で環境構築が完了します。
IDEはEC2インスタンスの作成があるので、こちらも大体3分くらいかかります。
両方とも準備が完了すると、下記のような画面になります。

CodeStar7.png

リポジトリの状態

左側にあるメニューより「コード」を選択すると、CodeCommitの画面が表示されます。
Springのテンプレート構造が作成されており、buildspec.ymlまで作成されています。

CodeCommit1.png

なお、buildspec.ymlの中身はこんな感じです。

codespec.yml
version: 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.json

OpenJDK8で、Mavenでコンパイルされていることがわかりますね。

IDE

では、メニューからIDEを選択してみます。
すると、CLoud9が開始され、CodeCommitのリポジトリから、コードをgit cloneで自動的にコードを取得してくれます。

CodeCommit1.png

 あお、チームの設定を行うと、クロスアカウントでのコードリポジトリの共有や、モブプログラミングなんかも行うことができます。

アプリ実行

今回は、LambdaによるAPIです。
ダッシュボードにある「アプリの表示」ボタンを押すと、プロジェクトのテンプレートで最初に入っているAPIが呼び出されます。

{"Output":"Hello World!"}

具体的なAPI GatewayやLambdaはどうなっているのでしょう?

API Gateway

REST APIが一つできていることがわかりますね。

APIGateway.png

これ自体は、コード上のtenpkate.ymlに定義されています。

template.yml
Resources:
  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: post

Lambda

こちらは、Get用とPost用の2つが存在します。これも、API Gatewayで記載したtemplate.ymlで定義さている内容ですね。

Lambda1.png

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

パイプラインを確認すると、ビルドが開始されたことがわかります。

build1.png

実は、ここでエラーになります。
確認してみると、どうやらテストで失敗しているようですね。

[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が早いうちに東京リージョン対応になることを望みますが、それを差し引いても利用価値はありそうですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

結論

  1. RXTXはFATAL ERRORを吐く。お手上げ。もうメンテされていないしお蔵入り!
  2. jSerialCommは不安定。無反応だったり例外を吐く。もうちょっと頑張るか?
  3. jpurejavacommはエベント受信が無反応。使い方が悪いのか?
  4. jsscはもしかしたら何とかなるかもしれない。受信内容の精査が必要。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【チュートリアル】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.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

@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) { ... }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JAVASCRIPT 開発者を採用する方法

JavaScript言語は、JavaおよびC言語に基づいた構成を持っている、強力なクライアント側のマルチパラダイムの動的言語です。それに多数のタイプとオペレーター、組み込みオブジェクトやメソッドが含まれています。JavaScriptはオブジェクト指向プログラミングと関数型プログラミングの両方をサポートしていますので、1つの言語内でほぼどんなオブジェクトまたは機能でも作成できます。

JavaScriptの起源と最新のトレンドに詳しいJavaScript プログラマーを採用する方法

JavaScriptは単独で実行不可能であり、JavaScriptコードを実行するブラウザーが存在します。 ユーザーがJavaScriptの有効なHTMLページを開こうとすると、スクリプトがブラウザーに送信され、ブラウザーはスクリプトの下で動作します。ブラウザー以外に、JavaScriptはAdobeサービス、サーバーサイド環境、データベース、SVG画像などで表示することもできます。JavaScript言語は幅広いタイプのアプリケーションに使用できます。
javascript-kaihatsusha-infographic.png

JavaScript開発の基本、利点やトラップ

2018年にリリースされたState of the Developer Ecosystemのレポートによると、JavaScriptは3年連続で、世界で最も使用されているプログラミング言語であると認識されました。この調査は、世界中の17か国の6千人のプログラマーを対象に実施されました。

JavaScriptプログラミング言語を使用し、他の言語の専門家の代わりにJavaScript プログラマーを採用する利点と欠点を説明します。間違いなく、利点の方が多いです。
javascript-kaihatsusha-no-tansho-to-chousho.png

Javascript プログラマー コスト

PayScaleによるトップ5か国のフリーランスベースでのJavaScript エンジニアの平均年間報酬をご覧ください。
javascript-developer-cost.png
その国の一般的なJavaScript プログラマーの時給を詳しく見てみると、1時間あたりの報酬はイギリスが一番高いことが明らかになります。

それでも、ウクライナのフリーランスJavaScript 開発者の時給は、JavaScriptプログラマーが勤務ないしは居住する上位国の中で最低です。ウクライナからのJavaScriptプログラマーが技術的知識と創造力において、世界中で非常に人気であるということはよく知られています。したがって、トップクラスのフリーランスJavaScriptエンジニアを採用する予定がある場合は、ウクライナのコーダーの採用をご検討ください。

ウクライナの開発者にご興味のある方は、以下のリンクをご参照ください。
https://jp.mobilunity.com/blog/hire-javascript-developer-jp/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Eclipseプラグイン・プロジェクトでJavaのクラスのメソッド・パラメータの名前をリフレクションで取得できない時の暫定対応メモ

Java8以降、クラスのコンストラクタを含むメソッドのパラメータ名をリフレクションを通して取得できるようになりました。javac-parametersオプションを追加してコンパイルすることで、これに必要な情報がクラスファイルに格納されます。

Eclipseにおいては、例えばJavaプロジェクトの場合、プロジェクト共通あるいはプロジェクト毎の設定において、以下のチェックを入れてエクスポートすることで、対応したjarを作成することができます。恐らく、チェックすることで、-parametersオプションが内部で設定されてコンパイルされるのだろうと思います。
eclipse-setting.png
簡略ですが、以下のような感じでコンストラクタやメソッドオブジェクトへの参照からリフレクションを使って、パラメータ名を確認することができます。

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バンドルを作成することにしました。(安易ですが)

以上、備忘録でした。(何か誤解しているかも?)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メルカリで検索した一覧から金額の簡単な統計分析 Java開発

今回作成したシステムについて

今回はJava言語を使って、ECサイト上の商品から価格の相場などを提供するためのシステムを作りました。
個人開発のため、少しの空き時間で作成したプロトタイプになります。
いろんなレビューをいただけたら幸いです。
込み入った技術の話は割愛します。もしかしたら、他の投稿で書くかも…

使用した主な技術

  • [Java] Web Scraping (Webスクレイピング)
  • [Java] GUI 操作
  • [Java] グラフ、チャートのプロット
  • [sh script(シェル)] コマンドラインからの設定と起動

全体像(イメージ)

overview.png

デモンストレーション

ezgif-6-3f593f1383be.gif

追記

本当は技術的な話を書きたかったけど、友達がみて簡単にわかる程度で。
友達が興味とか示してくれればシリーズ化して深く書き上げたい!
ちなみにソースコードはGitHubにアップロードしています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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();
    }
}

s1.jpg

複数のマスターを作成し、スライドに個別に適用する

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();
    }
}

s2.jpg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アップロードされたファイルの拡張子とサイズをチェックする方法

  • 環境
    • 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

やりたいこと

  1. アップロードされたファイルの拡張子が指定のもの以外の場合はエラーにしたい
  2. アップロードされたファイルのサイズが指定より大きかった場合はエラーにしたい
  3. エラーメッセージは親画面で指定したい

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で親画面から取得する

  1. エラーメッセージを親画面の隠し項目で設定しておく
  2. 子画面の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. 子画面を表示するときにパラメータでメッセージを渡す

  1. 親画面で子画面を表示するJavaSctiptを生成するときにエラーメッセージをGETのパラメータで設定する
  2. 子画面を開いたらパラメータをf:viewParamで受け取ってバッキングビーンに設定する
  3. バッキングビーンのエラーメッセージをJSON形式で置いておく
  4. 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. 親子画面で同じバッキングビーンを使う

  1. 親子画面で共通のバッキングビーンにエラーメッセージ取得処理を実装する
  2. あとは「方法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.java
package 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;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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で利用できるものにmockspyの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を利用する,または,リフレクションなどの黒魔術に手を出すことになります.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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">
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【新人向け】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.java
public 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.Java
public class Cart {
  // プロパティ
  private List<Product> list; // 入っている商品とその個数

  // メソッド
  /** 
   * 商品を追加する処理を行う
   */
  public void add(Product 商品) { /* 略 */ }

  /**
   * 入っている商品とその個数を返す処理
   */
  public List<Product> getList() { /* 略 */ }
  /**
   * 入っている商品の合計を返す処理
   */
  public int getSum() { /* 略 */ }
}

ちなみに全体像は次の通りになります。
Product.java
public 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.java
import 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フォルダがなければ作成します。

「ファイル」メニュー→「新規」→「ソースフォルダー」を選択します。

「ファイル」メニュー→「新規」→「ソースフォルダー」を選択します

次に表示される画面の「フォルダー名:」に「test」と入力し「完了」をクリックします。

表示される画面の「フォルダー名:」に「test」と入力し「完了」をクリックします

先ほど確認または作成したtestフォルダを右クリックし、「新規」→「その他」を選択します。

testフォルダを右クリックし、「新規」→「その他」を選択

新規の画面が表示されますので「Java」→「JUnit」→「JUnit テスト・ケース」を選択し、「次へ」を選択します。
もしくは新規の画面の上部のウィザードフィールドに「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 3 ライブラリーをビルド・パスに追加」を選択し、OKを押します

作成されたJUnitテストケースの実体は単なるJavaのプログラムファイルです。今回は次のように記述します。
CartTest.java
package 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では何のテストをしているか分かりやすいように日本語が使われることも多いです1testNo1と書くよりもこちらの方が分かりやすいのではないでしょうか。

またassertEqualsの引数の1つ目に文字列が入っているのに気がついたと思いますが、これはコメントだと思ってください。これを残しておくとJUnitでテストしたときに、この第一引数がメッセージとして表示され、何を意図したテストで失敗したのかが分かります。

JUnit4のテストケースを作成する

JUnit4のテストケースを作成します

JUnit4のテストケースを作成します

1.「新規 JUnit 4 テスト」を選択します
2. 名前にテストケースを書くクラスの名前を入力します(赤線部)。「テスト対象Test」が良いと思われます。
3. パッケージ名(青線部)はテストのクラスの対象と同じパッケージ(テスト対象Javaプログラムの最初の方に書いてある「package XXX;」の部分)を指定します。
4. 「完了」をクリックします。

パッケージ名と名前を書くクラスの名前を入力します。入力したら「完了」をクリックします

JUnit4用のライブラリが最初はありませんので、追加するかどうか聞かれますので「次のアクションを実行」を選択し、「JUnit 4 ライブラリーをビルド・パスに追加」を選択し、OKを押します。

「次のアクションを実行」を選択し、「JUnit 4 ライブラリーをビルド・パスに追加」を選択し、OKを押します

作成されたJUnitテストケースの実体は単なるJavaのプログラムファイルです。今回は次のように記述します。
CartTest2.java
package 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では何のテストをしているか分かりやすいように日本語が使われることも多いです1testNo1と書くよりもこちらの方が分かりやすいのではないでしょうか。

またassertEqualsの引数の1つ目に文字列が入っているのに気がついたと思いますが、これはコメントだと思ってください。これを残しておくとJUnitでテストしたときに、この第一引数がメッセージとして表示され、何を意図したテストで失敗したのかが分かります。

蛇足ですがJUnit 3とほぼ同様のコードも書けます
CartTest.java
package 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"であると検証する)って感じです。
他にもこんな感じで書けます。isnotはMatcherと呼ばれるものです。

assertThat( target, is( not( "not target") ) )

参考:
Java Master - JUnit 入門基本的な使い方
JUnitの使い方(初級)


JUnitの実行

作ったテストケースを実行します。「実行」メニュー→「実行」→「JUnitテスト」を選択します。

スクリーンショット 2020-08-10 11.27.02.png

すると画面のどこかに下記のような画面が出てきます。

出てこない場合は「ウィンドウ」メニュー→「ビューの表示」→「その他」を選択し「Java」→「JUnit」(赤線部)を選択してください。

「ウィンドウ」メニュー→「ビューの表示」→「その他」を選択

「Java」→「JUnit」(赤線部)を選択


JUnitの実行画面

ここでmyStore.CartTestの左の三角形をクリックしてみましょう。下記のように何のテストをしたのかが分かります。なお、成功したテストは緑(Green)で表示され、失敗したテストは赤(Red)で表示されます。
クラス内で実行したテストを表示

ちなみに、ここで、テストプログラムの28行目の"A001"を"A003"に変更して再度実行してみましょう。先ほど同じように右クリックで実行しても良いですが、JUnitビューがある場合は「テストの再実行」(赤線部)を押すと再実行可能です。
「テストの再実行」(赤線部)を押すと再実行可能

なお、"A001"を"A003"にしたことでファイルが変更されましたので保存するか確認するダイアログが出てきますので問題なければ「OK」を押します。ファイル名の横のチェックを外すとそのファイルが保存前状態でのプログラムが実行されます。
保存して起動ダイアログが表示されたら「OK」を押す<br>

すると、今度はアイコンの左下に群青色のバツが付きます。これが失敗の印です。
そして、失敗したテストをクリックすると、テストケースのどこで失敗したか表示されます。これをダブルクリックすると対象の行が表示されます。
失敗したテストをクリックすると、テストケースのどこで失敗したか表示されます。これをダブルクリックすると対象の行が表示されます。

また、障害トレースのjunit.framework.ComparisionFailure:の後ろ側を見るとエラーの詳細に「カートの1つめの商品IDがA001であるかの確認があること」の記述が確認できると思います。
また「Expected: is」に正しい値、「but: was」に実際の値(実行の結果)が表示されます。
エラーの詳細にカートの1つめの商品IDがA001であるかの確認があることが確認できる

さて、ながったらしい話になりましたが以上になります。


コラム(蛇足) テストとは?品質とは?何をどう検査するか?

単なるポエムです

JUnitは実行と検証の二つが大事だと書きました。このうち、検証について触れておこうと思います。
ここは超ややこしいので読み飛ばしてもOKです。
検証は実行の結果の値が期待する結果と同じかどうかを確認することですが、「何を」検証するかについては意見の分かれるところです。明確な正解があるのかも知れませんが、私は知りません(@t_wada さん辺りは知ってそうですが・・・)。

例えば、下記のようなクラスがあったとします。

SomeClass.java
class 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を使うテストも書くことになります。
このように直接的ではなく、間接的にテストすることもあり得るのではないかと思います。

こう言った「何を」「どのように」「誰が」と言った視点を元にプロジェクトで統一した視点を持ち、テストすることがチーム開発では必要ですので配属先などでどのようにテストするか聞きましょう。決まっていなければ開発の最初にチームのメンバーで統一した視点を共有できるようにしましょう。


  1. と言っても、半角スペースが入っていればエラーになりますので、一言でテストが表せるような言葉を選びましょう 

  2. リフレクションについては次などを参照 :Samurai blog - 【Java入門】リフレクションでメソッドの実行、フィールドの変更ひしだま's 技術メモページ - リフレクション、 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プロパティグラフに触れてみる

1. 初めに

  • プロパティグラフに触れる機会があったので、備忘録として残す気持ちで書きます。
  • グラフと聞くと、グラフ理論のようなアカデミックな数学を連想し易いですが、「グラフデータベース」を活用する場合はグラフ理論のような複雑な話はあまり必要ありません。
  • ここでは「グラフデータベース」にフォーカスを当ててながら認めます。

2. グラフとは

グラフは一言で言うと、単なる頂点(Vertex)と辺(edge)の集合です。
グラフはエンティティをノードとして表し、エンティティが世界とどのように関連しているかを関係として表現します。
グラフのデータモデルは以下の3つがあります。

プロパティグラフ

プロパティグラフの特徴として下記4つあります。

  • プロパティグラフにはノードと関係が含まれている。
  • ノードには、プロパティ(Key/Value)が含まれている。
  • 関係には名前と方向(direction)があり、必ず開始ノードと終了ノードがあります。
  • 関係にもプロパティを含めることができます。

例) 簡単なプロパティグラフ

image.png

ハイパーグラフ

ハイパーグラフは関係を任意の数のノードに接続できる汎用的なグラフモデルです。
前述のプロパティグラフでは関係は1つの開始ノードから終了ノードしか持てないが、ハイパーグラフでは任意の数のノードに接続できます。

例) アリスとボブが車を3台所有していることを表現するハイパーグラフ

image.png

トリプル

トリプルは「主語- 述語- 目的語」というデータ構造になります。
トリプルは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の全体概要です。
image.png

Oracle Cloud Infrastructure

PGXをどのように用いるのか、簡単なクラウドアーキテクチャは下記の通りです。
簡素化のために全てPublic Subnetベースで環境を整えます。

image.png

インストール手順は書くのがめんどくさいので省きますが、 中身は下記の通りです。
参考資料の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します。

image.png

ノード作成

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.java
import 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の両方で使い分けできることも確認しました。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java/JShellを用いて爆速でプロパティグラフのクエリを叩き込む

1. 初めに

  • プロパティグラフに触れる機会があったので、備忘録として残す気持ちで書きます。
  • グラフと聞くと、グラフ理論のようなアカデミックな数学を連想し易いですが、「グラフデータベース」を活用する場合はグラフ理論のような複雑な話はあまり必要ありません。
  • ここでは「グラフデータベース」にフォーカスを当ててながら認めます。

2. グラフとは

グラフは一言で言うと、単なる頂点(Vertex)と辺(edge)の集合です。
グラフはエンティティをノードとして表し、エンティティが世界とどのように関連しているかを関係として表現します。
グラフのデータモデルは以下の3つがあります。

プロパティグラフ

プロパティグラフの特徴として下記4つあります。

  • プロパティグラフにはノードと関係が含まれている。
  • ノードには、プロパティ(Key/Value)が含まれている。
  • 関係には名前と方向(direction)があり、必ず開始ノードと終了ノードがあります。
  • 関係にもプロパティを含めることができます。

例) 簡単なプロパティグラフ

image.png

ハイパーグラフ

ハイパーグラフは関係を任意の数のノードに接続できる汎用的なグラフモデルです。
前述のプロパティグラフでは関係は1つの開始ノードから終了ノードしか持てないが、ハイパーグラフでは任意の数のノードに接続できます。

例) アリスとボブが車を3台所有していることを表現するハイパーグラフ

image.png

トリプル

トリプルは「主語- 述語- 目的語」というデータ構造になります。
トリプルは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の全体概要です。
image.png

Oracle Cloud Infrastructure

PGXをどのように用いるのか、簡単なクラウドアーキテクチャは下記の通りです。
簡素化のために全てPublic Subnetベースで環境を整えます。

image.png

インストール手順は書くのがめんどくさいので省きますが、 中身は下記の通りです。
参考資料の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します。

image.png

ノード作成

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.java
import 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の両方で使い分けできることも確認しました。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java+PGXでグラフクエリを爆速で叩く

1. 初めに

  • プロパティグラフに触れる機会があったので、備忘録として残す気持ちで書きます。
  • グラフと聞くと、グラフ理論のようなアカデミックな数学を連想し易いですが、「グラフデータベース」を活用する場合はグラフ理論のような複雑な話はあまり必要ありません。
  • ここでは「グラフデータベース」にフォーカスを当ててながら認めます。

2. グラフとは

グラフは一言で言うと、単なる頂点(Vertex)と辺(edge)の集合です。
グラフはエンティティをノードとして表し、エンティティが世界とどのように関連しているかを関係として表現します。
グラフのデータモデルは以下の3つがあります。

プロパティグラフ

プロパティグラフの特徴として下記4つあります。

  • プロパティグラフにはノードと関係が含まれている。
  • ノードには、プロパティ(Key/Value)が含まれている。
  • 関係には名前と方向(direction)があり、必ず開始ノードと終了ノードがあります。
  • 関係にもプロパティを含めることができます。

例) 簡単なプロパティグラフ

image.png

ハイパーグラフ

ハイパーグラフは関係を任意の数のノードに接続できる汎用的なグラフモデルです。
前述のプロパティグラフでは関係は1つの開始ノードから終了ノードしか持てないが、ハイパーグラフでは任意の数のノードに接続できます。

例) アリスとボブが車を3台所有していることを表現するハイパーグラフ

image.png

トリプル

トリプルは「主語- 述語- 目的語」というデータ構造になります。
トリプルは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の全体概要です。
image.png

Oracle Cloud Infrastructure

PGXをどのように用いるのか、簡単なクラウドアーキテクチャは下記の通りです。
簡素化のために全てPublic Subnetベースで環境を整えます。

image.png

中身は下記の通りです。

クライアントサーバ

  • 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します。

image.png

ノード作成

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.java
import 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の両方で使い分けできることも確認しました。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.java

build.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

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.java

build.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

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mavenのコマンド集

はじめに

MavenはJava向けのビルドツール、pom.xmlを元にビルドを実行します。
コマンドについて記載します。

eclipseへ変換

eclipseに変換

mvn eclipse:eclipse

eclipse関連のファイルを作成する。

eclipseに変換

mvn eclipse:clean

eclipse関連のファイルを消去する。

IntelliJへ変換

IntelliJに変換

mvn idea:idea

Intellij関連のファイルを作成する。

intellijの消去

mvn idea:clean

Intellij関連のファイルを消去する。

Mavneプロジェクト作成

Mavneプロジェクト作成

mvn archetype:generate

archetypeの指定

mvn archetype:generate -DarchetypeGroupId=${groupId} -DarchetypeArtifactId=${ArtifactId}

-Dでプロパティを指定する。そのほかにもプロパティを指定するときに使用できます。

ビルド関連

実際のプロジェクトではこの辺を使用する。

コンパイル

mvn compile

ユニットテスト

mvn test

主にJunitでテストする。

Jar or War の作成

mvn package

jar か war はpom.xmlで指定する。

ローカルリポジトリにインストール

mvn install

デフォルトだとmaven central repositoryからライブラリ(Jar)をローカルリポジトリにインストールします。
ローカルリポジトリは~/.m2/repository${Maven home}/conf/settings.xmlで設定したパスにあります。

削除

targetディレクトリ以下を削除する。

mvn clean

まとめ

JavaのCICD環境を整えることがあります。内部的にはMavenコマンドを叩いてるだけだったりするので、今回まとめました。

参考文献

https://qiita.com/KevinFQ/items/e8363ad6123713815e68

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mavenのゴールの種類

はじめに

MavenはJava向けのビルドツール、pom.xmlを元にビルドを実行します。
コマンドについて記載します。

eclipseへ変換

eclipse関連のファイルを生成する

mvn eclipse:eclipse

eclipse関連のファイルを消去する

mvn eclipse:clean

IntelliJへ

IntelliJ関連のファイルを生成する

mvn idea:idea

intellij関連のファイルを消去する

mvn idea:clean

Mavneプロジェクトの作成

Mavneプロジェクトの作成

mvn archetype:generate

archetypeの指定

mvn archetype:generate -DarchetypeGroupId=${groupId} -DarchetypeArtifactId=${ArtifactId}

-Dでプロパティを指定する。そのほかにもプロパティを指定するときに使用できます。

ビルド関連

実際のプロジェクトではこの辺を使用する。

コンパイル

mvn compile

ユニットテスト

mvn test

主にJunitでテストする。

Jar or War の作成

mvn package

jar か war はpom.xmlで指定する。

ローカルリポジトリにプラグインやライブラリをインストール

mvn install

デフォルトだとmaven central repositoryからライブラリ(Jar)をローカルリポジトリにインストールします。
ローカルリポジトリは~/.m2/repository${Maven home}/conf/settings.xmlで設定したパスにあります。

成果物を消去

targetディレクトリ以下を削除する。

mvn clean

まとめ

JavaのCICD環境を整えることがあります。内部的にはMavenコマンドを叩いてるだけだったりするので、今回まとめました。

参考文献

https://qiita.com/KevinFQ/items/e8363ad6123713815e68

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ConcurrentHashMapはkey,valueにnullを許容しない

ConcurrentHashmapはHashMapをスレッドセーフにしたものだけどちょっと違うところもあるよというメモ。

基本:
Map
なんとかMapの違い

HashMap

nullを許容する
スレッドセーフでない

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.java
        ConcurrentHashMap 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.java
        ConcurrentHashMap 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.java
        if (inputvalue == null) {
            return;
        }

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/jdk

bcc と 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.java
public 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スレットが起動してから、foobarのスレットも起動しました。そして、foomonitor_contended_enterの状態になりました、その原因はもちろんbar先に critical region に入りましたから。その後、barmonitor_contended_exitになって、critical regionを退出。すぐにfoomonitor_contended_enteredになって、実行が開始しました。

まとめ

簡単な応用例ですが、bpftrace あるいは bcc はすごく強力なツールです、これから他の可能性をもっと発掘て行きたいと思います。

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む