SSTエンジニアブログ

SSTのエンジニアによるWebセキュリティの技術を中心としたエンジニアブログです。

Java開発のお助けTIPS(2020年5月)

こんにちは、SSTでWeb脆弱性診断用のツール(スキャンツール)開発をしている坂本(Twitter, GitHub)です。

5月のGW明けごろに、社内のエンジニア向けにJava開発のスキルアップサポートを行う機会がありました。 コロナの影響で在宅となったため、Google Meets を使ってリモートでJava開発についての細々としたノウハウを若手エンジニアに伝授しました。 「細かすぎたかな・・・」と不安になりましたが幸いにも好評だったので、ざっくりとした箇条書きになりますがこちらでも公開します。

対象読者 : 中級レベルにステップアップしたい Java ビギナー(Javaの入門書を1 - 2冊、環境構築も含めて写経したくらいを想定)

なお一部には坂本個人の意見も混ざっています。参考程度にとどめ、もし所属するチームで定められたルールやレギュレーションがあったり、読者自身のこだわり/意見がある場合はそちらを尊重してください。

Java のインストール

中級レベルにステップアップしたい場合は、まず 入門書を読みながらインストールしたJDK/JREを一旦全てアンインストールすることを推奨 します。 理由は、JDK/JRE のインストール方法とバージョンに起因したトラブルで、あっという間に時間が溶けるケースが多いためです。

よくあるトラブル例:

  • Windows環境でJREをインストーラ形式でインストールしたが、その後JDKもインストーラ形式でインストールした。この時、JDKのインストーラのコンポーネント選択で Public JRE もチェックしてインストールしてしまったため、 ".jar" をダブルクリックしたときに起動する Java がJDKで一緒に入ったPublic JREに入れ替わってしまった。
  • JREやJDKを複数バージョン、インストーラ形式でインストールしてしまったため、PATH環境変数含め「どのJavaで起動してるのか/するのか分からない」状況になり、Javaの開発/実行、あるいはその手前のIDEの起動などでエラーが発生し、解決まで半日~数日かかってしまった(or そもそも解決できなかった)。

これらを回避するための一番手堅い方法は、特にWindows向けのアドバイスになりますが「JDKは ".zip" でダウンロードして展開して使う」です。(OSがサポートしているインストーラ形式ではなく、ファイルを圧縮しただけのアーカイブ形式を使うのがポイント)

  • インストーラ経由ではないので、Windows レジストリやPATHなどのシステム/OS設定が変更される心配が無い。
  • 手動でPATHを通す必要があるが、そのことが逆に 「今PATHに入っているJavaはどのJDK/JREか」を常に意識するようになるため、トラブルを未然に防ぐ効果を期待できる。
  • アンインストールもファイルを削除するだけ + PATHを手動調整するだけなので、綺麗に削除できる。
  • ".jar" ファイルをダブルクリックして実行したいJavaアプリケーションがある場合だけ、JREをインストーラ経由で導入する。

個人的な意見ですが、開発者としてインストーラ形式でJavaをインストールするのは、「".jar"ファイルをダブルクリックしたときにOS側でどのようにJavaプログラムの起動に至るのか解説できる人」あるいは「インストーラがどういう処理をしていて、どういう副作用が発生しているか把握できる人」以外にはオススメできません。(エンドユーザとしての立場であれば、JREをインストーラ形式でインストールするのは何の問題も無いと思います)

開発者としてのJDK/JREインストール方式の使い分けは、坂本の場合は以下のようにしています。

  • ".jar" をダブルクリックするタイプのアプリケーションの実行については、そのアプリケーションが推奨しているバージョンの JRE をインストーラ形式でインストールする。
    • (特にWindows用のJREインストーラの場合 : ) ".jar" ファイルダをブルクリックで実行可能になるよう、インストーラがシステム/OS設定を更新してくれる。
  • 開発用のJDKについては、任意のJDKを ".zip" などのアーカイブ形式で展開し、複数バージョン揃える。
    • LinuxやMacであれば sdkman を使うのも有りだと思います。
    • Eclipseの実行に使うJDKは、 eclipse.ini の -vm オプションで明示的に特定のJDKバージョンを指定する。(後述)

