WindowsでGradleのbootRunがCreateProcess error=206になるときの対応方法と、Kotlinで記述している場合の注意点

開発端末がmacOSだけのプロジェクトに、ひとりWindowsアサインされた。

プロジェクト管理がGradle、フレームワークがSpring Boot、開発言語がJavaまたはKotlinだったので、さっそく gradlew.bat bootRun すると、 A problem occurred starting process 'command '...\java.exe'' というエラーが発生。

Gradle + Spring Bootではよくあることなので、対応方法をメモ。

さらに、JavaではなくKotlinで開発しているプロジェクトでは、別途問題が発生したので、そちらについてもメモ。

環境

Windows 10 64bit Pro Version 2004。

Windows(Win32 API)起因の問題のため、Gradle, Spring Boot, JavaやKotlinのバージョンは関係ないと思われる。

問題

対象のプロジェクトで gralde bootRun すると、以下のエラーが発生する。

* What went wrong:
Execution failed for task ':bootRun'.
> A problem occurred starting process 'command '${java.exeのパス}''

gradle bootRun -i -s でログとスタックトレースを出力すると、以下のようになる( Starting process の出力は改行している)。

> Task :bootRun FAILED
Excluding [com.google.protobuf:protobuf-java]
Task ':bootRun' is not up-to-date because:
  Task has not declared any outputs despite executing actions.
Starting process 'command '${java.exeのパス}''.
 Working directory: ${projectDir}
 Command: ${java.exeのパス} ${-Dオプション...}
 -cp ${依存JARなどへのパス...}
 ${@SpringBootApplication を付けたクラスの完全修飾クラス名}
:bootRun (Thread[Task worker for ':' Thread 3,5,main]) completed. Took ... secs.
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':bootRun'.
> A problem occurred starting process 'command '${java.exeのパス}''
* Try:
Run with --debug option to get more log output. Run with --scan to get full insights.
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':bootRun'.
  // 中略
Caused by: org.gradle.process.internal.ExecException: A problem occurred starting process 'command '${java.exeのパス}''
  // 中略
Caused by: net.rubygrapefruit.platform.NativeException: Could not start '${java.exeのパス}'
  // 中略
Caused by: java.io.IOException: Cannot run program "${java.exeのパス}" (in directory "${projectDir}"): CreateProcess error=206, ファイル名または拡張子が長すぎます。
    at net.rubygrapefruit.platform.internal.DefaultProcessLauncher.start(DefaultProcessLauncher.java:25)
    ... 7 more
Caused by: java.io.IOException: CreateProcess error=206, ファイル名または拡張子が長すぎます。
    ... 8 more

最も深い例外は「CreateProcess error=206, ファイル名または拡張子が長すぎます。」。

コマンドプロンプトだけでなく、PowerShellやGit Bashから実行しても同様のエラーが発生する。

原因

-cp に大量のJARなどのパスが渡されることで、Win32 APICreateProcess 関数に渡せる文字列の、最大長を超過している模様。エラーになった時のコマンド全体の文字列長は、約57,000文字だった。

詳細は確認していないが、 What is the command line length limit? - The Old New Thing によると、文字数は最大32,767文字の模様。

The maximum command line length for the CreateProcess function is 32767 characters. This limitation comes from the UNICODE_STRING structure. CreateProcess is the core function for creating processes, so if you are talking directly to Win32, then that’s the only limit you have to worry about.

UNICODE_STRING (subauth.h) - Win32 apps | Microsoft Docs を見ると、バイト長を最大65,535の USHORT で保持しているので、1文字2バイトの小数部切り捨てで32,767文字かな?

対応

検索すると、 GRADLE_USER_HOME の位置を変えてパスを短くする、 pathingJar タスクを定義するなど、いろいろ対応方法が出てくる。

この問題解決用のGradle Plugin、 com.github.ManifestClasspath があるので、今回はそれを利用し、クラスパスを単一のJARファイルにまとめることで実行できるようにする。(プラグインの実装は確認していないが、AntのManifestclasspathタスクと同じことをしているのかな?)

ManifestClasspathプラグインのインストール、設定

Gradle Plugin PageGitHubにインストール方法が記述されている。

今回のプロジェクトでは、Gradle設定はGroovyで記述されていたため、 build.gradle に以下を記述。なお、 @SpringBootApplication を付与したクラスの完全修飾クラス名は、 hepokon365.Application としておく。

buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.github.viswaramamoorthy:gradle-util-plugins:0.1.0-RELEASE"
  }
}

apply plugin: "application" // mainClassName を有効化するため
apply plugin: "com.github.ManifestClasspath"
mainClassName = "hepokon365.Application"

この状態で gradle bootRun -i すると、 -cp ${projectDir}\build\mfjars\bootRun_ManifestJar.jar となり、引数が短くなっているのが確認できる。

コマンド全体の文字列長も約4,000文字となり、開発言語がJavaの場合は bootRun で起動可能となった。

Gradle Kotlin DSLの場合

Kotlin DSLの場合は以下の用意に記述する。

plugins {
    application
    id("com.github.ManifestClasspath") version "0.1.0-RELEASE"
}

application {
    mainClassName = "hepokon365.Application"
}

開発言語がKotlinの場合に発生しうる問題

