ぶらっ記ぃ

日本語の練習をしています

scribejavaでOAuthの認証を行いアクセストークンを取得する

github.com

ここに書いてある通り。

この記事ではTwitterを対象にAmmoniteでやります。

依存を追加

執筆時点の最新版を使います

@ import $ivy.`com.github.scribejava:scribejava-apis:6.4.1`
import $ivy.$

サービスクラスのインスタンスを得る

Twitter, Yahoo! など、デフォルトでいくつかのWebサービスに対応したクラスが提供されている。

@ import com.github.scribejava.apis.
AWeberApi                              KeycloakApi                            TumblrApi
Asana20Api                             LinkedInApi                            TutByApi
AutomaticAPI                           LinkedInApi20                          TwitterApi
BoxApi20                               LiveApi                                UcozApi
DataportenApi                          MailruApi                              ViadeoApi
DiggApi                                MediaWikiApi                           VkontakteApi
DiscordApi                             MeetupApi                              WunderlistAPI
DoktornaraboteApi                      MicrosoftAzureActiveDirectory20Api     XingApi
EtsyApi                                MicrosoftAzureActiveDirectoryApi       YahooApi
FacebookApi                            MisfitApi                              YahooApi20
FitbitApi20                            NaverApi                               facebook
FlickrApi                              OdnoklassnikiApi                       fitbit
Foursquare2Api                         PinterestApi                           imgur
FoursquareApi                          Px500Api                               mailru
FrappeApi                              RenrenApi                              microsoftazureactivedirectory
FreelancerApi                          SalesforceApi                          odnoklassniki
GeniusApi                              SinaWeiboApi                           openid
GitHubApi                              SinaWeiboApi20                         salesforce
GoogleApi20                            SkyrockApi                             tutby
HHApi                                  StackExchangeApi                       vk
HiOrgServerApi20                       TheThingsNetworkV1StagingApi           wunderlist
ImgurApi                               TheThingsNetworkV2PreviewApi
KaixinApi20                            TrelloApi

Twitterだとこんな感じ

@ import com.github.scribejava.core.builder.ServiceBuilder
import com.github.scribejava.core.builder.ServiceBuilder

@ import com.github.scribejava.apis.TwitterApi
import com.github.scribejava.apis.TwitterApi

@ val twitter = new ServiceBuilder("your_api_key").apiSecret("your_api_secret").build(TwitterApi.instance)
twitter: com.github.scribejava.core.oauth.OAuth10aService = com.github.scribejava.core.oauth.OAuth10aService@160cf225

リクエストークンを取得する

@ val requestToken = twitter.getRequestToken
requestToken: com.github.scribejava.core.model.OAuth1RequestToken = com.github.scribejava.core.model.OAuth1RequestToken@f46f5616

「簡単でしょう?」 と、ドキュメントでは言っている

認証画面のURLを得る

@ val authUrl = twitter.getAuthorizationUrl(requestToken)
authUrl: String = "https://api.twitter.com/oauth/authorize?oauth_token=x8052gAAAAAA3Q69Axxxxxxxxxx"

ユーザ認証を行いPINを得る

前節の authUrl をブラウザで開く。
コピペが面倒なひとは java.aws.Desktop#browse を使うといいでしょう。

@ java.awt.Desktop.getDesktop.browse(new java.net.URI(authUrl))
(ブラウザが開く)

2019-08-22_21-24.png (68.9 kB)

2019-08-22_21-26.png (43.0 kB)

RequestToken と PIN から AccessToken を得る

@ val accessToken = twitter.getAccessToken(requestToken, "3xxxxx4")
accessToken: com.github.scribejava.core.model.OAuth1AccessToken = com.github.scribejava.core.model.OAuth1AccessToken@f8e4cf29

@ accessToken.getToken
res15: String = "your_access_token"

@ accessToken.getTokenSecret
res16: String = "your_token_secret"

やった~

(おまけ) Twitterのアクセストークンを取得するScalaスクリプト

というわけで、いつものScalaスクリプトです。

twitter_auth.sc

import $ivy.`com.github.scribejava:scribejava-apis:6.4.1`

import com.github.scribejava.core.builder.ServiceBuilder
import com.github.scribejava.apis.TwitterApi

import java.awt.Desktop
import java.net.URI

@main
def main(apiKey: String, apiSecret: String): Unit = {
  val twitter = new ServiceBuilder(apiKey).apiSecret(apiSecret).build(TwitterApi.instance)
  val requestToken = twitter.getRequestToken()
  val authUrl = twitter.getAuthorizationUrl(requestToken)

  Desktop.getDesktop.browse(new URI(authUrl))

  println("Enter the PIN...")
  val pin = scala.io.StdIn.readLine

  val accessToken = twitter.getAccessToken(requestToken, pin)
  println(s"Access Token: ${accessToken.getToken}")
  println(s"Access Token Secret: ${accessToken.getTokenSecret}")
}
amm twitter_auth.sc --apiKey your_api_key --apiSecret your_api_secret

ngrok とか、 serveo をうまく使えばPINの入力も不要になるかも。

ScalaからAWS CDKを叩いてインフラを構築する

モチベーション

先日、TypeScriptとPython向けに AWS Cloud Development Kit がGAになりました。

aws.amazon.com

