javax.jws.WebParamなどがコンパイルエラーになったらJava 11に更新されているかもしれない

周囲に突然Javaアプリケーションのコンパイルがエラーになる人がちらほら。原因はシンプルだったが、意図せず発生していたためメモ。

問題

Spring Bootを利用したアプリケーションが、macOSのPCでコンパイルできなくなる事例が発生。

Controllerのメソッド引数にJAX-WSjavax.jws.WebParam アノテーションが付与された箇所があり、WebParamが参照できず、コンパイルエラーが発生していた。

原因

非常に単純。

問題の端末で、Javaのバージョンが11に更新されていた模様。JAX-WSJava 11からは除外されたため、別途依存性を追加しないと、そのままではコンパイルできない。

javax.jws.WebParam については、WSDLを生成したりという意図はなく、単純に org.springframework.web.bind.annotation.RequestParam と間違えて使用されていた。

ただ、そもそもなぜJavaのバージョンが上がったのかについては特定できず。Javaの自動アップデートか?

対応

JAVA_HOMEJava 8を参照するよう変更して対応。

また、特に意味もないのにControllerの引数に javax.jws.WebParam が付与された状態も問題なので、それぞれ削除し、必要に応じて org.springframework.web.bind.annotation.RequestParam を付与した。

既存のソースコードに手を入れられない場合、コンパイルだけなら javax.xml.ws:jaxws-api および javax.jws:javax.jws-api を依存関係に追加すればいい。以下に詳しい。

terasolunaorg.github.io

動かす必要があれば、別途実装を依存関係に追加する必要がある。

Maven Repository: com.sun.xml.ws » jaxws-rt

wsimportなどのツールインストールはこちら。

qiita.com

その他にもいろいろ削除されたモジュールがある。以下に詳しい。

k11i.biz

振り返り

コンパイルエラーの出ているクラス名を確認し、 javax.jws あたりで大体わかった。

Spring MVCを使ったアプリケーションで WebParam が使われていたという、残念な状態に気づけて良かったのかもしれない。

Windowsの自分はどこ吹く風だったが、根本原因であるJava 11へのアップデートがかかった原因がわからないのは気持ち悪いなぁ。

UserScriptのURLからのインストールと自動更新させる方法

社内向けにちょこちょこUserScriptを書いているが、作成時や更新のたびに、メール添付したりSlackなどのチャットツールでソース流したりと、配布するのが面倒だった。

今回、更新頻度が高くなりそうなスクリプトを書くついでに、UserScript配布サイトのように、リンククリックからのダウンロードや自動更新を行えないか調べたのでメモ。

環境

Tampermonkey v4.10にて確認。

配布方法

TampermonkeyのFAQ、Q102に記載があった。

www.tampermonkey.net

URLの末尾を「.user.js」にしてやると、そのページを開いたときにTampermonkeyやGreasemonkeyといった拡張機能がブラウザに追加されていれば、そのURLのスクリプトをインストールするか確認してくれる。

また、すでにインストールされた状態でアクセスすると、バージョン番号が同じなら「再インストール」、バージョンが上がっていれば「更新」、バージョンが下がっていれば「ダウングレード」と、状況に応じてそれぞれ確認される。

更新方法

Tampermonkeyのドキュメントに記載があった。

www.tampermonkey.net

UserScript冒頭のコメント部分に @updateURL を追加し、そこにスクリプトのURLを記述しておく。

「UserScriptの更新を確認」した場合などに、インストールされているスクリプト@updateURL に書かれたスクリプト@version を比較し、 @updateURLスクリプトが更新されていれば自動更新される。

きちんとバージョン番号の比較を行っているらしく、バージョンが下がった場合は更新されない。

前述のドキュメントには、 @downloadURL も更新に関係しそうな記述があるが、 @updateURL の記述だけでも自動更新は有効だった。

以下のページにあるように、GitサーバーのmasterブランチのrawファイルURLを書いておくのが手っ取り早い。

www.tampermonkey.net

サンプル

サンプルを書いてみた。

GitHub - hepokon365/updatable_userscript

以下、 https://example.com を開くとコンソールログにバージョン番号を出すだけのスクリプト

https://github.com/hepokon365/updatable_userscript/raw/v1.0.0/src/sample.user.js