Javaであればめでたしめでたしだが、開発言語がKotlinのプロジェクトでは、前述の対応をして bootRun すると、「エラー: メイン・クラス hepokon365.Application が見つからなかったかロードできませんでした」が発生。

対象のクラスが記載された Application.kt ファイルを見ると、Kotlin + Spring Bootの公式チュートリアルの記載と同様、以下のように記述されている。

package hepokon365

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

Kotlinでメインクラスが見つからない場合の対応

前述の build.gradle の設定では、 Application.main を実行しようとしているが、Kotlinでクラス外関数を記述した場合、 ${ファイル名}Kt クラスのstaticメソッドとしてコンパイルされていたのが原因。

gradle bootRun がプロセス実行エラーになっている際、 -i オプションでログを出していると、コマンドの末尾のメインクラス名がKt付きになっているのが確認できる。

対応方法としては、単純に build.gradlemainClassName に設定するクラス名を ${ファイル名}Kt に変更するか、 companion objectメンバ関数として main を宣言し、 @JvmStatic アノテーションを付与すればいい。

@SpringBootApplication
class Application {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            runApplication<Application>(*args)
        }
    }
}

既存環境への影響を考え、今回は mainClassName の変更で対応。

mainClassName = "hepokon365.ApplicationKt"

ちなみに、 @file:JvmMultifileClass@file:JvmName("Application") アノテーションでmain関数もApplicationクラスで実行できないかと思ったが、クラスが重複しているということでコンパイルエラーに。

振り返り

対応できなければmacを使わされるところだった、あぶないあぶない。

何回か書いている気もするが、Spring Boot使うときは、GradleよりMavenを使うのが無難だと思う。

あとは、Project Jigsawによるモジュール機能を使えば、よしなにできるのかな?

備考

Grailsフレームワークとして使用している場合は、 pathingJar が設定できる模様。

qiita.com

GradleでprocessResources時にシンボリックリンクを作成(Windows, macOS対応)

Spring Bootを使ったJavaプロジェクトで、 bootRun をしてから起動するまで数分かかっていた。

調べてみると、 processResources タスクで src/main/webapp/WEB-INFbuild/build/resources/main/WEB-INF にコピーしていたが、 src/main/webapp/WEB-INF 配下がNodeパッケージとなっており、node_modules配下のファイルコピーで時間がかかっていた。

取り急ぎ、シンボリックリンクを作って対応したのでメモ。mklinkについて調べたのはこのため。

環境

Java v8.262.10, Gradle v6.3。

Windows 10およびmacOSで動作確認済み。

シンボリックリンクの作成

build.gradle に以下を追加。

Windowsの場合、開発者モードを有効化するか管理者権限で mklink を実行しないとシンボリックリンクが作れないため、PowerShell経由で管理者権限で起動したコマンドプロンプトで、 mklink を実行している。

import java.nio.file.Files

final def isWindows = System.getProperty('os.name')
    .toLowerCase()
    .contains('windows')
final def srcDir = 'src/main/resources/WEB-INF'

task createWebInfSymbolicLink(type: Exec) {
  // Windowsの場合、スラッシュをバックスラッシュに置換
  // FilenameUtils#separatorsToSystem(String) などでもいい
  def normalizePath = { isWindows ? it.replaceAll('/', '\\\\') : it }
  def destDir = "${projectDir}/build/resources/main/WEB-INF"
  def destDirFile = destDir as File
  def normalizedSrcDir = normalizePath("../../../${srcDir}")

  if (destDirFile.directory) {
    def destDirPath = destDirFile.toPath()

    // シンボリックリンクかを判定
    if (Files.isSymbolicLink(destDirPath)) {
      // srcへのシンボリックリンクがあれば作業不要。
      // ExecタスクではExecSpecを返す必要があるため、適当にechoする。
      def destLink = Files.readSymbolicLink(destDirPath)
      if (normalizedSrcDir == destLink.toString()) {
        def executer = isWindows ? ['cmd', '/c'] : []
        return commandLine(executer + ['echo', 'exists link'])
      }

      // シンボリックリンクでも、パスが違っていれば削除
      destDirFile.delete()
    } else {
      // 実ファイルであればディレクトリを削除
      destDirFile.deleteDir()
    }
  }

  // Windows以外であれば、lnでシンボリックリンクを作成
  if (!isWindows) {
    return commandLine('ln', '-s', "${normalizedSrcDir}", "${destDir}")
  }

  // Windowsの場合、コマンドプロンプトからPowerShellを起動し、
  // さらにPowerShellから管理者権限のコマンドプロンプトでmklinkを実行。
  // 直接PowerShellを起動しても大丈夫な気がするが、
  // やったことないのでとりあえずコマンドプロンプト経由で。
  def arguments = [
      '/c', 'mklink', '/d', normalizePath(destDir), normalizedSrcDir
  ]
  commandLine 'cmd', '/c', 'powershell',
      '-Command', '"Start-Process -Verb Runas"',
      '-FilePath', '"cmd"',
      '-ArgumentList', arguments.collect { /"${it}"/ }.join(',')
}

// processResources の後に実行するよう設定
processResources.finalizedBy createWebInfSymbolicLink

振り返り

mklinkシンボリックリンクを作ろうとすると、やはり権限が必要になるのがネック。