Java開発の勉強中は、複数バージョンのJDKを入れたり削除したりが数年の間に何度か繰り返されます。 そうしたときに、きちんとPATH環境変数を管理していないとバージョン関連のトラブルであっという間に時間が溶けて行きます。 アーカイブファイルを展開して使うのであれば、複数バージョンのJDKの同居は簡単ですし、システム/OS設定の衝突を心配する必要はまずありません。 PATH環境変数の設定についてだけ面倒な点が残りますが、坂本の場合は以下のような .bat ファイルを用意して、コマンドプロンプトから利用する時はこれらの .bat を呼び出してJDKバージョンを切り替えるなど工夫をしています。

jdk8-adopt.bat:

@echo off
set JAVA_HOME=C:\work\jdk\adoptopenjdk-win-x64\jdk8u212-b04
set PATH=%JAVA_HOME%\bin;%PATH%

jdk11-adopt.bat:

@echo off
set JAVA_HOME=C:\work\jdk\adoptopenjdk-win-x64\jdk-11.0.3_7
set PATH=%JAVA_HOME%\bin;%PATH%

Eclipse と Maven関係

(診断ツール開発では、坂本が今まで慣れ親しんできたことを理由に IDEはEclipse、ビルドツールは Maven を使い続けています。どちらも2020年現在は他の選択肢がありますので、あくまでも参考程度にお読みください。)

Eclipse関連

  • もし入門書で日本語化されたEclipseをインストールして使っているなら、ある程度操作に慣れて主なメニュー構成も頭に入ってきた段階で、Eclipse本家サイトから英語版をダウンロードして使うことをオススメします。
    • Eclipseで何かエラーやトラブルに遭遇した場合、日本語メニューよりは英語メニューで検索した方がヒット率が上がります。
    • 例 : 日本語のメニュー表記で調べても何も出てこないのに、英語版のメニュー表記で検索したらすぐに stackoverflow のQA記事が見つかり、そこをヒントにして解決策やworkarroundを見つけることができた。
  • Eclipse の .zip ファイルを Windows マシンにダウンロードして展開する場合、C:\workC:\tmp などなるべくファイルパス名が短い場所に展開してください。
    • Eclipse のアーカイブファイルの中には非常にパス名が長いものがあり、展開時のファイルパスの長さ制限に引っかかってしまい展開に失敗することがあります。
    • なおファイルパスの長さ制限は展開時だけらしく、一度正常に展開が終われば C:\xxx\yyy\zzz\... とある程度の深さのフォルダに移動しても問題有りません。
  • Eclipse 実行時は「Eclipseを実行するJava」を明示的に指定するのをオススメします。将来のバージョンアップや複数バージョンのJVM同時利用時のトラブルを回避できます。
    • eclipse.ini の 先頭に -vm オプションで javaw.exe へのフルパス指定(Winの場合。それ以外なら java へのフルパス)
    • 個人的には「Eclipseを実行するJDK」はLTSの安定版で固定しておき、それとは別に Eclipse の Java Build Path や コマンドプロンプトから javac/mvn コマンド実行するための JDK を各プロジェクトに合わせたバージョンで揃えてます。(もちろん、全て ".zip" でダウンロードして展開させ、安全に共存できるようにしています)
    • この辺の IDE と Java ランタイムについての関係性は、以下のスライドも参考になります。
  • チーム開発ではEclipseのformatter/cleanup設定を揃える。
    • README.md などに、開発者向けの環境構築やアドバイスとして記載すると良い。
    • formatter/cleanup 設定をXMLにエクスポートして、リポジトリに登録するのもいい。