// ==UserScript==
// @name         Updatable Userscript
// @namespace    https://github.com/hepokon365/updatable_userscript
// @version      1.0.0
// @description  updatable userscript sample.
// @author       hepokon365
// @match        https://example.com/
// @grant        none
// @updateURL    https://github.com/hepokon365/updatable_userscript/raw/master/src/sample.user.js
// @downloadURL  https://github.com/hepokon365/updatable_userscript/raw/master/src/sample.user.js
// @supportURL   https://github.com/hepokon365/updatable_userscript
// ==/UserScript==

(() => {
  console.log('v1.0.0');
})();

インストールしたときは v1.0.0 だが、masterブランチを参照して更新がかかると v1.1.0 になる。

振り返り

社内での配布は、利用者全員にGitサーバーのアカウントを作成するわけにもいかなかったため、CIで配布用のURLにコピーしたりといったひと手間が必要だったが、それでも従来の配布による更新よりはだいぶ楽になった。

NumLockが解除されるのでAutoHotkeyで常時ON状態に固定する

会社支給のPCのNumLockが、意図せず解除されてしまう。

テンキーありのラップトップPCだが、BackSpaceとNumLockの間隔がほとんどなく、キーが小さいうえにキーストロークが浅い。理由としては何かのトラブルで勝手に解除されているとかではなく、単純にBackSpaceと一緒にNumLockを押下してしまっている模様。

タイピングの癖は変えられないし、NumLockを強制的にONで固定する方法も調べたが、PC起動時のON/OFF設定や、レジストリをいじる方法しか見つからなかった。

仕方なく、AutoHotkeyで無理矢理どうにかしてみたのでメモ。

環境

AutoHotkeyのインストールと初期設定

導入済みなら不要だが、とりあえずインストールと初期設定の方法を書いておく。

インストール

Chocolateyを使って cinst -y autohotkey.portable でインストールするのが手っ取り早い。

初期設定

スクリプトファイルを作成し、プログラムと関連付けしてスタートアップで起動できるようにする。

  1. スクリプトファイル autohotkey.ahk を任意の場所に作成する(実際は、名前も拡張子も任意でいい)。
  2. スクリプトファイルをダブルクリックして、 C:\ProgramData\chocolatey\bin\AutoHotkey.exe と関連付ける
  3. スクリプトファイルのショートカットをスタートアップに登録する
    • Windows +R で「ファイル名を指定して実行」を開き、 shell:startup で開くのが手っ取り早い

NumLockのON固定AutoHotkeyスクリプト

さて本題。

やることは単純で、NumLockがOFFの場合のテンキー入力にフックをかけて、NumLockがONの場合と同じキー入力をさせればいい。

NumLockのON/OFFで操作が変わるのは、テンキーの0...9および.(ドット)。

キーリスト - AutoHotkey Wiki で確認し、以下をAutoHotkeyスクリプトに記述すると、NumLockのON/OFFにかかわらず数字およびドットが入力できるようになった。

; NumLock OFF Remap
NumpadDel::.
NumpadIns::0
NumpadEnd::1
NumpadDown::2
NumpadPgDn::3
NumpadLeft::4
NumpadClear::5
NumpadRight::6
NumpadHome::7
NumpadUp::8
NumpadPgUp::9

振り返り

力技だが何とかなった。だいぶストレス軽減。

PowerToysにKeyboard Managerが追加され、そちらでもキーボードの再マップができる模様。

github.com

とはいえまだPowerToysは正式版ではないので、とりあえずは使い慣れたAutoHotkeyで対応しておく。

備忘

環境によってキー名が異なるかもしれない。キー名は以下をAutoHotkeyスクリプトに追加することで確認できる。

#InstallKeybdHook
KeyHistory

GradleのテストがUP-TO-DATEの場合の再テスト実行方法とcleanについて

前回のように、調整でGradleの test タスク実行を繰り返していると、変更がない場合 UP-TO-DATE となって実行されない。

gradle clean test ではコンパイルからやり直しだし、いちいちテストを変更して再実行するのも面倒。手軽にテストを再実行する方法を調べたのでメモ。

環境

Groovy v3.0.3, Gradle v6.3。

--rerun-tasks

Command-Line Interface