今回の方法では管理者権限が必要になるので、 gradle clean などを実行した後だとUACのポップアップが出てしまう。

ディレクトリジャンクションを使って、管理者権限なしで実行できそうなので、そのうち改良したい。

Windowsでシンボリックリンクを作るにはmklinkが使えるし、管理者権限なしでフォルダにリンクを張りたいならジャンクションでいいかも

会社で「Windowsだとシンボリックリンクが作れない」という話になった。 mklink で作れると話したら驚かれたのでメモ。

導入が Windowsでシンボリックリンクを作る | Developers.IO と全く同じだなぁ...

環境

Windows 10 Pro 64bit v1909、コマンドプロンプト Version 10.0.18363.1139。

docs.microsoft.com

mklink [オプション] <link> <target> で、シンボリックリンクなどのリンクを作成する。

link はリンク作成先、 target はリンクが指す実際のファイル/ディレクトリパスを、絶対パスまたは相対パスで指定する。

オプションで作成するリンクの種類が異なり、それぞれでファイルまたはディレクトリの指定可否が決まる。

オプション 作成されるリンク target指定先
未指定 ファイルのシンボリックリンク ファイル
/d フォルダのシンボリックリンク ディレクト
/h ハードリンク ファイル
/j ディレクトリジャンクション ディレクト

targetに相対パスを指定する際の注意

target相対パスを指定する場合、オプションによって、パスの起点となるディレクトリ位置が異なる。

未指定または /dシンボリックリンクを作成する場合は、 ln と同様、 link からの相対パスとなる。

/h でハードリンクを作成、または /jディレクトリジャンクションを作成する場合、コマンド実行しているカレントディレクトリからの相対パスとなる。

リンクを作成するディレクトリをカレントディレクトリとすれば、どの方法でも同じ相対パスで指定できる。

コマンド例

以下のディレクトリ構成で、 link ディレクトリ直下にリンクを作る場合。

C:\root
  ┣ target
  ┃  ┗ src
  ┃    ┗ exp.txt
  ┗ link
root直下でコマンド実行
リンク種類 コマンド
ファイルのシンボリックリンク mklink link\symlink_file ..\target\src\exp.txt
フォルダのシンボリックリンク mklink /d link\symlink_dir ..\target\src
ハードリンク mklink /h link\hardlink.txt target\src\exp.txt
ディレクトリジャンクション mklink /j link\junction target\src
link直下でコマンド実行
リンク種類 コマンド
ファイルのシンボリックリンク mklink symlink_file ..\target\src\exp.txt
フォルダのシンボリックリンク mklink /d symlink_dir ..\target\src
ハードリンク mklink /h hardlink.txt ..\target\src\exp.txt
ディレクトリジャンクション mklink /j junction ..\target\src

リンクの表示と確認

エクスプローラーでの表示

ファイルのシンボリックリンクは、ファイルの種類が「.symlink」、ファイルサイズが0になる。アイコンはファイルへのショートカットと同様。

また、ディレクトリのシンボリックリンクディレクトリジャンクションは、アイコンはフォルダへのショートカットと同じだが、ファイルの種類は「ショートカット」ではなく「ファイル フォルダー」となる。

f:id:hepokon365:20201024204000p:plain
コマンド例で作成したリンクの、エクスプローラーでの表示

dirコマンドでの表示

dir コマンドでの属性には、以下のように表示される。

また、シンボリックリンクディレクトリジャンクションには、リンク先のファイルパスが表示される。

C:\root\link>dir /o:gn
YYYY/MM/DD  HH:MI    <DIR>          .
YYYY/MM/DD  HH:MI    <DIR>          ..
YYYY/MM/DD  HH:MI    <JUNCTION>     junction [C:\root\target\src]
YYYY/MM/DD  HH:MI    <SYMLINKD>     symlink_dir [..\target\src]
YYYY/MM/DD  HH:MI       %FILE_SIZE% hardlink.txt
YYYY/MM/DD  HH:MI    <SYMLINK>      symlink_file [..\target\src\exp.txt]

相対パスでリンクを作成すると、シンボリックリンクの場合は ln と同様、相対パスを保持しているが、ディレクトリジャンクションの場合は絶対パスに変換される模様。

相対パスで作成したシンボリックリンクを移動すると、移動先からの設定されている相対パスと同名のファイル/ディレクトリがない限りリンクを開けなくなるが、ディレクトリジャンクションを移動しても、リンク先のパスが変わらなければ、問題なく開くことができる。

lsコマンドでの表示

Git Bashから ls コマンドを使うと、シンボリックリンクディレクトリジャンクションには、Linuxなどでの実行結果と同様、ファイル名の後ろに @ がつく。

また、 ls -l で、シンボリックリンクディレクトリジャンクションにはリンク先のパスが表示される。

ディレクトリジャンクションは、絶対パス指定のディレクトリのシンボリックリンク扱いの模様。

$ ls -1 --group-directories-first
junction@
symlink_dir@
hardlink.txt
symlink_file@

$ ls -l --group-directories-first
total 1
lrwxrwxrwx 1 ... 18 ... junction -> /c/root/target/src/
lrwxrwxrwx 1 ... 13 ... symlink_dir -> ../target/src/
-rw-r--r-- 2 ...  1 ... hardlink.txt
lrwxrwxrwx 1 ... 21 ... symlink_file -> ../target/src/exp.txt

