これはなんですか?
jarの中にあるクラスや変更できないコンパイル、パッケージング済みライブラリなどについて、挙動を変えたいときにその動作を受け持つクラスの挙動を変えるための秘技。
直接クラスを書き換えたりするのではなく、実行時にメモリ上にロードされたクラスを書き換えるため、ライブラリそのものを書き換えわけではない。
Javaバージョン問わずの話で、この先も大きく変わることはないと思う。
私が最初に触ったJDKは1.4で、最初にこの技法を用いたのは1.5。
その頃からJava 11までずーっと有効でした。
私はJDKの開発者ではないから、クラスがロードされ保持する具体的な仕組みとかはよくわかっていない。また、その説明を仔細に行うものではない。
秘技の説明
私はクラスが保持される仕組みは、Javaの挙動から推測するに、概ねマップ(Map)のようなものだと思っている。
ジェネリクスの中の型は、Stringの部分はクラスのフルパス "java.util.ArrayList"
などで、Classの部分はjava.util.ArrayList.Class。
ロードされる順番は -cp class path(s) の引数次第だけど、各種開発環境やビルドツールに隠蔽されよくわからなくなっているが、例えばSpring Bootでmaven+IDEを使っている場合はだいたい下記のような感じです。
- JDKのライブラリ
java.*
など - dependenciesに書いた依存関係にある
- src/main
- src/test (テスト実行しているとき)
数字が大きいほうがあとから読み込まれ、優先される。
かんたんな例だと、 JUnit で @SpringBootTest のテストを書いているとき、 src/main の中に邪魔なスタートアップ処理(1)がある場合、そいつをsrc/mainで上書き(2)して なかったことに したりできる。
邪魔なスタートアップ処理(1)
public class ApplicationStartup
implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(final ApplicationReadyEvent event) {
// Spring Boot起動時に呼ばれる処理で、テストしたいクラスとは関係がないが、邪魔になる処理
...
}
}
上記をテスト実行時になかったことにしたい場合の処置(2)
public class ApplicationStartup
implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(final ApplicationReadyEvent event) {
// _人人人人人人人人_
// > 何も書かない <
//  ̄Y^Y^Y^Y^Y^Y^Y^ ̄
}
}
アラ不思議、テスト実行時にはなんら呼ばれることなく以下省略。
なお、getResource(String)してリソースをとるものであれば、例えばpropertiesやtemplateなども同様に上書きすることができる。
HTTPヘッダにUTF-8み日本語ファイル名をエンコードせず入れろという糞みたいな要求について、RFC的にダメだということを説明しても受け入れないファッキンフールな人たち向けに、openapi-generatorのクライアント部分で用いているokhttp3のクラスをこの手法で書き換えて対処したことがある。苦汁を1ガロンくらい飲んだ上でやむなく実装した。
処置の内容は okhttp3.Headers.class
というクラスがヘッダの形式チェックをしている箇所であることを特定し、その部分を少し改造したコードを src/main/java/okhttp3/Headers.java
としてプロジェクトに配置した。
この処置を行うためには、オープンソースのコードを読んだり、挙動を司る部分を特定する、というスキルが必要です。
最終手段だけど役に立つが、ご利用は計画的にしたほうがよい。
深淵をのぞく時、深淵もまたこちらをのぞいているのだ。
この先はかすれていて読めない