--rerun-tasks を付与して実行すると、各タスクがUP-TO-DATEチェックを無視して強制実行される。

ただし、指定したタスクだけでなく依存タスクまで再実行されるため、 gradle test --rerun-tasks ではcleanした場合と変わらず、コンパイルなどから再実行される。

cleanTest

The Java Plugin を見ると、 clean${TaskName} とタスク名を指定することで、指定したタスクの成果物のみ削除することができる模様。

gradle cleanTest test とすると、 compileJava などはUP-TO-DATEのまま、test のみ再実行された。

自作タスクのclean

前回作った fastTest タスクを再実行する場合、 gradle cleanFastTest fastTest となる。

試しに gradle fastTest してから gradle cleanTest fastTest したところ、 fastTest はUP-TO-DATEで実行されなかった。

振り返り

Mavenでは mvn test するとテスト実行は必ず行われていたように思う。

コンパイルはともかく、テストの実行はスキップしてほしくないので、やっぱりGradleよりMavenのほうが好きだな...

備考

UP-TO-DATEとなる条件を定義することもできる模様。

ryozi.hatenadiary.jp

SpockのテストをグルーピングしGradleタスク化、指定されたタスクで条件判定して処理を切り分け

前回の続き。

アサインされたJavaプロジェクトではSpring Bootを使用しているが、ユニットテストではモックが使われていなかった。

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) でWebサーバーを起動し、 TestRestTemplate でControllerクラスのメソッドを実行しているため、テスト実行まで数分かかる。

また、Repositoryに対する個別のテストは書かれておらず、ControllerやServiceから間接的にテストされていた。アクセスするDBは processTestResources にてローカルにH2 Databaseのファイルを作成し、ControllerやServiceのテストクラスのsetupメソッドでデータ削除を実行したりと、実行にも時間がかかる。

こうした場合、速度(Slow/Fast)やテストサイズ(Small/Medium/Large)でテストをグループ化するが、引数を渡して test タスク実行では面倒なので、別途タスク化して実行したい。さらに、追加したタスクが指定された場合、H2 Databaseは不要なので、DBファイル作成処理は実行させないようにしたい。調べてみたらとりあえず実現できたのでメモ。

環境

Groovy v3.0.3, Gradle v6.3。

テストのグルーピング

検索すればたくさん出てくるが、SpockはJUnit4を利用しているため、 @Category によるカテゴリ化テストを利用できる。

Gradleからは test.useJUnit(Closure)includeCategoriesexcludeCategories として対象のクラス名を指定すればいい。

Test - Gradle DSL Version 6.3

@Category には任意のクラスを渡せるため、StringなりObjectなりでもいいが、一応クラスを作っておく。インスタンス生成できないよう、finalかつprivateコンストラクタを持ったクラスを作成。

package com.example

public final class FastTests {
    private FastTests() {
    }
}

@SpringBootTest を付与していない、シンプルなテストクラスに @Category(FastTests.class) として付与しておく。

Gradleタスクとしてカテゴリ化テストを切り出し

以下の記事に方法が全部書いてあった。

mike-neck.hatenadiary.com

gradle fastTest で実行できるよう、 build.gradle にタスク定義を記述。オプションで出力レベルを変更させたくないため、 lifecycle ログの出力設定を追加し、以下のようになった。

task fastTest(type: Test, dependsOn: testClasses) {
  useJUnit {
    includeCategories 'com.example.FastTests'
  }

  testLogging.lifecycle {
    // `events 'started', 'passed', 'skipped'` などを指定すると、テストメソッド単位でログ出力されるため、afterSuiteで出力する
    events 'standard_out'
    exceptionFormat 'full'
    showStandardStreams true
  }

  afterSuite { TestDescriptor desc, TestResult ret ->
    // 除外されたテストも SUCCESS 扱いで afterSuite に渡されるが、その場合は testCount が 0 になる
    if (!ret.testCount) {
      return
    }

    // className が設定されていればクラス単位の結果
    // className が null で parent が `Gradle Test Run :${taskName}` の場合は `Gradle Test Executor ${index}` ごとの結果
    // className が null で parent が null の場合は `Gradle Test Run :${taskName}` の結果、総計となる

    def hasClassName = !!desc.className

    // Test Executorごとの結果は不要
    if (!hasClassName && desc.parent) {
      return
    }

    // クラス名が一意ならパッケージなし、一意でないならパッケージありで className が入る
    def descName = hasClassName ? "${ret.resultType} ${desc.className}"'Results :'

    // Mavenのテスト結果風にメッセージを作成
    def retMessage = [
        'Tests run': ret.testCount,
        Failures: ret.failedTestCount,
        Errors: ret.exceptions.size(),
        Skipped: ret.skippedTestCount
    ].collect {
        "${it.key}: ${it.value}"
    }.join(', ') + (hasClassName ? ", Time elapsed: ${(ret.endTime - ret.startTime) / 1000} sec" : '')

    logger.lifecycle "${descName}\n${retMessage}"
  }
}