リンクの削除

作成されたシンボリックリンクや、ディレクトリジャンクションに対し、 unlink のような削除用コマンドは用意されていない。

通常のファイルやディレクトリと同様、 rd/rmdirdel で削除可能。エクスプローラーからも削除できる。

ln コマンドとの違い

ln コマンドは ln [OPTION]... [-T] TARGET LINK_NAME なので、 mklink とは linktarget の順序が逆になる。

lnTARGET相対パス指定する際は、カレントディレクトリではなく LINK_NAME からの相対パスにする必要があるので、 mklink のほうがわかりやすい気もする。

一方、絶対パス指定であれば ln -s src dest で指定できるので ln のほうがシンプルか。

また、デフォルトではハードリンクが作成されるのも mklink と異なる(あまり ln をハードリンク作成に使うこともないとは思うが)。

注意点

シンボリックリンクの作成権限

オプション未指定、または /dシンボリックリンクを作成する場合、デフォルトでは権限がないため、 この操作を実行するための十分な特権がありません。 というエラーが発生する。

シンボリックリンクを作成するには、コマンドプロンプトを管理者権限で実行して実行するか、開発者モードを有効とする必要がある模様。

開発者モードを有効化するには、Windows10の「設定」>「更新とセキュリティ」>「開発者向け」から「開発者モード」を選択し、再起動する。

f:id:hepokon365:20201024170258p:plain

コマンドプロンプトからしか実行できない

PowerShellや、Git for Windowsに付属するGit Bashから実行しようとしても、エラーとなる。

PS C:\Users\hepokon365> mklink
mklink : 用語 'mklink' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されま
せん。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してく
ださい。

exeにパスを通そうと思って where mklink しても、ファイルパスが見つからない。

C:\Users\hepokon365>where mklink
情報: 与えられたパターンのファイルが見つかりませんでした。

どうやら、 cmd.exe の組み込みコマンドの模様。

例えばPowerShellから実行する場合、 Start-Process cmd "/c mklink ..."コマンドプロンプトを起動し、コマンドとして渡してやればいい。この時、 -Verb Runas オプションを使用すれば、管理者権限でコマンドプロンプトの実行が可能。

Start-Process -Verb Runas cmd "/c mklink /d <link> <target>"

なお、Git Bashでは ln が使えるが、 -s オプション付きで実行しても、ファイルコピーが行われるだけの模様。

振り返り

シンボリックリンクが作れるとはいえ、管理者権限または開発者モードを有効にしないと使えなかったりと制限が多い。

ディレクトリのシンボリックリンクを作る用途で、かつ作成後にターゲットディレクトリを移動することがないのであれば、 /j によるディレクトリジャンクションリンクが権限不要で同じように使えるため、そちらを検討してもよさそう。

従業員数に応じた企業規模の分類(SMB,MM,GBなど)とその正式名称(たぶん)

IR資料などでたまに見かける、「SMB」といった従業員数に応じた企業の分類と、その正式名称が毎回わからなくなるのでメモ。

企業分類の例

ビズリーチSalesforceの記事に、分類とそれぞれの企業規模が記載されている。

www.bizreach.jp

従業員数4,000名未満のお客様を対象とし、1~14名(ESB)、15名~99名(SMB)、100~499名(MM)、500~4,000名未満(GB)と従業員数ごとで分類

この分類は、Salesforceによるものの模様。Salesforceを利用している会社では、この分類をそのまま使用することが多いようで、IR資料などでちょくちょく見かける。

分類ごとのSalesforceにおける人数と正式名称(仮)

正式名称は、それっぽいのをピックアップしただけなので、間違っているかもしれない。

ESB

「Entrepreneurship and Small Business」の略称。15人未満の企業を指す。

起業直後くらいのイメージかな?

SMB

「Small and Medium Business」の略称。100人未満の、いわゆる中小企業を指す。

MM

「Mid Market」の略称。500人未満の企業を指す。日本語では、中堅企業のイメージか。

Wikipediaにそれらしいページがあった。

Middle-market company - Wikipedia

Definitions of the middle market are generally derived by dividing the United States economy into three categories: small business, middle-market, and big business.

ということで、中小企業と大企業の中間を指す模様。海外では一般的な分類なのかな?

GB

「General Business」の略称。4000人未満の企業を指す。日本語では、準大手企業のイメージか。

EBU

「Enterprise Business Unit」の略称。4000人以上の企業を指す。ここまで行くと、まごうことなき大企業だろう。

その他の分類

この分類は、Salesforceによるものの模様。会社によっては、分類が異なったり、同じ分類でも従業員数の閾値が異なる。

以下を見ると、ガートナー社ではGBは1000人未満らしい(未確認)。

makitani.net

  • 中堅企業(General Business):従業員数1000未満500人以上の企業
  • 中規模企業(Mid-Market Business):従業員数500人未満100人以上の企業
  • 小規模企業(Small Business):従業員数100人未満の企業

また、この「中堅企業」「中規模企業」「小規模企業」といった従業員数1000未満の企業を「Commercial Business Unit (CBU)」と称し、それより規模の大きい企業を「Enterprise Business Unit (EBU)」と分類するところもある。