現在GAではないのですが、JavaもこのAWS CDKの対象言語となっています。

じゃあ、JavaでできるならScalaでもできるよね?(決まり文句)
Scalaでインフラコードを書けるのは嬉しそう。

というわけでScalaからAWS CDKを叩けるか試してみました。

セットアップ

AWS CDKのインストール

npm install -g aws-cdk

Ammoniteのインストール

ammonite.io

今回はScalaスクリプトを使ってAWS CDKのAPIを叩いてみます。

以下は執筆時の最新版をインストールしています。

sudo sh -c '(echo "#!/usr/bin/env sh" && curl -L https://github.com/lihaoyi/Ammonite/releases/download/1.6.9/2.13-1.6.9) > /usr/local/bin/amm && chmod +x /usr/local/bin/amm' && amm

実行ファイルをプロジェクト下に置いておくと、他の人がAmmoniteをインストールする必要がなくなるのでおすすめ。

sudo cp /usr/local/bin/amm /path/to/your_project/amm

Scalaスクリプトの実装

AWS CDKのAPIを叩くScalaスクリプトを実装します。

リソースの定義などは公式のサンプルなどを参考にしてください。

// 依存ライブラリを追加
import $ivy.`software.amazon.awscdk:core:1.0.0.DEVPREVIEW`
import $ivy.`software.amazon.awscdk:s3:1.0.0.DEVPREVIEW`
import software.amazon.awscdk.core.{App, Construct, Stack, StackProps}
import software.amazon.awscdk.services.s3.{Bucket, BucketProps}

class ExampleStack(parent: Construct, id: String, props: StackProps = null) extends Stack(parent, id, props) {
  new Bucket(this, "aws-cdk-scala-script-example-bucket", BucketProps.builder().build())
}

// CDKアプリケーションを定義
val app = new App()

// CloudFormationのスタックを作成。appに追加。
new ExampleStack(app, "example-stack")

// CloudFormationのテンプレートを生成を明示的に行う。
// 本来は暗黙的に生成を行うようでこれはワークアラウンド。Issueが上がっている。
// https://github.com/aws/jsii/issues/456
app.synth()

cdk.json でCDKアプリケーションの実行方法を設定

今回はScalaスクリプトがCDKアプリケーションということになるので、先程のスクリプトをAmmoniteで実行するよう設定します。

{
  "app": "./amm aws-cdk-example.sc"
}

CDKアプリケーションで作成したスタックをデプロイする

スタックの一覧

cdk list ( or ls ) コマンドでCDKアプリケーションで定義したスタックの一覧が表示できます。

ここでは、上記で設定したScalaスクリプトコンパイルと実行が行われます。

$ cdk ls
Compiling /home/blacky/projects/scala/aws-cdk-scala-script/aws-cdk-example.sc
example-stack

cdk.out ディレクトリ以下にはCDKアプリケーションから作成されたテンプレートなどができています。

tree cdk.out/
cdk.out/
├── cdk.out
├── example-stack.template.json
└── manifest.json

0 directories, 3 files

$ cat cdk.out/example-stack.template.json
{
  "Resources": {
    "awscdkscalascriptexamplebucketD9CE1DF9": {
      "Type": "AWS::S3::Bucket",
      "UpdateReplacePolicy": "Retain",
      "DeletionPolicy": "Retain",
      "Metadata": {
        "aws:cdk:path": "example-stack/aws-cdk-scala-script-example-bucket/Resource"
      }
    },
    "awscdkscalaD5F5E654": {
      "Type": "AWS::IAM::User",
      "Metadata": {
        "aws:cdk:path": "example-stack/aws-cdk-scala/Resource"
      }
    },
    "awscdkscalaDefaultPolicyF833299D": {
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyDocument": {
          "Statement": [
            {
              "Action": [
                "s3:GetObject*",
                "s3:GetBucket*",
                "s3:List*",
                "s3:DeleteObject*",
                "s3:PutObject*",
                "s3:Abort*"
              ],
              "Effect": "Allow",
              "Resource": [
                {
                  "Fn::GetAtt": [
                    "awscdkscalascriptexamplebucketD9CE1DF9",
                    "Arn"
                  ]
                },
                {
                  "Fn::Join": [
                    "",
                    [
                      {
                        "Fn::GetAtt": [
                          "awscdkscalascriptexamplebucketD9CE1DF9",
                          "Arn"
                        ]
                      },
                      "/*"
                    ]
                  ]
                }
              ]
            }
          ],
          "Version": "2012-10-17"
        },
        "PolicyName": "awscdkscalaDefaultPolicyF833299D",
        "Users": [
          {
            "Ref": "awscdkscalaD5F5E654"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "example-stack/aws-cdk-scala/DefaultPolicy/Resource"
      }
    }
  }
}

スタックのデプロイ

cdk deploy でスタックのデプロイを行います。