これで、 gradle fastTest でログ出力しつつ @Category(FastTests.class) を付与したテストクラスを実行できるようになった。

指定されたタスクによる処理の切り分け

H2 Databaseのファイル作成処理は createTestDB タスクとして記述され、 processTestResourcesdependsOn されている。

fastTest タスクが実行されるのはローカルでのユニットテスト実行に限定できるため、これが指定されたときは依存関係設定をしなければいい。

指定されたタスク名は project.gradle.startParameter.taskNames で取得できる。Javadocによると taskNamesList<String> のため、以下の記述で実現できた。

// `!in` はGroovy v3から
if ('fastTest' !in project.gradle.startParameter.taskNames) {
  processResources.dependsOn createTestDB
}

振り返り

Spring Bootのテストだからとナイーブに @SpringBootTest を使ってしまうと、テストが遅くなりがち。

ローカルで何度も実行することを考えると、Smallテストの実行は30秒以内に終わらせたい。

ただ、テストが遅くなったから修正しようにも、なかなかテストコードのリファクタリングをする時間は取れない。

事前にテスト記述のルールを決めておくなど、ある程度の準備は必要だと思った。

Serviceの結合テストではDIできればいいので @SpringBootTest(webEnvironment = WebEnvironment.NONE) を使う、単体テストとしてモックを使ったテストを記述してSmallテストとして実行できるようにするなど、テスト自体の改善もしないとなぁ。

以下、テスト記述の参考。

備考

Spockのテストのグルーピングは、 SpockConfig.groovy を使えばアノテーションでもできる模様。 SpockConfig.groovy についての知識がなく、起動時のパラメータで切り替える必要がありそうだったので、今回は見送り。

Spockのwhen/then/whereで成功と例外の両方をテストする

Javaユニットテストを書くとき、普段は例外が発生するパターンのテストを例外ごとに書き、それとは別に正常終了するテストを書いて、パラメータと結果の組み合わせをfixtureで渡している。

新しくアサインされたJavaプロジェクトでテストを書こうとしたら、テスト対象のメソッドはDAOによるデータ取得とビジネスロジックが交互に行われているトランザクションスクリプトで、DAOをモック化しても大量の前処理が必要だった。

テストフレームワークはSpockを使っているが、Groovyで書いても前処理だけで60行ほど使ってしまうので、成功と例外をまとめて同じテストメソッドで書けないかと思い、試してみたらスマートではないがなんとかできたのでメモ。

状況

前処理で大量にDAOをモック化し、検索結果として取得するインスタンスを用意しないといけない。

省略しているが、既存のテストメソッドでDAOなどを利用しているため、 setup メソッドでのモック化や @Shared での共有もやりづらく、追加したテストメソッド内で完結させたい。

def targetClass = new TargetClass()

// DAOのインスタンス生成

@Unroll
def "targetMethodの例外発生テスト"() {
  given:
  // DAOのモック化、取得するインスタンス生成など

  when:
  targetClass.targetMethod(...)

  then:
  def e = thrown()
  e.class == ...
  e.message == ...
}

@Unroll
def "targetMethod(#param) == #expected のテスト"() {
  given:
  // DAOのモック化、取得するインスタンス生成など
  // テストのパターンによってインスタンスをいじりたい

  expect:
  targetClass.targetMethod(param)

  where:
  param || expected
  ...
}

対応