といった記載もある。「分類するところ」がどこなのかわからないが。

振り返り

それぞれの略称でWeb検索すると、海外の企業の採用ページなどが出てくる。いくつか見てみたが、従業員数の閾値は規模が大きくなるほどバラつきがあった。

ガートナーのGBが1000人未満を指しているとすれば、SalesforceのGBとは最大で4倍の従業員数差が生じる。

自社内でのやり取りや、Salesforceの利用者間でのやり取りなど、それぞれの前提がそろっていればいいが、そうでない場合はどの程度の規模や従業員数をどう分類するかをすり合わせておかないと、齟齬が出そうだと思った。

余談

「GB」が最も検索しずらかった。「Salesforce GB」で検索すると、ストレージ容量の話しか出ないんだもの...

あとは、なんで「MM」だけ「Business」がつかないんだろう?

Javaでinstanceofの代わりに使えるClassクラスのメソッドはisInstanceとisAssignableFromだというメモ

タイトル通り。

使おうと思うたびに「 instanceof 演算子の代わりに使えるメソッドなんだっけ?」となるのでメモ。

環境

OpenJDK 8 8.262.10。

それぞれの使い方

instanceof 演算子

instanceOf ではない。

インスタンス(参照型) instanceof クラス/インターフェース名 と記述。

以下の条件に該当すれば true 、該当しなければ false を返す。

  • 右辺がクラス名の場合、左辺が右辺で指定したクラス、またはサブクラスのインスタンス
  • 右辺がインターフェース名の場合、左辺が右辺で指定したインターフェース、またはそのサブインターフェースを実装している

後述のClassクラスのメソッドの説明を借りると、「左辺が右辺と代入互換の関係にあるか」を判定する。

地味に便利なのは、左辺のインスタンスnull でも例外は発生せず、 false を返してくれる。

Object str = "str";
assert str instanceof String;
assert str instanceof CharSequence;
assert str instanceof Object;

Object sb = new StringBuilder("sb");
assert !(sb instanceof String);
assert sb instanceof CharSequence;
assert sb instanceof Object;

Object nil = null;
assert !(nil instanceof String);
assert !(nil instanceof CharSequence);
assert !(nil instanceof Object);

Class#isInstance(Object)

Class<?>.isInstance(インスタンス) のように記述。

instanceof 演算子とは、左辺と右辺が逆になる。

Javadoc

指定されたObjectが、このClassが表すオブジェクトと代入互換の関係にあるかどうかを判定します。このメソッドは、Java言語のinstanceof演算子と動的に等価です。

こちらも、引数として渡すインスタンスnull でも、例外は発生しない。

Object str = "str";
assert String.class.isInstance(str);
assert CharSequence.class.isInstance(str);
assert Object.class.isInstance(str);

Object sb = new StringBuilder("sb");
assert !String.class.isInstance(sb);
assert CharSequence.class.isInstance(sb);
assert Object.class.isInstance(sb);

Object nil = null;
assert !String.class.isInstance(nil);
assert !CharSequence.class.isInstance(nil);
assert !Object.class.isInstance(nil);

Class#isAssignableFrom(Class<?>)

Class<?>.isAssignableFrom(Class<?>) のように記述。こちらはインスタンスではなく、 Class を引数にとる。

Javadoc

このClassオブジェクトが表すクラスまたはインタフェースが、指定されたClassパラメータが表すクラスまたはインタフェースと等しいかどうか、あるいはそのスーパー・クラスあるいはスーパー・インタフェースであるかどうかを判定します。

instanceofClass#isInstance(Object) との違いとして、 null を渡すと NullPointerException が発生する。

Class<?> strClass = String.class;
assert String.class.isAssignableFrom(strClass);
assert CharSequence.class.isAssignableFrom(strClass);
assert Object.class.isAssignableFrom(strClass);

Class<?> sbClass = StringBuilder.class;
assert !String.class.isAssignableFrom(sbClass);
assert CharSequence.class.isAssignableFrom(sbClass);
assert Object.class.isAssignableFrom(sbClass);

Class<?> objClass = Object.class;
assert !String.class.isAssignableFrom(objClass);
assert !CharSequence.class.isAssignableFrom(objClass);
assert Object.class.isAssignableFrom(objClass);

try {
    Object.class.isAssignableFrom(null);
    assert false;
} catch (NullPointerException e) {
    assert true;
} catch (Exception e) {
    assert false;
}

引数として渡すインスタンスnull でなければ、 Class.isInstance(obj) == Class.isAssignableFrom(obj.getClass()) となる。

注意点

Class#isInstance(Object) に、 Class を渡してもコンパイルエラーにならない。

昔、それが原因のバグを修正する羽目になったので、それからは null チェックしてから getClass() して Class#isAssignableFrom を使うのが好み。

振り返り

レガシーなコードに手を入れている時、 instanceofif else で5個くらいつながっていたので、書き直そうと思ったが、どうしても isAssignableFrom が思い出せなかった。

老化がヤバい。

Groovyの名前付き引数(パラメータ)について

Groovy + Spockでユニットテストを書いているが、Groovyの名前付き引数・名前付きパラメータを知らないメンバーがいた。

なかなか特殊な仕様なのでメモ。

環境

