scribejavaでOAuthの認証を行いアクセストークンを取得する
この記事では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))
(ブラウザが開く)
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スクリプト
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
ScalaからAWS CDKを叩いてインフラを構築する
モチベーション
先日、TypeScriptとPython向けに AWS Cloud Development Kit がGAになりました。
現在GAではないのですが、JavaもこのAWS CDKの対象言語となっています。
じゃあ、JavaでできるならScalaでもできるよね?(決まり文句)
Scalaでインフラコードを書けるのは嬉しそう。
というわけでScalaからAWS CDKを叩けるか試してみました。
セットアップ
AWS CDKのインストール
npm install -g aws-cdk
Ammoniteのインストール
今回は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
スタックのデプロイと適用ができました!
実際にリソースが作成されているか確かめてみましょう。
確かに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
無事、IAMユーザとポリシーの設定ができました!
まとめ
ScalaでAWSのインフラ作成ができるようになりました。激アツですね!
TerraformのようなHCLの書き方を覚える必要がないですし、慣れた言語やIDEの恩恵を受けながらガツガツインフラを書けるのは夢がありそうです。
ただし、気になるところはいくつかあって、
- AWS以外のインフラも併せて作成する場合には素直にTerraformなどを使ったほうが良さそう。
- Javaのビルダーパターンによるリソースの定義が若干鬱陶しい
- AWS CDK Javaはまだ開発段階
- コンパイル時にリソース定義の誤り(必須パラメータが足りないなど)は検出されない。
- バリデーションに抜けがあって、スタックの適用時にエラーになる。
など、これからに期待という感じでしょうか。
ウッ… pic.twitter.com/wUSa7uYg8w
— blac_k_ey (@blac_k_ey) July 13, 2019
あーっ…こういうのコンパイルで弾いてほしかった… pic.twitter.com/Z6QM4OWWWh
— blac_k_ey (@blac_k_ey) July 13, 2019
ふぇぇ…バリデーションエラーどころかランタイムエラーでたよ… pic.twitter.com/imuisg6HNP
— blac_k_ey (@blac_k_ey) July 13, 2019
今回の成果物
余談
AWS CDK Javaはjsiiというライブラリ使ってJavaからTypeScriptのモジュールを呼び出しているらしいです。
Maven Central RepositoryからJarを取得して実行可能なスクリプトに変換する
以下は 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
scalafmtでfor式の `<-` と `=` をAlignする
Scalaコードをフォーマットするのによくscalafmtが用いられると思います。
自身は仕事/個人開発でよく下記の設定を使っています。(宣伝)
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.arrowEnumeratorGenerator
と align.tokenCategory
を設定する
most
の設定がどうなっているか見てみます。
val most: Align = more.copy( arrowEnumeratorGenerator = true, tokenCategory = Map( "Equals" -> "Assign", "LeftArrow" -> "Assign" ) )
more
の設定から align.arrowEnumeratorGenerator
と align.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
期待する出力が得られました!
また、今度は ClassC
の INFO
ログも不要になったとします。
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-loggingの StrictLogging
を使うことでロガー生成のボイラープレートを削減できます。
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年振り返り
できごと
- 転職した
- ScalaMatsuriにスタッフとして初参加
- ScalaMatsuriのアンカンファレンスで登壇
- カンファレンスで登壇されている方々を見て「自分も何か喋りたい!」という思いから勢いでカンファレンスボードにお題を出していました。
- 「AmmoniteによるScala Script入門」というタイトルで発表しました。
- 前準備も特にしていなかったので、当日に資料を作ってアドリブで発表するということをやってました()
- ニッチなネタというのもありましたが、良い反響をもらえて発表した甲斐があったと思います。
- Scala関西Summitで登壇した
- 初めてOSSにコントリビュートした
- Mavenにライブラリを公開した
- やってることは大したことないのですが、自分の書いたコードを初めて世界に公開することになりました。
- まるで自分の子供のように感じるのが感慨深いです。
- これからも自分が困ったことをベースに生み続けて、育てていければいいなと思っています。
GitHubのStarがほしい!!!!
よかった
- 自力で自分のやりたい仕事に就くことができたこと
- イベントでの登壇など、アウトプット量を増やすことができたこと
- OSS活動の第一歩を踏み出せたこと
わるかった
- 英語の勉強がまるで進まなかった
- オンライン英会話に登録するも全然続いていない…
- モチベーションを上手く保てる方法がなかなか見つからず…
2018年は飛躍の年にしたいと考えていたので、その点はとても成功したのかなと思います。
2019年は苦手意識のある自己表現に対して、アウトプット量を増やしつつ、その質をあげていければいいなと思っています。
良いお年をお迎えください!
来年もよろしくお願いします!
sentry-config v0.4.0 をリリースしました
sentry-config ってなにそれ
sentry-javaの設定をtypesafe-configでできるようにするライブラリです。
変更点
一通りの設定項目をサポート
公式ドキュメントに記載のない設定項目を含め、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の設定をちょろっと変えたい…って時に役立つはずです、たぶん。
ライブラリで提供したい利便性をよく考えてから開発を始めましょう。