Maven プロジェクトのgit管理とEclipse開発でのスタートアップ関連

  • IDEが生成するファイル・ディレクトリなどは .gitignore で除外設定する。
    • SpringBoot が生成する .gitignore や、OSSの設定を参考にするのがオススメ。
    • Mavenプロジェクトの場合、Eclipse にインポートしたときに自動生成される .settings/*, .project, .classpath なども .gitignore で除外し、git管理に置かない。
    • 個人環境でちょこっとEclipseの設定を変更しただけでも差分が発生してしまい、git管理でのノイズになるため。
    • また、Eclipseが生成する設定ファイルには個人環境でのフルパスも入り込みやすく、チーム開発で個人の環境依存を避ける時の障害となるため。
    • Eclipseのインストール先から何から何まで厳密に管理して同一環境を揃えたい場合はgit管理下に置いてもOK。
    • ※坂本個人の意見になります。チームで方針がある場合はそちらを尊重してください。
      • VM等により全く同じ開発環境を配布し、Eclipseの細かい設定も全て構成管理に含めたい場合はバージョン管理に含めた方が良いでしょう。
  • Eclipseに既存Mavenプロジェクトとしてインポートする時は、pom.xmlの内容を参考にJavaのバージョンなど判断して Eclipse 用設定ファイルが生成される。
    • pom.xml でちゃんと maven-compiler-plugin の java.version.source, java.version.target を指定してあげないと、1.7など古いJavaと認識されてEclipse用設定ファイルが生成されてしまうことがある。
  • pom.xmlをゼロから都度作成する必要はない。
    • 他のプロジェクトやOSSのpom.xmlを参考に、秘伝のタレを受け継いでいく。
    • ざっくり目安で5年以上前のやつだといろいろ古かったりもするので、最新はどうなっているか調べ直す必要あり。
  • Maven Wrapper(mvnw) 組み込むとJDKが準備できればビルドできるので便利。 .gitignore に !.mvn/wrapper/maven-wrapper.jar 追記してリポジトリに含めてあげるの忘れずに。
    • Windows環境で Maven Wrapper 組み込むと、mvnw (shell script) に実行属性が付かないので、そのままだとunix環境でcloneしたときに不便。 -> git update-index --add --chmod=+x mvnw してあげる。

Java プログラミング関連

※CLIツールやマルチスレッドを使ったHTTP(S)通信に挑戦中のエンジニアをサポートしたため、それに関連したノウハウを中心にざっくり箇条書きで紹介していきます。

CLIツール開発関連

マルチスレッド関連

  • まずは 増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編, 結城浩著 でサンプルコードを写経して一通り学ぶ。
  • CoundDownLatch を使うとスレッドプール上でのworker終了の待ち合わせを実現できる。(他にもやり方あるけど簡易的なアプローチとして便利。)
  • queue の check & take はatomicに行う。check と take を分けてしまうと、別スレッドがその間に最後の1個を check -> take してしまうと、元のスレッドに戻ってきてからtakeが走り空(null)が返されてしまう。
  • ExecutorService のスレッドプール上で実行中のworkerで例外が発生しても、そのままではスタックトレースなど何も表示されない。worker内できちんとtry-catchして、チェック例外/ランタイム例外両方とも補足してログなりスタックトレース出力なりをしてあげる。
  • コレクションライブラリでは、スレッドセーフになってるものとそうでないのがあるのに注意。

OOP設計/テストコード作成初級

WARNING!! 以下は特に坂本の好みが色濃く出ています。異論反論噴出する領域なので、鵜呑みにせず、読者の周囲のエンジニアやチームメンバーと「SSTの坂本というJava使いはこんな考えらしいけどどう思う?」など話してみてください。また、ビジネスロジックなどの内部処理を対象とした考え方になってます。Presentation/View レイヤーはスコープ外です。

  • 良い設計 > テストコードが書ける設計 >>>> (越えられない壁) >>>> テストコードが書きづらい設計 > テストコードが書けない設計
    • どんなに拙くてもいいので、まずは「テストコードが書ける設計」を目指す。
    • テストコードが書ける設計に慣れて、その後より良い設計を目指せばいい。
  • どうやったらテストコードを書けるようになるか?
    • 引数だけで返り値が定まり、処理の結果が分かるような静的メソッドはテストコードを書きやすいし、まずはそうしたユーティリティメソッドで練習しても良い。
    • ファイル入出力、標準入出力、データベース、ネットワーク通信、OS環境(OSのランタイム情報やハードウェアなど)を扱う処理はテストコードを書くのに工夫が必要。(もう少し具体的なパターンについては後述)
      • いわゆる副作用が発生するところを、インターフェイスで分離し、テストコードではmock実装を渡せるようにすると良い。
      • DIコンテナが使えればそれに頼るのもあり。constructor injection を使ってインスタンス初期化時に、そうした副作用があるインスタンスをmockに差し替える。
    • 境界値テストなど一般的なテスト技法を知り、テストコードでの入出力として表現できるようにする。
  • どんなクラス/メソッドについてテストコードを書けば良いか?
    • 「バグがあったら怖いビジネスロジック」 を優先してテストコードを書く。複雑な条件判定, 誤差伝播が怖い数値計算, ストアドプロシージャと連動するような複雑なDBトランザクション処理, e.t.c....
    • 以下のようなビジネスロジックとは直接影響しない or 「入力」と「期待」をプログラミングで再現するのが大変な領域は優先度を落とし、無理にテストを自動化する必要は無い。
      • UIなど、変更が頻発するような領域。※UIやE2Eテストの優先度は開発内容やチーム構成によって変化しますので、あくまでも坂本の現状での個人的見解としてご理解ください。 (診断ツール開発においては、UIテストやE2Eテストの前に、他に自動化するテストが沢山あるので相対的に優先度が低くなっている現状に基づいた見解となっています。)
      • 単にフレームワーク/ライブラリと値をつなげてるだけのようなglueコード
      • IDE/ライブラリ/フレームワークが自動生成したコード
    • 「JVM内で完結する」テストコードはサイズも小さいため、書きやすいので優先度が高くなる。「JVM内で完結しない」テストコードは外部リソースの調整などが入ってくるためサイズが大きくなり、書くのもメンテするのも大変になってくるので、優先度を下げてもいい。(「JVM内で完結する/しない」の詳細は後述)
  • テストのサイズ分類や優先度の考え方について、次の書籍も参考になるのでオススメ。
  • なぜそこまでしてテストコードの作成に拘るの?
    • テストコードは、「自分が書いたコードを最初に実行させる砂場(sandbox)」になるから。
    • アプリケーションに組み込んでいきなり動かすよりは、小さいパーツを細かく何度も動かして調整し、満足できるレベルになった部品をビルドアップしていく方が自分的にはストレスが少ないから。
    • テストコードは「このクラス/メソッドはこう使う / こういう使い方を想定している」という取り扱い説明書になる。数ヶ月後~数年後の未来の自分を救ってくれる。(実感)
    • 「こういう使い方をすると、それは意図していない使い方なのでこういう例外が発生するよ」というテストコードがあると更に自分を救ってくれる。
    • 極論すると、メンテされない JavaDoc を書くくらいなら、使い方を読み取れてビルド時に必ずその通りに動作することが保証されるテストコードを作ったほうがマシ。
  • テストコードの書きやすさは、「JVM内で処理が完結するか?」で大別できる。JVM内で完結できるのであれば、概ね、いわゆる「単体」テストとしてテストコードを作成できる。
    • ファイル操作 -> 相対パスで操作できるようにすれば、テストコード内でプロジェクト配下に事前に用意しておいたテスト用ファイルを使える。あるいはテスト用の一時ディレクトリを作成してそこにファイルを書き出すなども可能。
    • ネットワーク通信 -> テストコード内部で Netty 等のライブラリを使ってmock用の通信サーバを立ち上げる。
      • webサーバと通信させたい場合は MockServer などの便利なライブラリがあるので、それを探して使うと楽。
      • server socket を bind するときに、ポート番号0を指定するとOS側で空いてるポートを探してバインドしてくれるので衝突の心配が無い。listen()に成功したら、ローカル側のポート番号を取得してクライアントコードでそれを使って通信する。
    • DB操作 -> JPAなどORMで抽象化し、SQL非依存であれば H2 Database Engine などJavaに組み込めるオンメモリDBを使うと良い。
  • JVM内では完結できないものについては可能ならテストコードを作成し、「JVM内で完結するテスト」とは別ステージ/別コマンドで実行できるようにする。
    • 例: SQLに依存する処理についてはテストグループを分けて、実際のDBと接続できる環境でのみ実行するようにする。
    • Mavenプロジェクトであれば mvn test で実行するのは「JVM内で完結するテスト」とし、「JVMの外部」と接続するテストは mvn integration-test で実行するように調整する。

OOPでクラスを作る時の考え方:

  • インスタンスフィールドは本当に必要になるまで使わない。(できるだけ変数スコープは小さくする)
    • まずはメソッドの引数, ローカル変数, メソッドの戻り値だけで頑張ってみる。
    • 最初は static メソッドだけで頑張るくらいの意識で、複数のstaticメソッドがグルーピングされたときに共通して現れる引数が出てきたら、オブジェクトの状態として持つべきインスタンスフィールドの目印。
  • フィールドやメソッドは、本当に必要となるまではpublicにしない。
    • 最初は private にしておくか、必要に応じて中間である package スコープ(未指定) / protected スコープを指定する。
  • リソースのopenと操作/closeは1つのメソッド/try-catch内で完結させる。
    • リソース管理の操作を分離してしまうと、その間の処理が膨れ上がったりしたときの途中returnなどでのclose漏れが容易に発生してバグの原因になりやすい。
    • 可能な限りopen-closeは1メソッドの中で完結させる。
    • どうしてもクラス設計として「コンストラクタで open させて、その後のインスタンスメソッドでそのリソースをread/writeしたい」場合は Closeable / AutoCloseable インターフェイスを実装して、try-with-resourceの中で扱えるようにする。
  • データ構造だけのシンプルなクラスでは、toString() を overrideしておくと print デバッグが捗る。
    • Eclipse などIDEの自動生成で簡単に override できる。(Eclipse: クラスを右クリック→Source メニューにある)
  • データ構造だけのシンプルなクラスでは、 hashCode()equals() を override しておくと Map のキーにできたりSet で扱えたり equalsで比較できて便利。
    • 特に equals() overrideしておくと、テストコードでの assert が可能になるので戻り値比較が簡単に書ける。
    • Eclipse などIDEの自動生成で簡単に override できる。(Eclipse: クラスを右クリック→Source メニューにある)
    • hashCode()/equals() はきちんと作ろうとすると大変なので、IDEの自動生成にお任せするのが一番。
    • lombok@Data , @Value を使うとさらに簡単になる。
  • データ構造だけのシンプルなクラスでは、Getter/Setterは基本的には作らない。
    • (作る流派もある。以下、作らない流派からのご意見:)
    • もしin/outで変換が必要だとしたら、それは使う側で処理すべきもの。
    • データコンテナ(データ構造)はあくまでもデータを保存するだけに徹する。
    • 多少のユーティリティメソッドはあっても良い。
    • ただし、フレームワークやライブラリ側でgetter/setterを必要としている場合は作る。lombokの @Data, @Value を使うと簡単。
  • Singleton は絶対に作るな。絶対に、だ。いかなる理由があっても「1つのJVM内で、privateフィールドへのリフレクションという禁断の扉を開かない限りは1つ以上のインスタンスを作れない」仕組みを作るな。
    • java.lang.Runtimejava.lang.System はstaticメソッドの塊で、事実上の singleton じゃないか、なぜJDKは攻撃せずに、自分たちで作るケースだけ禁止する?」 → 「他が作ってるからOK」は坂本が許可しない。「他が作ってるから」じゃない。「自分達が、何を作るか」の問題なんだ。もう一度書く。Singletonは絶対に作るな。あれはソフトウェア開発の歴史の中で誰もが目にしたグローバル変数のデメリットを、あたかも「デザインパターンのカタログに載ってるから使ってもOKな優れた技法」に錯覚させるという点で、さらに質が悪い。
    • どうしてもインスタンスを1つに制限したい時は、「1つに制限される期間」を「ライフサイクル」として表現できるようにしろ。つまり、「ライフサイクル」をうまく管理すればテストコードでテスト用のインスタンスを複数作れる仕組みにするんだ。例えば Application 全体の Context クラスなどがそうだ。それは Application の「ライフサイクル」の間は1つと考える事ができて、それをDIコンテナでインジェクトすれば良い。テストコードでmock用のContextだって用意できる。それなら問題ない。だが SomeClass.getSingletonInstance() しか用意せず、テストコード用にインスタンスを分けられないような構成は絶対に作るな。絶対に、だ。何を言ってるのか分からなければ、なおさら単純だ。「Singletonを自分で作るな。Singletonデザインパターンを自分で実装するな。Singletonデザインパターンのことは綺麗サッパリ頭から消し去れ。あれはグローバル変数より遥かに、どれだけ言葉を尽くしても語り尽くせないほど邪悪な、最も忌むべき存在だ。」ほら、簡単だろう?それだけだ。だから、絶対にSingletonデザインパターンに手を出すな。





Singletonデザインパターンを作るのは避けるべきだが、JDKやフレームワーク/ライブラリが提供する Singleton や java.lang.System / java.lang.Runtime など static メソッドの塊を使うにはどうすれば良いか? → 人により解決策は異なり、以下はあくまでも坂本が考える解法となる。

  • そもそもなぜ Singleton デザインパターンを推奨できないかというとテストコードの作成で困るのが最大の理由。
  • なぜテストコードの作成で困るのか?
    • Singleton インスタンスの取得は大抵は SomeSingletonClass.getSingletonInstance() のように static メソッドとして提供されている。
    • ビジネスロジック中で直接これを呼び出して、Singletonインスタンスの戻り値に依存した処理を行うとする。Singleton インスタンスの内容はINPUTに相当するため、そのビジネスロジックのテストコードを作成する際はある程度入力パターンを用意したい。しかし、ロジック内でstaticメソッド呼び出しによりインスタンスを取得しているため、テスト用のmockに差し替えることができない。
      • 差し替えられるmockライブラリもあるにはあるが、classloaderなどへの副作用が大きく予期せぬトラブルに遭遇するリスクがあるため、坂本は採用には慎重派。そこまでしてmockするよりは、後述のインターフェイスで分離するアプローチを採用している。
    • そうなると、テストの入力パターンとして Singleton インスタンスの戻り値を変化させることができないため、テストコードを書きづらくなる。
  • 前述の通り坂本の個人的意見では「テストコードを書ける設計」が最優先であるため、どれほど綺麗に設計されていても、テストコードを書きづらい設計は避けたい。
  • 解決方法として以下の3つがある。
    1. static メソッド呼び出しで Singleton インスタンス相当を取得するコードから、メソッド/コンストラクタ引数などで外部からインスタンスを渡すようにする。(可能な限り、Singleton インスタンス相当の管理をアプリケーションライフサイクルの一番外側にずらして、ビジネスロジックの方で直接 static メソッドで取得する必要が無いようにする。)
    2. Singleton インスタンス相当のライフサイクル管理をDIコンテナにお任せして、ビジネスロジックのクラスにインジェクトできるようにする。(DIコンテナ側で、テストコードでテスト用のインスタンスをインジェクトできるようカスタマイズできることを前提)
    3. java.lang.Systemjava.lang.Runtime の static メソッドにアクセスしたい場合は、インターフェイスで分離する。(後述)
static メソッドをインターフェイスで分離してテストコードでmock可能にする例

以下のような java.lang.System に依存するビジネスロジックがあったとする。

OffsetDateTime odtx = OffsetDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneOffset.UTC);
// ...
String s = System.getenv(name);
// ...
// 現在時刻(odtx) と環境変数に依存して条件分岐や出力を生成するビジネスロジック

このままでは現在時刻や環境変数が実行環境に依存してしまい、テストコード中でmockかができない。(例:2月29日や特定日時の直前・直後等のテストコードを書くことができない。)

そこで、まずは使用する java.lang.System の static メソッドを独自のインターフェイスとして準備する → ISystemAccessor

interface ISystemAccessor {
    long currentTimeMillis();
    String getEnv(String name);

    // 実際に System クラスの static メソッドを呼ぶ実装
    static ISystemAccessor ofPlatform() {
        return new ISystemAccessor() {
            @Override
            public long currentTimeMillis() {
                return System.currentTimeMillis();
            }
            @Override
            public String getEnv(String name) {
                return System.getenv(name);
            }
        };
    }
}

ビジネスロジックで System.currentTimeMillis()System.getenv(String) を呼び出しているところを、ISystemAccessor インスタンスのインスタンスメソッド呼び出しに変更する。

void someBusinessLogic(final ISystemAccessor sa) {
// ...
OffsetDateTime odtx = OffsetDateTime.ofInstant(Instant.ofEpochMilli(sa.currentTimeMillis()), ZoneOffset.UTC);
// ...
String s = sa.getEnv(name);
// ...
// 現在時刻(odtx) と環境変数に依存して条件分岐や出力を生成するビジネスロジック

ここまでくれば、テストコードでは以下のようなmockを用意して、任意の時刻・環境変数での動きを確認することが可能となる。

class MockedSystemAccessor implements ISystemAccessor {
    private final long millis;
    private final Map<String, String> envs;

    MockedSystemAccessor(final long millis, final Map<String, String> envs) {
        this.millis = millis;
        this.envs = envs;
    }

    @Override
    public long currentTimeMillis() {
        return this.millis;
    }

    @Override
    public String getEnv(String name) {
        return this.envs.get(name);
    }
}

テストコードのイメージ:

@Test
public void testSomeBusinessLogic() {
        ISystemAccessor sa = new MockedSystemAccessor(1_591_966_800_000L,  Map.of("env1", "aaa", "env2", "bbb"));
        someBusinessLogic(sa);
       // ...
}

参考書籍: レガシーコード改善ガイド

ネットワークプログラミング

※たまたまシンプルなHTTP(S)のGETリクエストのみを扱っていたため、Http(s)URLConnection を使うアプローチについてのアドバイスになります。

  • HttpsURLConnection で証明書検証をOFFにしたい場合は、"java https disable certificate validation" でググる。
    • HostnameVerifier のカスタマイズも忘れずに。
    • どうしても HttpsURLConnection 全体でstaticに設定しないといけないのがネック。
  • Http(s)URLConnection で上位Proxyを有効にしたい場合は、URLConnection.openConnection(Proxy proxy) で接続を開始する。
  • serrver-socket bind時にポート番号0を指定すると、OS側で空いてるポート番号をランダムに選んでくれる。
  • もう少し高度なHTTP通信をしたいのであれば、以下のようなライブラリを試してみる。
  • オススメ書籍

チーム開発と品質

オススメ書籍:

時間があれば読みたい:

Eclipse/Mavenプラグインなど:

  • Checkstyle プラグインを導入すると、ソースコードのスタイルチェックを行える。
    • https://checkstyle.sourceforge.io/
    • これで、ソースコードの書き方に関する最低ラインを統一できる。
    • 指摘内容とアドバイスを見てくと、「Javaってこういう書き方はNGなのか / こういう書き方ができるのか」と勉強になる。
  • Spotbugs プラグインを導入すると、コンパイルされた .class を静的に解析してバグチェックできる。
    • https://spotbugs.github.io/
    • すごい精度が高いわけではないが、ちょっとしたケアレスミスやJavaの落とし穴など見つけてくれる。
    • 指摘内容を見てくと「Javaってこういう落とし穴があったのか」と勉強になる。
  • PMD プラグインを導入すると、コードスタイルや軽微なバグなどチェックしてくれる。
  • 上記3つはEclipseプラグインとして導入して手軽に始められる。
    • Mavenプラグインとしてビルドシステムに組み込んで、ビルド時にチェックしてレポート出力することもできる。

なお、MavenプラグインとEclipseプラグインの設定を共通化し、Eclipse上とMavenビルドでのチェックでズレが発生しないようにするには、結構な職人技が必要になったりする。 また Checkstyle / PMD などのスタイルチェッカーの設定と、Eclipse側の code formatter の設定を揃える必要もある点に注意。 メジャーなスタイルに寄せておけば、いずれもあまり大きな調整なしに揃えることができる。

その他Javaプログラミング

  • for ループでは、拡張forを優先して使う。
  • ネストしたクラスの中から、外のクラスのthisを参照するには「外のクラス名.this」が使える。
public class OuterClass {
    public void doSomething() {
        // ...
        addSomeHandler(new SomeHandler() {
            @Override
            public void handle(Something someObject) {
                callback(OuterClass.this, someObject);
                // -> callback に doSomething() 中のthisを渡せる。
            }
        });
    }
}
  • DefaultTableModelなどVectorを使ってるのは古いAPIなので、使うのは避ける。参考程度にとどめておく。

開発全般

  • 実現方法が分からなくなったときは、「他に同じような機能を実現してるのは無いか?」と考えて、探してみる。
    • ググって、有名で使ってる人の多いOSSのソースコードから探してみる。
    • 他にも解説記事など探してみる。ただし鵜呑みにしたりせず、各種公式ドキュメントや公式リファレンスを探して正確な情報の収集に努める。必要なら書籍などもあたってみる。
    • →自分が思いついたことの8割は他の人はすでに思いついて試してたりする。そうしたのを参考にしたり、また新たなヒントを得ることができる。
  • なるべく英語でググる方法に慣れる・習熟する。
    • 深い話題・原因調査になると英語記事/公式サイトの質量が圧倒的。
    • もちろん玉石混交ではあるが、単純な物量として英語リソースは圧倒的な分量がある。石も多いが、玉も見つけやすい。
  • なにか処理が動いてないような場所を見つけたら、try-catch(Throwable) で囲ってみて、なにか例外が発生していないか調べてみるのが一つの手。
    • メソッドが「この例外を投げます」と宣言しているチェック例外だけでなく、それ以外の非チェック例外(RuntimeException経由)が発生している可能性がある。
    • Javaの例外は Throwable を親クラスとしているので、Throwableでcatchすると、とにかく何かしらのエラーは補足できる。
    • printStackTrace() には PrintStream/Writer を引数に取るoverloadがあり、これを使うと標準出力/標準エラー出力以外の独自の出力先にスタックトレースを書き込める。
    • スタックトレースで caused ... みたく別のスタックトレースが続く時は、そちらが原因で、それをラップした構造になっている。
    • getCause() で 原因となる例外を取り出せるので、それを printStackTraceするとさらに細かい調査が可能になる。

以上です。書いてくうちに「あれもこれも」という欲望を押さえられず、5月時点の内容から3割増しくらいになりました。誰かのお役に立てれば幸いです。あとテストコードやOOPについて、坂本の考え方と完全に一致という稀有な方はTwitterなどでDMください、一緒に語り明かしましょう!