サクッとGroovy Web Consoleで確認。

println System.getProperty('java.version')
println GroovySystem.version

してみたところ、

1.8.0_181-google-v7
2.5.7

だった。

仕様

ドキュメントにはサラッとした記述しかない。

昔は Named argument と呼んでいたが、今見ると Named parameters と呼ばれている。

いつから変わったのかと思ったら、 v2.5.3までは Named argumentv2.5.4Named parameters になった模様。

コンストラクタの名前付き引数

デフォルトコンストラクタが存在するか、第1引数にMapを取るコンストラクタが存在する場合、フィード名をキーとしたMapを渡すことで、名前付き引数として扱える。

また、GroovyでのMapリテラル[key: value] だが、名前付き引数呼び出し時であれば、 [] で囲む必要がなくなる。

// デフォルトコンストラクタの場合
@groovy.transform.ToString
class ExampleClass1 {
  Integer id
  String name
}

assert new ExampleClass1().toString() == 'ExampleClass1(null, null)'
assert new ExampleClass1(name: 'name').toString() == 'ExampleClass1(null, name)'
assert new ExampleClass1(id: 1).toString() == 'ExampleClass1(1, null)'
assert new ExampleClass1(name: 'name', id: 1).toString() == 'ExampleClass1(1, name)'

// Mapを引数に取るコンストラクタの場合
@groovy.transform.ToString
class ExampleClass2 {
  Integer id
  String name

  // デフォルト値として空のMapを指定するか、Nullセーフ演算子を使わないと、引数省略時にNullPointerExceptionが発生する
  ExampleClass2(Map parameters) {
    this.id = parameters?.id
    this.name = parameters?.name
  }
}

assert new ExampleClass2().toString() == 'ExampleClass2(null, null)'
assert new ExampleClass2(id: 1, name: 'name').toString() == 'ExampleClass2(1, name)'

メソッドやクロージャの名前付き引数

メソッドやクロージャの場合も、第1引数にMapを取れば名前付き引数となる。

名前付き引数呼び出し時であれば、 [] が省略できるのもコンストラクタと同様。

// デフォルト値として空のMapを指定するパターン
def ExampleMethod(Map parameters = [:]) {
  return [parameters.id, parameters.name]
}

assert ExampleMethod() == [null, null]
assert ExampleMethod(id: 1) == [1, null]
assert ExampleMethod(name: 'name') == [null, 'name']
assert ExampleMethod(id: 1, name: 'name') == [1, 'name']

注意点

第1引数にMapを取る場合、デフォルト値として空のMapを指定しても、明示的に null を渡されると参照時に NullPointerException が発生する。

null が渡される可能性があれば、Mapの値の参照は、常にNullセーフ演算子 ?. で行っておくのが無難。

名前付き引数のデフォルト値の設定

第1引数にMapを取る場合、デフォルト値としてMapを設定し、そこにkeyとvalueを指定すると、引数省略時はそのMapおよびkeyとvalueが使用される。

だが、引数の一部を指定した場合、未指定の引数はnullになる。

@groovy.transform.ToString
class ExampleClass {
  Integer id
  String name

  ExampleClass(Map parameters = [id: 0, name: 'default']) {
    this.id = parameters.id
    this.name = parameters.name
  }
}

// 引数省略時はうまくいったように見える
assert new ExampleClass().toString() == 'ExampleClass(0, default)'

// 引数をすべて指定した場合も問題なし
assert new ExampleClass(id: 1, name: 'name').toString() == 'ExampleClass(1, name)'

// 引数の一部を指定した場合、未指定の引数はnullになる
assert new ExampleClass(id: 1).toString() == 'ExampleClass(1, null)'
assert new ExampleClass(name: 'name').toString() == 'ExampleClass(null, name)'

引数の一部を指定した場合でも、未指定の引数にデフォルト値を設定したい場合、コンストラクタやメソッド内でデフォルト値を設定したMapを用意し、そこに第1引数のMapをputAllするのがよさそう。

def ExampleMethod(Map parameters) {
  def params = [id: 0, name: 'default'] + (parameters ?: [:])
  return [params.id, params.name]
}

assert ExampleMethod() == [0, 'default']
assert ExampleMethod(id: 1) == [1, 'default']
assert ExampleMethod(name: 'name') == [0, 'name']
assert ExampleMethod(id: 1, name: 'name') == [1, 'name']

名前付き引数と通常の引数の組み合わせ

第2引数以降に、通常の変数を宣言することも可能。

その場合、 key: value 形式で指定した引数は第1引数のMapに、それ以外の引数は第2引数以降に指定順に渡される。

必須としたい引数は第2引数以降で宣言し、必須ではない引数は名前付き引数にする、といったことが可能。

ただし、第1引数のMapにはデフォルト値を指定しておかないと、名前付き引数が1つも渡されていない場合、 MissingMethodException が発生する。

Groovyの仕様では、引数のデフォルト値を指定すると、通常はそれ以降の引数にもデフォルト値を設定する必要があるが、この名前付き引数と通常の引数の組み合わせの場合、第2引数以降はすべて必須パラメータとなり、デフォルト値を指定できない(指定しても、省略してメソッドを呼び出せない)模様。

そのため、第2引数以降を省略したければ、メソッドをオーバーロードする必要がある。