$ cdk deploy example-stack # 今回スタックはひとつなので `example-stack` は省略可
example-stack: deploying...
example-stack: creating CloudFormation changeset...
 0/3 | 18:42:21 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata | CDKMetadata
 0/3 | 18:42:22 | CREATE_IN_PROGRESS   | AWS::S3::Bucket    | aws-cdk-scala-script-example-bucket (awscdkscalascriptexamplebucketD9CE1DF9)
 0/3 | 18:42:24 | CREATE_IN_PROGRESS   | AWS::S3::Bucket    | aws-cdk-scala-script-example-bucket (awscdkscalascriptexamplebucketD9CE1DF9) Resource creation Initiated
 0/3 | 18:42:24 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated
 1/3 | 18:42:24 | CREATE_COMPLETE      | AWS::CDK::Metadata | CDKMetadata
 2/3 | 18:42:44 | CREATE_COMPLETE      | AWS::S3::Bucket    | aws-cdk-scala-script-example-bucket (awscdkscalascriptexamplebucketD9CE1DF9)
 3/3 | 18:42:46 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | example-stack

 ✅  example-stack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:012345678912:stack/example-stack/a7f50010-xxxx-yyyy-zzzz-012345678912

スタックのデプロイと適用ができました!
実際にリソースが作成されているか確かめてみましょう。

f:id:Nomad_Blacky:20190714193705p:plain

確かにS3バケットが作成されているのがわかります!

スタックの更新

せっかくなので他のリソースも作ってみましょう。
Scalaスクリプトを書き換えます。

import $ivy.`software.amazon.awscdk:core:1.0.0.DEVPREVIEW`
import $ivy.`software.amazon.awscdk:s3:1.0.0.DEVPREVIEW`
import $ivy.`software.amazon.awscdk:iam:1.0.0.DEVPREVIEW` // 追加
import software.amazon.awscdk.core.{App, Construct, Stack, StackProps}
import software.amazon.awscdk.services.iam.User
import software.amazon.awscdk.services.s3.{Bucket, BucketProps}

class ExampleStack(parent: Construct, id: String, props: StackProps = null) extends Stack(parent, id, props) {
  val bucket = new Bucket(this, "aws-cdk-scala-script-example-bucket", BucketProps.builder().build())

  // ユーザーの定義
  val user = new User(this, "aws-cdk-scala")
  // バケットへのアクセスを許可する
  bucket.grantReadWrite(user)
}

val app = new App()

new ExampleStack(app, "example-stack")

app.synth()

書き換えたら、 cdk diff を実行してみましょう。
これは現在のスタックの状態との差分を表示するコマンドです。