where で指定するパラメーターに例外クラスを含めておき、 thenthrown で例外を取得。パラメーターで例外が指定されている場合は例外を比較、 null なり _ なりで有効なクラスが指定されていなければメソッド実行結果と期待値を比較、といった判定ができないかと思ったが、 thrown で例外クラスの指定が必要だったため難しかった。

また、 thrownthen の先頭で書かないとエラーになるため、例外クラスを where で書いて判定することもできなかった。

仕方ないので、 whenthen を複数記述し、成功時とエラー時で処理を切り分けることとした。 where で指定したパラメーター数だけ例外発生のテストが重複して実行されてしまうが、ひとまず目的は達成できた。

@Unroll
def "targetMethod(#param)のテスト"() {
  given:
  // DAOのモック化、取得するインスタンス生成など
  def errorParam = ... // エラーが発生するパラメーター

  when:
  def actual = targetClass.targetMethod(param)

  then:
  actual == expected

  when:
  targetClass.targetMethod(errorParam)

  then:
  Exception e = thrown()
  e.message == errorMessage

  where:
  param || expected
  ...   || ...
}

振り返り

既存のソースにテストを追加する形となったため、かなり無理矢理になってしまった。やはりテストを書きやすいよう、データの扱いとビジネスロジックは分離しておくべきだなぁ。

where に例外クラスやメッセージを追加することで、もう少しうまくまとめられるかもしれない。そもそももっとうまいやり方があるかもしれないが、とりあえず目的達成はできたのでここまで。テストに時間をかけすぎるのも本質的ではないし。

Windowsで使えるcURLいろいろ

Mac使いの人から、「WindowsじゃcURL使えないよね?」と聞かれたので、「最近のWindows 10ならcURLプリインストールされてますよ」と答えると驚かれた。

ほかにもいろいろな方法があるので、ざっと思いつくWindowsでのcURL使い方をメモ。

環境

Windows 10 Pro 64bit Version 1909

プリインストール版

Windows 10 Version 1803から、しれっとプリインストールされるようになっている。

ちなみに、 curl 以外にも tarwget が使えるようになり、Version 1709の時点で ssh も(コマンドプロンプトsshするかはともかく)使える。

バージョン表記は以下。2020/4/18時点でのcURLバージョンは7.69.1だったので、ちょっと古い。

>curl --version
curl 7.55.1 (Windows) libcurl/7.55.1 WinSSL
Release-Date: [unreleased]
Protocols: dict file ftp ftps http https imap imaps pop3 pop3s smtp smtps telnet tftp
Features: AsynchDNS IPv6 Largefile SSPI Kerberos SPNEGO NTLM SSL

Bashインストール

Windows Subsystem for Linuxや、Git for Windowsをインストールすると使えるGit Bashでも、cURLが使用できる。

Git Bashだと、その時点の最新のcURLがついてくる模様。

$ git --version
git version 2.26.1.windows.1

$ curl --version
curl 7.69.1 (x86_64-w64-mingw32) libcurl/7.69.1 OpenSSL/1.1.1f (Schannel) zlib/1.2.11 libidn2/2.3.0 libssh2/1.9.0 nghttp2/1.40.0
Release-Date: 2020-03-11
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp scp sftp smtp smtps telnet tftp
Features: AsynchDNS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz Metalink MultiSSL NTLM SPNEGO SSL SSPI TLS-SRP

Windows PowerShellInvoke-WebRequest

いちおうWindows PowerShellでも curlwget が使えるが、これらは Invoke-WebRequestエイリアス。オプションや実行結果の形式が全く違い、 curl -X POST もエラーになるので使えない。

マルチプラットフォーム化したPowerShell Coreでは、これらのエイリアスは設定されなくなっている。

Windows PowerShellでプリインストール版のcURLを使う

curl.exe のように拡張子までつけて実行するか、 Remove-Item alias:curlエイリアスを削除すればいい。詳しくはこちら。

devadjust.exblog.jp

別途インストール

Windows向けcURLを別途インストールすれば、最新バージョンを使用できる。

Chocolateyでもインストール可能。 Chocolatey Software | cURL 7.70.0

Cygwin

Cygwinインストール時に、cURLをインストールできる。試していないが、確かデフォルトではスキップになっていたと思う。

振り返り

Windowsも進化してるんですよ。これで grepsed も標準で使えるようになればなぁ...