def ExampleMethod(Map parameters = [:], int id, String name) {
  return [id, name, parameters.address, parameters.email]
}

def ExampleMethod(Map parameters = [:], int id) {
  return ExampleMethod(parameters, id, 'default')
}

assert ExampleMethod(1) == [1, 'default', null, null]
assert ExampleMethod(1, 'name') == [1, 'name', null, null]
assert ExampleMethod(address: 'tokyo', email: 'mail@example.com', 1) ==[1, 'default', 'tokyo', 'mail@example.com']

// 名前付き引数の宣言順は不定
assert ExampleMethod(
    email: 'mail@example.com',
    1,
    'name',
    address: 'tokyo'
) == [1, 'name', 'tokyo', 'mail@example.com']

ここの挙動について、ドキュメントが見つけられなかった。動きから逆算しているので、仕様とは説明が異なるかもしれない。

振り返り

いつの間にやら「名前付き引数と通常の引数の組み合わせ」を使っていたが、どういった経緯でこの方法を知ったかは思い出せない。

機能名としては「名前付き引数」で通したが、ドキュメントの英語からだと「名前付きパラメータ」が正式名称になるのかな?

Gitログからチームを可視化するツール、gilotの各種コマンド(log,plot,info,hotspot,hotgraph,author)をWindowsで試してみた

前回の続き。

試していなかった、 info , hotspot , hotgraph , author といったコマンドを試してみる。

環境

gilotのアップデート

前回試してからgilotをアンインストールしていたため、インストールしようとしたらエラー。

pip install gilot
    (中略)
    Running setup.py install for python-Levenshtein ... error
    ERROR: Command errored out with exit status 1:
    (中略)
    error: Microsoft Visual C++ 14.0 is required. Get it with "Build Tools for Visual Studio": https://visualstudio.microsoft.com/downloads/

どうも、前回試した後に、 Microsoft Visual C++ Build Tools をアンインストールしていたらしい。それぞれインストールして成功、gilotのバージョンは0.2.4。

cinst microsoft-visual-cpp-build-tools
pip install gilot
    (中略)
Successfully installed argparse-1.4.0 datetime-4.3 gilot-0.2.4 python-Levenshtein-0.12.0 python-louvain-0.14 zope.interface-5.1.0

また、後述する author コマンドによる出力で使用されるため、

IPAゴシック Ver.003.03をインストールしておくといい。

対象のリポジトリ

前回と同様、以降のコマンドはGit Bashで実行する。

試しに、 vuejs/vue リポジトリを対象にしてみる。

git clone https://github.com/vuejs/vue.git

logコマンド

gilot log Gitリポジトリパス でログ出力するのは前回と同じだが、 hotspothotgraph では、 --full オプションを付けて出力する必要がある。

--full を付けて出力しても、 plot によるグラフ出力ができなくなったりはしないため、とりあえず付けておくのが良さそう。

デフォルトブランチの dev を対象として、出力を実行。

gilot log --full --branch dev -o log-vue.csv vue/

あまり量が出ない。プルリクエストガイドラインによると、マージ前にsquashしているからか。

デフォルトでは直近6か月分が出力されるが、--month 整数 オプションで月数を変更したり、 --since YYYY-MM-DD--until YYYY-MM-DD で開始日・終了日を指定できる。

トータルで3,000コミット強程度のため、最初のコミットまで遡ってみる。日付は 11 Apr 2016 のため、 --since 2016-04-01 を指定。終了日も指定しないと、6か月後の2016/10/1までになるため、 --until 2020-09-01 も指定しておく。

gilot log --full --since 2016-04-01 --until 2020-09-01 --branch dev -o log.csv vue/

4分程度で出力完了。

解析コマンド共通オプション

共通で以下のオプションがある。詳細は、各コマンドのヘルプ参照。

オプション名 概要
-i ,
--input
logの出力結果をファイル出力している場合、そのパスを指定。
--allow-files 解析対象に含める、Gitリポジトリ上のファイルパス。
JavaMavenプロジェクトであれば 'src/main/*' を指定するなど。
--ignore-files 解析対象から除外する、Gitリポジトリ上のファイルパス。
--allow-files で指定したファイルパスから、特定のファイルを除外することができる。

いずれも、スペース区切りで複数記述したり、 *ワイルドカードとして使用できる。

実際のプロジェクトに対して実行する場合、ユニットテストを除外したりする必要がありそう。

plotコマンド

gilot plot で、logの結果をプロットした画像に変換できる。デフォルトでのタイムスロットは2週間ごとだが、4年半分と期間が長いため、 -t 1m で1か月単位にしてみる。

gilot plot -i log.csv -o plot.png -t 1m

結果は以下。

f:id:hepokon365:20200904000445p:plain
plot結果

infoコマンド

gilot info で、plotのグラフ生成元となる情報をJSONで出力可能。 -o オプションはないので、必要があればファイルにリダイレクトして出力する。 -t でタイムスロットは指定可能なので、以下のコマンドを実行。

gilot info -i log.csv -t 1m > info.json

結果は以下、長いので折り畳み。どうやら、タイムスロットの指定が効いていない模様。

info結果