$ cdk diff example-stack
Stack example-stack
IAM Statement Changes
┌───┬────────────────────────────────────────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────────────────────┬──────────────────────┬───────────┐
│   │ Resource                                                                               │ Effect │ Action                                                                   │ Principal            │ Condition │
├───┼────────────────────────────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ + │ ${aws-cdk-scala-script-example-bucket.Arn}                                             │ Allow  │ s3:Abort*                                                                │ AWS:${aws-cdk-scala} │           │
│   │ ${aws-cdk-scala-script-example-bucket.Arn}/*                                           │        │ s3:DeleteObject*                                                         │                      │           │
│   │                                                                                        │        │ s3:GetBucket*                                                            │                      │           │
│   │                                                                                        │        │ s3:GetObject*                                                            │                      │           │
│   │                                                                                        │        │ s3:List*                                                                 │                      │           │
│   │                                                                                        │        │ s3:PutObject*                                                            │                      │           │
└───┴────────────────────────────────────────────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────────────┴──────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Resources
[+] AWS::IAM::User aws-cdk-scala awscdkscalaD5F5E654
[+] AWS::IAM::Policy aws-cdk-scala/DefaultPolicy awscdkscalaDefaultPolicyF833299D

追加したリソースとIAMのポリシーの詳細が表示されているのがわかります。

変更を確認したらデプロイしましょう。

$ cdk deploy example-stack
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────────────────────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────────────────────┬──────────────────────┬───────────┐
│   │ Resource                                                                               │ Effect │ Action                                                                   │ Principal            │ Condition │
├───┼────────────────────────────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ + │ ${aws-cdk-scala-script-example-bucket.Arn}                                             │ Allow  │ s3:Abort*                                                                │ AWS:${aws-cdk-scala} │           │
│   │ ${aws-cdk-scala-script-example-bucket.Arn}/*                                           │        │ s3:DeleteObject*                                                         │                      │           │
│   │                                                                                        │        │ s3:GetBucket*                                                            │                      │           │
│   │                                                                                        │        │ s3:GetObject*                                                            │                      │           │
│   │                                                                                        │        │ s3:List*                                                                 │                      │           │
│   │                                                                                        │        │ s3:PutObject*                                                            │                      │           │
└───┴────────────────────────────────────────────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────────────┴──────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Do you wish to deploy these changes (y/n)? y
example-stack: deploying...
example-stack: creating CloudFormation changeset...
 0/3 | 19:03:03 | CREATE_IN_PROGRESS   | AWS::IAM::User     | aws-cdk-scala (awscdkscalaD5F5E654)
 0/3 | 19:03:04 | CREATE_IN_PROGRESS   | AWS::IAM::User     | aws-cdk-scala (awscdkscalaD5F5E654) Resource creation Initiated
0/3 Currently in progress: awscdkscalaD5F5E654
 1/3 | 19:03:40 | CREATE_COMPLETE      | AWS::IAM::User     | aws-cdk-scala (awscdkscalaD5F5E654)
 1/3 | 19:03:42 | CREATE_IN_PROGRESS   | AWS::IAM::Policy   | aws-cdk-scala/DefaultPolicy (awscdkscalaDefaultPolicyF833299D)
 1/3 | 19:03:44 | CREATE_IN_PROGRESS   | AWS::IAM::Policy   | aws-cdk-scala/DefaultPolicy (awscdkscalaDefaultPolicyF833299D) Resource creation Initiated
 2/3 | 19:03:52 | CREATE_COMPLETE      | AWS::IAM::Policy   | aws-cdk-scala/DefaultPolicy (awscdkscalaDefaultPolicyF833299D)
 2/3 | 19:03:54 | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack | example-stack
 3/3 | 19:03:55 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | example-stack

 ✅  example-stack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:012345678912:stack/example-stack/a7f50010-xxxx-yyyy-zzzz-012345678912

f:id:Nomad_Blacky:20190714193734p:plain

無事、IAMユーザとポリシーの設定ができました!

まとめ

ScalaAWSのインフラ作成ができるようになりました。激アツですね!
TerraformのようなHCLの書き方を覚える必要がないですし、慣れた言語やIDEの恩恵を受けながらガツガツインフラを書けるのは夢がありそうです。

ただし、気になるところはいくつかあって、

  • AWS以外のインフラも併せて作成する場合には素直にTerraformなどを使ったほうが良さそう。
  • Javaのビルダーパターンによるリソースの定義が若干鬱陶しい
  • AWS CDK Javaはまだ開発段階
    • コンパイル時にリソース定義の誤り(必須パラメータが足りないなど)は検出されない。
    • バリデーションに抜けがあって、スタックの適用時にエラーになる。

など、これからに期待という感じでしょうか。

今回の成果物

github.com

余談

AWS CDK Javaはjsiiというライブラリ使ってJavaからTypeScriptのモジュールを呼び出しているらしいです。

github.com

Maven Central RepositoryからJarを取得して実行可能なスクリプトに変換する

coursierのCLIでbootstrapを使うとできる

以下は Ammonite をダウンロードしてスクリプト化する例

$ coursier bootstrap com.lihaoyi:ammonite_2.12.8:1.6.6 -M ammonite.Main -o amm --standalone
https://repo1.maven.org/maven2/com/lihaoyi/ammonite_2.12.8/1.6.6/ammonite_2.12.8-1.6.6.pom
  100.0% [##########] 2.2 KiB (1.8 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-runtime_2.12/1.6.6/ammonite-runtime_2.12-1.6.6.pom
  100.0% [##########] 1.9 KiB (7.7 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-terminal_2.12/1.6.6/ammonite-terminal_2.12-1.6.6.pom
  100.0% [##########] 1.5 KiB (8.3 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-ops_2.12/1.6.6/ammonite-ops_2.12-1.6.6.pom
  100.0% [##########] 1.5 KiB (3.0 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-repl_2.12.8/1.6.6/ammonite-repl_2.12.8-1.6.6.pom
  100.0% [##########] 2.9 KiB (5.1 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-interp_2.12.8/1.6.6/ammonite-interp_2.12.8-1.6.6.pom
  100.0% [##########] 2.4 KiB (13.4 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-util_2.12/1.6.6/ammonite-util_2.12-1.6.6.pom
  100.0% [##########] 2.0 KiB (11.9 KiB / s)
https://repo1.maven.org/maven2/io/get-coursier/coursier_2.12/1.1.0-M13-1/coursier_2.12-1.1.0-M13-1.pom
  100.0% [##########] 2.6 KiB (14.6 KiB / s)
https://repo1.maven.org/maven2/io/get-coursier/coursier-cache_2.12/1.1.0-M13-1/coursier-cache_2.12-1.1.0-M13-1.pom
  100.0% [##########] 1.8 KiB (10.3 KiB / s)
https://repo1.maven.org/maven2/io/get-coursier/coursier-core_2.12/1.1.0-M13-1/coursier-core_2.12-1.1.0-M13-1.pom
  100.0% [##########] 1.8 KiB (10.2 KiB / s)
https://repo1.maven.org/maven2/org/scala-lang/modules/scala-xml_2.12/1.1.1/scala-xml_2.12-1.1.1.pom
  100.0% [##########] 2.7 KiB (15.1 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-repl_2.12.8/1.6.6/ammonite-repl_2.12.8-1.6.6.jar
  100.0% [##########] 173.0 KiB (252.9 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-util_2.12/1.6.6/ammonite-util_2.12-1.6.6.jar
  100.0% [##########] 173.8 KiB (252.3 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite_2.12.8/1.6.6/ammonite_2.12.8-1.6.6.jar
  100.0% [##########] 150.9 KiB (182.1 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-interp_2.12.8/1.6.6/ammonite-interp_2.12.8-1.6.6.jar
  100.0% [##########] 321.6 KiB (372.2 KiB / s)
https://repo1.maven.org/maven2/io/get-coursier/coursier_2.12/1.1.0-M13-1/coursier_2.12-1.1.0-M13-1.jar
  100.0% [##########] 250.7 KiB (696.5 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-ops_2.12/1.6.6/ammonite-ops_2.12-1.6.6.jar
  100.0% [##########] 123.3 KiB (163.6 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-runtime_2.12/1.6.6/ammonite-runtime_2.12-1.6.6.jar
  100.0% [##########] 216.3 KiB (1.1 MiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/ammonite-terminal_2.12/1.6.6/ammonite-terminal_2.12-1.6.6.jar
  100.0% [##########] 171.8 KiB (411.9 KiB / s)
https://repo1.maven.org/maven2/io/get-coursier/coursier-cache_2.12/1.1.0-M13-1/coursier-cache_2.12-1.1.0-M13-1.jar
  100.0% [##########] 274.2 KiB (304.7 KiB / s)
https://repo1.maven.org/maven2/io/get-coursier/coursier-core_2.12/1.1.0-M13-1/coursier-core_2.12-1.1.0-M13-1.jar
  100.0% [##########] 1.5 MiB (1.4 MiB / s)
https://repo1.maven.org/maven2/org/scala-lang/modules/scala-xml_2.12/1.1.1/scala-xml_2.12-1.1.1.jar
  100.0% [##########] 540.4 KiB (1.5 MiB / s)

$ ./amm
Loading...
Compiling (synthetic)/ammonite/predef/interpBridge.sc
Compiling (synthetic)/ammonite/predef/replBridge.sc
Compiling (synthetic)/ammonite/predef/DefaultPredef.sc
Welcome to the Ammonite Repl 1.6.6
(Scala 2.12.8 Java 1.8.0_181)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ 

スクリプトを見るとシェルスクリプトにバイナリが埋め込まれているのがわかる

$ cat amm | head -n30
#!/usr/bin/env sh
nargs=$#

i=1; while [ "$i" -le $nargs ]; do
         eval arg=\${$i}
         case $arg in
             -J-*) set -- "$@" "${arg#-J}" ;;
         esac
         i=$((i + 1))
     done

set -- "$@" -jar "$0"

i=1; while [ "$i" -le $nargs ]; do
         eval arg=\${$i}
         case $arg in
             -J-*) ;;
             *) set -- "$@" "$arg" ;;
         esac
         i=$((i + 1))
     done

shift "$nargs"

exec java -Dsun.misc.URLClassPath.disableJarChecking $JAVA_OPTS "$@"
+N#coursier/bootstrap/launcher/a.classV[WWI80)DEj5Z+(Fzq2Qb~ZvUЮ??з>wK
                                                                      @}g؁$
                                                                           K8)aJ%p^     K4
8 H Ȉ␌&                                                                                   ǛoqǻqLJq|      ǧq\y9A'C^AQ2'HjQeaztˈ !dHz9*
       ʸX
qAR
L()OQ`L<'1%R2N+x#
.P.AWS߀
       +J8*#
            `c&5mi?Wj=e_fJ']'uJ}A3u-O'*kYpg=>dgMxl,2wlW\öSa(zJWa9L-dzM~B㱱3
                                                                           {JGK@U
                                                                                 []xQw\U]B!(:ZSOTNڢv8^Vm7:jYK$ոu>2*;$b82Fe\E7ŦӖ#F<0oR:EUvv@M>g+'S)5-F)'':U'ALyDu*q6v-[
M?UM

Scalaスクリプトを実行したいけどAmmoniteインストールしてないよ!

…という人に向けてScalaスクリプトを配布するのに向いてるかも?

scalafmtでfor式の `<-` と `=` をAlignする

Scalaコードをフォーマットするのによくscalafmtが用いられると思います。

scalameta.org

自身は仕事/個人開発でよく下記の設定を使っています。(宣伝)

github.com

sbtプロジェクトのディレクトリ下でちょこっとコマンドを叩くだけでscalafmtの環境が整うので便利です!

for式の <-= をAlignしたい

しかし、コードを書いているとある点が気になりました。

以下のようなコードがあるとします。

for {
  a <- Some(10)
  bbbbbbbbbbbbbbb <- Some(20)
  ccc = 300
  dddddddddd <- Try {
    4000
  }.toOption
  eeeeee = 50000
} yield a + bbbbbbbbbbbbbbb + ccc + dddddddddd + eeeeee

scalafmt.conf は以下のような設定です。

style                          = defaultWithAlign
danglingParentheses            = true
indentOperator                 = spray
maxColumn                      = 120
rewrite.rules                  = [RedundantParens, SortImports, PreferCurlyFors]
binPack.literalArgumentLists   = false
unindentTopLevelOperators      = true

scalafmtを実行すると以下のようにフォーマットされます。

for {
  a               <- Some(10)
  bbbbbbbbbbbbbbb <- Some(20)
  ccc = 300
  dddddddddd <- Try {
    4000
  }.toOption
  eeeeee = 50000
} yield a + bbbbb + ccc + dddddddddd + eeeeee

いい感じになりました!
…が、 ccc = 300 の部分がAlignされていませんね。

この = をAlignしたいときにどういう設定をすればよいのでしょうか?

align = most を使う

ドキュメントを見てみると align = most が使えそうでした。

scalafmt.conf に以下を追加します。

align = most

scalafmtを実行します。

for {
  a               <- Some(10)
  bbbbbbbbbbbbbbb <- Some(20)
  ccc             = 300
  dddddddddd <- Try {
                 4000
               }.toOption
  eeeeee = 50000
} yield a + bbbbbbbbbbbbbbb + ccc + dddddddddd + eeeeee

<-= がAlignされましたね!

しかし、 Try のブロックのインデントがちょっと深いのが気になる人が居るかもしれません…

align.arrowEnumeratorGeneratoralign.tokenCategory を設定する

most の設定がどうなっているか見てみます。

github.com

  val most: Align = more.copy(
    arrowEnumeratorGenerator = true,
    tokenCategory = Map(
      "Equals" -> "Assign",
      "LeftArrow" -> "Assign"
    )
  )

more の設定から align.arrowEnumeratorGeneratoralign.tokenCategory を追加しています。

align.arrowEnumeratorGeneratorをtrueに設定されていることで問題のインデントがなされていそうです。

most の設定を参考にしつつ以下のように scalafmt.conf を書き換えます。

//align = most
align.arrowEnumeratorGenerator = false
align.tokenCategory            = {
  Equals = Assign
  LeftArrow = Assign
}

scalafmtを実行します。

for {
  a               <- Some(10)
  bbbbbbbbbbbbbbb <- Some(20)
  ccc             = 300
  dddddddddd <- Try {
    4000
  }.toOption
  eeeeee = 50000
} yield a + bbbbbbbbbbbbbbb + ccc + dddddddddd + eeeeee

<-= をAlignしつつ、 Try のインデントをおさえることができました!


scalafmtは柔軟にフォーマットを定義できて便利ですね~~~

ロガーを自分で実装するのはやめよう

最近以下のようなコードと出会いました。

package logging

import org.slf4j.LoggerFactory

object Logger {
  private val logger = LoggerFactory.getLogger("LOGGER")

  def debug(message: String): Unit = {
    logger.debug(message)
  }

  def info(message: String): Unit = {
    logger.info(message)
  }
  
  // warn, error...
}

いわゆる「自作ロガー」と言うものですね。
みなさんもこのようなコードを見たことがあるのではないでしょうか。

今回は、この自作ロガーのメリット・デメリットを考えてみます。

(前提)

今回の例では slf4j + Logback を使います。

自作ロガーのメリット

クラスごとにインスタンス生成が不要

このような自作ロガーのメリットはクラスごとにインスタンス生成が不要なことでしょう。

import logging.Logger
import org.slf4j.LoggerFactory

// 自作ロガーを使った場合
object Sample01 {
  def doSomething(): Unit = {
    Logger.info("do something!")
  }
}

// 使わない場合
object Sample02 {
  private[this] val logger = LoggerFactory.getLogger(getClass)
  
  def doSomething(): Unit = {
    logger.info("do something!")
  }
}

自作ロガーを使った方がシンプルに見えます。
ロギングをしたいクラスごとにインスタンス生成をする必要がないのでタイプ量が少なくて楽そうですね。

自作ロガーのデメリット

しかしながら、このような自作ロガーを使うことはメリットを大きく上回るデメリットがあると思っています。

ログに関心のないスタックが入り込む

以下のような logback.xml があるとします。

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>[%class#%method\(%file:%line\)] %logger{0} - %msg%n</pattern>
    </encoder>
  </appender>

  <logger name="LOGGER" level="debug">
    <appender-ref ref="STDOUT" />
  </logger>
</configuration>

%class#%method\(%file:%line\) の部分でロガーが呼ばれた箇所を出力しています。

自作ロガーを使ってロギングしてみましょう

object Sample03 {
  def main(args: Array[String]): Unit = {
    Logger.info("logging!!!!!!")
  }
}
[logging.Logger$#info(Logger.scala:9)] LOGGER - logging!!!!!!

Sample03$#main... の出力を期待してしまいますが、実際に出力されたのは logging.Logger$#info(Logger.scala:9) です。

まぁよく考えれば当たり前なのですが、これではどこでログが吐かれたのかわかりにくくデバッグの際に困ってしまいます。
また、例えばSentryなどでエラーログが吐かれた箇所のスタックトレースが表示されたときに関心のないスタックが含まれてしまうのは良くないです。

※ただし、この問題に関しては LocationAwareLogger を使うことで回避できます。

参考: 自作LoggerではLocationAwareLoggerを使おう

ロギングライブラリの仕組みに乗れない

以下のようなクラスがあるとします。
logback.xml の設定は上記と同じものを使っています。

import logging.Logger

package pkgA {
  class ClassA {
    def doSomething(): Unit = {
      Logger.debug("DEBUG A")
      Logger.info("INFO A")
    }
  }
}

package pkgB {
  class ClassB {
    def doSomething(): Unit = {
      Logger.debug("DEBUG B")
      Logger.info("INFO B")
    }
  }

  class ClassC {
    def doSomething(): Unit = {
      Logger.debug("DEBUG C")
      Logger.info("INFO C")
    }
  }
}

package main {

  import pkgA.ClassA
  import pkgB.{ClassB, ClassC}

  object Main {
    def main(args: Array[String]): Unit = {
      (new ClassA).doSomething()
      (new ClassB).doSomething()
      (new ClassC).doSomething()
    }
  }
}

mainを実行します。

[logging.Logger$#debug(Logger.scala:9)] LOGGER - DEBUG A
[logging.Logger$#info(Logger.scala:13)] LOGGER - INFO A
[logging.Logger$#debug(Logger.scala:9)] LOGGER - DEBUG B
[logging.Logger$#info(Logger.scala:13)] LOGGER - INFO B
[logging.Logger$#debug(Logger.scala:9)] LOGGER - DEBUG C
[logging.Logger$#info(Logger.scala:13)] LOGGER - INFO C

ログが出力されました。

ここで、 pkgB パッケージの DEBUG ログに関心がなくなったので出力を止めたいと思ったとします。
logback.xml を以下のように書き換えてみますが…

  <!-- debug -> info -->
  <logger name="LOGGER" level="info">
    <appender-ref ref="STDOUT" />
  </logger>
[logging.Logger$#info(Logger.scala:13)] LOGGER - INFO A
[logging.Logger$#info(Logger.scala:13)] LOGGER - INFO B
[logging.Logger$#info(Logger.scala:13)] LOGGER - INFO C

関係のない ClassA のログまでもログが止まってしまいました。
Logger オブジェクトがひとつのロガーインスタンスを使いまわしているのが原因です。

自作ロガーを使うのをやめてみましょう。

import org.slf4j.LoggerFactory

class ClassA {
  private[this] val logger = LoggerFactory.getLogger(getClass)

  def doSomething(): Unit = {
    Logger.debug("DEBUG A")
    Logger.info("INFO A")
  }
}
// ClassB, ClassC も同様に修正

また、 logback.xml を以下のように書き換えます。

<!-- コメントアウト
  <logger name="LOGGER" level="debug">
    <appender-ref ref="STDOUT" />
  </logger>
-->

  <logger name="pkgB" level="info" />

  <root name="LOGGER" level="debug">
    <appender-ref ref="STDOUT" />
  </root>

mainを実行してみましょう。

[pkgA.ClassA#doSomething(Sample.scala:11)] ClassA - DEBUG A
[pkgA.ClassA#doSomething(Sample.scala:12)] ClassA - INFO A
[pkgB.ClassB#doSomething(Sample.scala:27)] ClassB - INFO B
[pkgB.ClassC#doSomething(Sample.scala:36)] ClassC - INFO C

期待する出力が得られました!

また、今度は ClassCINFO ログも不要になったとします。
logback.xml に以下を追加します。

  <logger name="pkgB.ClassC" level="warn" />
[pkgA.ClassA#doSomething(Sample.scala:11)] ClassA - DEBUG A
[pkgA.ClassA#doSomething(Sample.scala:12)] ClassA - INFO A
[pkgB.ClassB#doSomething(Sample.scala:27)] ClassB - INFO B

ClassC のログがなくなりました!

このように、ロガーインスタンスを使い分けることで柔軟なログ設計が可能になります。

おまけ: ロガーインスタンスの生成にscala-loggingを使う

scala-loggingStrictLogging を使うことでロガー生成のボイラープレートを削減できます。

import com.typesafe.scalalogging.StrictLogging

class ClassA extends StrictLogging {
  // ↓と同等のことを行ってくれる
  // private[this] val logger = LoggerFactory.getLogger(getClass)

  def doSomething(): Unit = {
    logger.debug("DEBUG A")
    logger.info("INFO A")
  }
}

そのほか、macroによってログ出力が有効になっているかチェックするif式で挟んでくれたり、
StringInterpolationによるログメッセージを {} によるフォーマットに書き換えてくれたりなど、細かな最適化を行ってくれます。

Scalaでslf4jを使っているならぜひscala-loggingを使いましょう。

まとめ

「ロガーを自分で実装するのはやめよう」というちょっと主語の大きいタイトルになってしまいましたが、
正確には「自作ロガーで単一のロガーインスタンスを使うのはやめよう」かもしれないです。

いずれにしろ、自作ロガーを自分で実装する必要がある際は、上記のデメリットを考慮して実装するのがよいでしょう。

用法用量を守って正しく自作ロガーを使いましょう!

2018年振り返り

できごと

  • 転職した
    • 趣味でScalaを書き始めてからScalaの楽しさに気づいていき、Scalaで仕事をできればいいなと思い始めるようになりました。
    • 前職での仕事で自分とのキャリア感とのギャップを感じて、ある日思い切ってTwitterに「転職したい」とつぶやきました。
    • 思った以上に反響があり、10社以上の方からお声がけいただきました。大変ありがたく思っております。
    • 日頃からGitHubにコード上げるなど活動していたのが大きかったかなと思っています。
  • ScalaMatsuriにスタッフとして初参加
    • @grimrose さんにお声がけいただいたのがきっかけでした。
    • こうしてコミュニティ活動に関わるのは初めてで、とても貴重な体験をさせていただきました。
    • Scalaコミュニティがもっと活発になることで、Scalaの素晴らしさをもっと広めていければいいなと思います。
  • ScalaMatsuriのアンカンファレンスで登壇
    • カンファレンスで登壇されている方々を見て「自分も何か喋りたい!」という思いから勢いでカンファレンスボードにお題を出していました。
    • 「AmmoniteによるScala Script入門」というタイトルで発表しました。
    • 前準備も特にしていなかったので、当日に資料を作ってアドリブで発表するということをやってました()
    • ニッチなネタというのもありましたが、良い反響をもらえて発表した甲斐があったと思います。
  • Scala関西Summitで登壇した
    • 「Readable Code in Scala」というタイトルで登壇しました。
    • Scalaコードの可読性というテーマで、自身なんでこんな難しいテーマにしてしまったのか疑問です()
    • 発表時間20分に対してスライドが50枚を超えていて、流石に収まりきらなかったです…
    • 最後の5枚ぐらいのスライドもはしょってしまい、一番伝えたいことが伝えられなかった…
    • イベント後のアンケートも正直良かったと言える内容ではありませんでした…
    • ただ、この失敗で得たものはとても大きかったです。やっぱり人間失敗しないと理解できない生き物なので…
    • 次回はよりよい発表ができるように頑張ります!
  • 初めてOSSにコントリビュートした
    • ドキュメントの修正ですが初めてOSSにコントリビュートできました。
    • その後はScalaまわりでライブラリのバージョンを上げたりなど簡単な修正をいくつか出したりしていました。
    • 今後は実装まわりの修正をPR出してみたいですね…
  • Mavenにライブラリを公開した
    • やってることは大したことないのですが、自分の書いたコードを初めて世界に公開することになりました。
    • まるで自分の子供のように感じるのが感慨深いです。
    • これからも自分が困ったことをベースに生み続けて、育てていければいいなと思っています。
    • GitHubのStarがほしい!!!!

よかった

  • 自力で自分のやりたい仕事に就くことができたこと
  • イベントでの登壇など、アウトプット量を増やすことができたこと
  • OSS活動の第一歩を踏み出せたこと

わるかった

  • 英語の勉強がまるで進まなかった
    • オンライン英会話に登録するも全然続いていない…
    • モチベーションを上手く保てる方法がなかなか見つからず…

2018年は飛躍の年にしたいと考えていたので、その点はとても成功したのかなと思います。

2019年は苦手意識のある自己表現に対して、アウトプット量を増やしつつ、その質をあげていければいいなと思っています。

良いお年をお迎えください!
来年もよろしくお願いします!

sentry-config v0.4.0 をリリースしました

github.com

sentry-config ってなにそれ

sentry-javaの設定をtypesafe-configでできるようにするライブラリです。

前回リリースしたv0.3.0の記事

変更点

一通りの設定項目をサポート

公式ドキュメントに記載のない設定項目を含め、sentry-javaが提供する一通りの設定項目をサポートしました。

application.conf

sentry {
  dsn = "noop://localhost/1"
  release = "0.1.0"
  dist = "x86"
  environment = "development"
  servername = "dev-server"
  tags {
    key1 = "value1"
    key2 = "value2"
  }
  mdctags = [
    "mdcTag1",
    "mdcTag2"
  ]
  extra {
    ex1 = "exValue1"
    ex2 = "exValue2"
  }
  stacktrace.app.packages = [
    "com.github.nomadblacky",
    "sample.foo"
  ]
  stacktrace.hidecommon = false
  sample.rate = 0.75
  uncaught.handler.enabled = false
  buffer {
    enabled = false
    dir = "./buffer"
    size = 100
    flushtime = 200000
    shutdowntimeout = 300000
    gracefulshutdown = false
  }
  async {
    enabled = false
    shutdowntimeout = 500
    queuesize = 200
    priority = 9
    threads = 3
    gracefulshutdown = false
  }
  http.proxy {
    host = "proxy.org"
    port = 8888
    user = username
    password = pass
  }
  compression = false
  maxmessagelength = 2000
  timeout = 3000
}

ユーザ独自のクライアント生成をサポート

com.github.nomadblacky.sentry.config.DefaultTypesafeConfigSentryClientFactory を継承して、イベント生成のヘルパなどを登録できるようになりました。

package sample;

import com.github.nomadblacky.sentry.config.DefaultTypesafeConfigSentryClientFactory;

public class MyCustomSentryClientFactory extends DefaultTypesafeConfigSentryClientFactory {
    @Override
    public SentryClient createSentryClient(Dsn dsn) {
        // TypesafeConfig を使ってクライアントを初期化
        SentryClient client = super.createSentryClient(dsn);
        client.addBuilderHelper(new CustomEventBuilderHelper());
        return client;
    }
}

public class CustomEventBuilderHelper implements EventBuilderHelper { 
    @Override
    public void helpBuildingEvent(EventBuilder eventBuilder) {
          // ...
    }
}
$ java -Dsentry.factory=sample.MyCustomSentryClientFactory

or

sentry.properties

factory=sample.MyCustomSentryClientFactory

その他

プロパティの上書きみたいなイケてないところがなくなりました。
sentry.factory の指定が必要でそこはダサいですがたぶんどうしようもない…


これで一応、このライブラリでやりきるところはやりきった感じがします。

依存ライブラリのアップデートがあれば追従する感じでしょうか。
でも、dependabotが便利なのでそこは楽々できそう。

まだ実プロダクトに導入してない()ので、仕事で使ってるプロジェクトに導入していきたいですね。
実際に使ってみると至らない点が色々見えてくるかもしれない。

…まぁ、ここまでやっておいて今更、 sentry.properties で事足りるのがほとんどだよね。という気がしているけど泣かない😂
typesafe-configを使ってるプロジェクトで、環境毎にSentryの設定をちょろっと変えたい…って時に役立つはずです、たぶん。

ライブラリで提供したい利便性をよく考えてから開発を始めましょう。