{
    "gini": 0.7823790383228998,
    "output": {
        "lines": 922130,
        "added": 165974,
        "refactor": 0.8200101937904634
    },
    "since": "2016-04-11T11:47:28.000000000",
    "until": "2020-08-20T22:10:59.000000000",
    "timeslot": "2 Weeks",
    "insertions": {
        "mean": 4730.886956521739,
        "std": 9466.989587800872,
        "min": 0.0,
        "25%": 33.0,
        "50%": 195.0,
        "75%": 4341.0,
        "max": 53336.0
    },
    "deletions": {
        "mean": 3287.634782608696,
        "std": 7541.675235428418,
        "min": 0.0,
        "25%": 19.5,
        "50%": 152.0,
        "75%": 2620.5,
        "max": 52753.0
    },
    "lines": {
        "mean": 8018.521739130435,
        "std": 15533.282323544638,
        "min": 0.0,
        "25%": 62.5,
        "50%": 307.0,
        "75%": 7497.5,
        "max": 85416.0
    },
    "files": {
        "mean": 82.41739130434783,
        "std": 123.61243781516767,
        "min": 0.0,
        "25%": 4.5,
        "50%": 22.0,
        "75%": 140.0,
        "max": 831.0
    },
    "authors": {
        "mean": 5.756521739130434,
        "std": 6.069179117804726,
        "min": 0.0,
        "25%": 1.0,
        "50%": 4.0,
        "75%": 9.0,
        "max": 32.0
    },
    "addedlines": {
        "mean": 1443.2521739130434,
        "std": 7191.568304720781,
        "min": -45723.0,
        "25%": 0.0,
        "50%": 27.0,
        "75%": 1067.5,
        "max": 33762.0
    }
}

hotspotコマンド

gilot hotspot で、変更されやすいファイルを出力可能。これはグラフではなくテキストで出力される。

ヘルプを見ると -o オプションが用意されているが、 --csv オプションとセットで指定しないと標準出力に結果が表示されるため、リダイレクトする。

デフォルトでは30件出力される、件数の変更は -n 整数 オプションで可能。

gilot hotspot -i log.csv -n 10 > hotspot.txt

結果は以下。

------------------------------------------------------------
    gilot hotspot ( https://github.com/hirokidaichi/gilot )
------------------------------------------------------------
    
 hotspot  commits  authors file_name
    1.86      166        5 BACKERS.md
    1.30      174       28 README.md
    0.82       51        5 yarn.lock
    0.39      102        2 dist/vue.js
    (後略)

また、 --csv オプションでCSV出力も可能。このオプションを使用すると、 -o オプションによる出力先ファイル指定が有効となる。 また、 -n オプションは無視され、logで出力したコミットに含まれる全ファイルに対する解析結果が出力される。

gilot hotspot -i log.csv --csv -o hotspot.csv

結果は以下。 --csv オプションを付けない場合より、詳細な情報を取得できる。

file_name,hotspot,commits,authors,edit_rate,lines
BACKERS.md,1.8565003497904684,166,5,0.5469133051036327,4487
README.md,1.3033147402277916,174,28,0.5703506435863294,2253
yarn.lock,0.8172803516743423,51,5,0.6243990891456523,23714
dist/vue.js,0.3879481352423523,102,2,0.5622198102968241,42382
(後略)

hotgraphコマンド

同時に変更されることの多いファイルのネットワークを出力できる。

Windowsで実行する場合、 --stop-retry オプションを付けないとエラーが発生する。

gilot hotgraph -i log.csv -o hotgraph.png --stop-retry

結果は以下。コミットが多すぎて重なってしまった...

f:id:hepokon365:20200904010049p:plain
hotgraph結果

出力結果の調整をしたい場合、 -r 整数 でランクの閾値の変更ができる。デフォルトでは70の模様。小さくすると結果は少なく、大きくすると結果は多くなる。

また、オプションに --csv があるが、指定しても意味はなさそう。

authorコマンド

コミッターごとのコミット数や割合をグラフ化できる。

gilot author -i log.csv -o author.png

結果は以下。Evan Youさん無双。

f:id:hepokon365:20200904011707p:plain
author結果

デフォルトでは上位10名が出力される。 -t 整数 で人数の変更可能。

-n 文字列 オプションを指定すると、右上の「GIT LOG -- AUTHORS REPORT」の「--」が指定文字列となる。

また、 --only コミッター名 で、指定したコミッター以外を「Others」にできる。

gilot author -i log.csv -o author-only.png --only 'Evan You' 'vue-bot'

結果は以下。

f:id:hepokon365:20200904012442p:plain
author only結果

authorコマンド実行時の注意点

出力フォントにIPAゴシックを使用している模様。フォントがインストールされていないと、以下の警告が発生。

WARNING : findfont: Font family ['IPAGothic'] not found. Falling back to DejaVu Sans.

グラフ自体は出力されるが、コミッター名に日本語など非ASCII文字が含まれると文字化けする。

警告表示されたら、IPAゴシックをインストール後、 python -c "import matplotlib, shutil; print(matplotlib.get_cachedir());" で表示される matplotlib のキャッシュディレクトリを削除して再実行する。

振り返り

おそらく、 vuejs/vue-nextなど他のGitリポジトリも含めれば、また違った結果が出たかと思う。

あとは、plot結果のグラフの読み方を理解しないとだな...