高度なJavaクラスチュートリアル:クラスのリロードのガイド
公開: 2022-03-11Java開発プロジェクトでは、一般的なワークフローでは、クラスが変更されるたびにサーバーを再起動する必要があり、誰もそれについて不満を言うことはありません。 それはJava開発についての事実です。 私たちはJavaを使った最初の日から、そのように取り組んできました。 しかし、Javaクラスのリロードを実現するのは難しいですか? そして、その問題は、熟練したJava開発者にとって、解決するのが困難であり、刺激的である可能性がありますか? このJavaクラスのチュートリアルでは、問題に対処し、オンザフライのクラスリロードのすべての利点を活用し、生産性を大幅に向上させるのに役立ちます。
Javaクラスのリロードについてはあまり議論されておらず、このプロセスを調査するドキュメントはほとんどありません。 私はそれを変えるためにここにいます。 このJavaクラスのチュートリアルでは、このプロセスを段階的に説明し、このすばらしいテクニックを習得するのに役立ちます。 Javaクラスのリロードを実装するには細心の注意が必要ですが、その方法を学ぶことで、Java開発者としてもソフトウェアアーキテクトとしても大きなリーグに入ることができます。 また、Javaの最も一般的な10の間違いを回避する方法を理解しても問題はありません。
ワークスペースの設定
このチュートリアルのすべてのソースコードは、ここのGitHubにアップロードされています。
このチュートリアルに従ってコードを実行するには、Maven、Git、およびEclipseまたはIntelliJIDEAのいずれかが必要です。
Eclipseを使用している場合:
- コマンド
mvn eclipse:eclipse
を実行して、Eclipseのプロジェクトファイルを生成します。 - 生成されたプロジェクトをロードします。
- 出力パスを
target/classes
に設定します。
IntelliJを使用している場合:
- プロジェクトの
pom
ファイルをインポートします。 - IntelliJは、例を実行しているときに自動コンパイルされないため、次のいずれかを行う必要があります。
- IntelliJ内で例を実行すると、コンパイルするたびに
Alt+BE
を押す必要があります run_example*.bat
を使用して、IntelliJの外部で例を実行します。 IntelliJのコンパイラ自動コンパイルをtrueに設定します。 その後、Javaファイルを変更するたびに、IntelliJはそれを自動コンパイルします。
例1:Javaクラスローダーを使用したクラスのリロード
最初の例では、Javaクラスローダーの一般的な理解が得られます。 これがソースコードです。
次のUser
クラス定義があるとします。
public static class User { public static int age = 10; }
次のことができます。
public static void main(String[] args) { Class<?> userClass1 = User.class; Class<?> userClass2 = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example1.StaticInt$User"); ...
このチュートリアルの例では、2つのUser
クラスがメモリにロードされます。 userClass1
はJVMのデフォルトのクラスローダーによってロードされ、 userClass2
はDynamicClassLoader
を使用してロードされます。DynamicClassLoaderはカスタムクラスローダーであり、そのソースコードはGitHubプロジェクトでも提供されます。これについては、以下で詳しく説明します。
main
方法の残りは次のとおりです。
out.println("Seems to be the same class:"); out.println(userClass1.getName()); out.println(userClass2.getName()); out.println(); out.println("But why there are 2 different class loaders:"); out.println(userClass1.getClassLoader()); out.println(userClass2.getClassLoader()); out.println(); User.age = 11; out.println("And different age values:"); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass1)); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass2)); }
そして出力:
Seems to be the same class: qj.blog.classreloading.example1.StaticInt$User qj.blog.classreloading.example1.StaticInt$User But why there are 2 different class loaders: qj.util.lang.DynamicClassLoader@3941a79c sun.misc.Launcher$AppClassLoader@1f32e575 And different age values: 11 10
ここでわかるように、 User
クラスは同じ名前ですが、実際には2つの異なるクラスであり、独立して管理および操作できます。 年齢の値は静的として宣言されていますが、2つのバージョンがあり、各クラスに個別にアタッチされており、個別に変更することもできます。
通常のJavaプログラムでは、 ClassLoader
はクラスをJVMに取り込むポータルです。 あるクラスで別のクラスをロードする必要がある場合、ロードを実行するのはClassLoader
のタスクです。
ただし、このJavaクラスの例では、 DynamicClassLoader
という名前のカスタムClassLoader
を使用して、 User
クラスの2番目のバージョンをロードします。 DynamicClassLoader
の代わりに、デフォルトのクラスローダーを再度使用する場合(コマンドStaticInt.class.getClassLoader()
を使用)、ロードされたすべてのクラスがキャッシュされるため、同じUser
クラスが使用されます。
DynamicClassLoader
通常のJavaプログラムには複数のクラスローダーが存在する可能性があります。 メインクラスをロードするClassLoader
がデフォルトであり、コードから、必要な数のクラスローダーを作成して使用できます。 したがって、これがJavaでのクラスのリロードの鍵となります。 DynamicClassLoader
は、このチュートリアル全体の中でおそらく最も重要な部分であるため、目標を達成する前に、動的クラスの読み込みがどのように機能するかを理解する必要があります。
ClassLoader
のデフォルトの動作とは異なり、 DynamicClassLoader
はより積極的な戦略を継承します。 通常のクラスローダーは、親のClassLoader
に優先順位を与え、親がロードできないクラスのみをロードします。 これは通常の状況には適していますが、私たちの場合には適していません。 代わりに、 DynamicClassLoader
は、親への権利を放棄する前に、すべてのクラスパスを調べて、ターゲットクラスを解決しようとします。
上記の例では、 DynamicClassLoader
は"target/classes"
(現在のディレクトリ内)という1つのクラスパスのみで作成されているため、その場所にあるすべてのクラスをロードできます。 そこにないすべてのクラスについては、親クラスローダーを参照する必要があります。 たとえば、 StaticInt
クラスにString
クラスをロードする必要があり、クラスローダーはJREフォルダーのrt.jar
にアクセスできないため、親クラスローダーのString
クラスが使用されます。
次のコードは、 DynamicClassLoader
の親クラスであるAggressiveClassLoader
からのものであり、この動作が定義されている場所を示しています。
byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }
DynamicClassLoader
の次のプロパティに注意してください。
- ロードされたクラスは、デフォルトのクラスローダーによってロードされた他のクラスと同じパフォーマンスおよび他の属性を持っています。
-
DynamicClassLoader
は、ロードされたすべてのクラスおよびオブジェクトと一緒にガベージコレクションできます。
同じクラスの2つのバージョンをロードして使用できるため、現在、古いバージョンをダンプし、新しいバージョンをロードして置き換えることを検討しています。 次の例では、まさにそれを…継続的に行います。
例2:クラスを継続的にリロードする
この次のJavaの例は、JREがクラスを永久にロードおよびリロードでき、古いクラスがダンプされてガベージコレクションが行われ、新しいクラスがハードドライブからロードされて使用できることを示しています。 これがソースコードです。
メインループは次のとおりです。
public static void main(String[] args) { for (;;) { Class<?> userClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example2.ReloadingContinuously$User"); ReflectUtil.invokeStatic("hobby", userClass); ThreadUtil.sleep(2000); } }
2秒ごとに、古いUser
クラスがダンプされ、新しいクラスがロードされ、そのメソッドhobby
が呼び出されます。
User
クラスの定義は次のとおりです。
@SuppressWarnings("UnusedDeclaration") public static class User { public static void hobby() { playFootball(); // will comment during runtime // playBasketball(); // will uncomment during runtime } // will comment during runtime public static void playFootball() { System.out.println("Play Football"); } // will uncomment during runtime // public static void playBasketball() { // System.out.println("Play Basketball"); // } }
このアプリケーションを実行するときは、 User
クラスのコードで示されたコードにコメントを付けたりコメントを外したりする必要があります。 最新の定義が常に使用されることがわかります。
出力例を次に示します。
... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball
DynamicClassLoader
の新しいインスタンスが作成されるたびに、最新のクラスファイルを出力するようにEclipseまたはIntelliJを設定したtarget/classes
フォルダーからUser
クラスが読み込まれます。 すべての古いDynamicClassLoader
と古いUser
クラスはリンクが解除され、ガベージコレクターの対象になります。
JVM HotSpotに精通している場合は、クラス構造を変更して再ロードすることもできることに注意してplayFootball
メソッドを削除し、 playBasketball
メソッドを追加します。 これは、メソッドのコンテンツのみを変更できる、またはクラスを再ロードできないHotSpotとは異なります。
クラスをリロードできるようになったので、一度に多くのクラスをリロードしてみます。 次の例で試してみましょう。
例3:複数のクラスをリロードする
この例の出力は例2と同じですが、コンテキスト、サービス、およびモデルオブジェクトを使用して、よりアプリケーションに似た構造でこの動作を実装する方法を示します。 この例のソースコードはかなり大きいので、ここではその一部のみを示しました。 完全なソースコードはこちらです。
main
方法は次のとおりです。
public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }
そして、メソッドcreateContext
:
private static Object createContext() { Class<?> contextClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example3.ContextReloading$Context"); Object context = newInstance(contextClass); invoke("init", context); return context; }
メソッドinvokeHobbyService
:

private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue("hobbyService", context); invoke("hobby", hobbyService); }
そして、これがContext
クラスです。
public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }
そしてHobbyService
クラス:
public static class HobbyService { public User user; public void hobby() { user.hobby(); } }
この例のContext
クラスは、前の例のUser
クラスよりもはるかに複雑です。他のクラスへのリンクがあり、インスタンス化されるたびに呼び出されるinit
メソッドがあります。 基本的に、これは実際のアプリケーションのコンテキストクラス(アプリケーションのモジュールを追跡し、依存性注入を行う)と非常によく似ています。 したがって、このContext
クラスを、リンクされているすべてのクラスと一緒にリロードできることは、この手法を実際の生活に適用するための大きな一歩です。
クラスとオブジェクトの数が増えると、「古いバージョンを削除する」という手順もより複雑になります。 これは、クラスのリロードが非常に難しい最大の理由でもあります。 古いバージョンを削除するには、新しいコンテキストが作成されたら、古いクラスとオブジェクトへのすべての参照が削除されていることを確認する必要があります。 これにどのようにエレガントに対処しますか?
ここでのmain
メソッドはコンテキストオブジェクトを保持し、それがドロップする必要のあるすべてのものへの唯一のリンクです。 そのリンクを解除すると、コンテキストオブジェクトとコンテキストクラス、およびサービスオブジェクト…がすべてガベージコレクターの対象になります。
通常、クラスが非常に永続的であり、ガベージコレクションが行われない理由についての簡単な説明:
- 通常、すべてのクラスをデフォルトのJavaクラスローダーにロードします。
- クラスとクラスローダーの関係は双方向の関係であり、クラスローダーはロードしたすべてのクラスもキャッシュします。
- クラスローダーがライブスレッドに接続されている限り、すべて(ロードされたすべてのクラス)はガベージコレクターの影響を受けません。
- とはいえ、リロードするコードをデフォルトのクラスローダーによってすでにロードされているコードから分離できない限り、新しいコードの変更が実行時に適用されることはありません。
この例では、すべてのアプリケーションのクラスを再ロードするのは実際にはかなり簡単であることがわかります。 目標は、ライブスレッドから動的クラスローダーへのシンドロップ可能な接続を使用中に維持することです。 しかし、一部のオブジェクト(およびそれらのクラス)をリロードせず、リロードサイクル間で再利用したい場合はどうでしょうか。 次の例を見てみましょう。
例4:永続化されたクラススペースと再ロードされたクラススペースの分離
これがソースコードです。
main
方法:
public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }
したがって、ここでのトリックは、 ConnectionPool
クラスをロードし、リロードサイクルの外でインスタンス化し、永続化されたスペースに保持し、 Context
オブジェクトへの参照を渡すことであることがわかります。
createContext
メソッドも少し異なります。
private static Object createContext(ConnectionPool pool) { ExceptingClassLoader classLoader = new ExceptingClassLoader( (className) -> className.contains(".crossing."), "target/classes"); Class<?> contextClass = classLoader.load("qj.blog.classreloading.example4.reloadable.Context"); Object context = newInstance(contextClass); setFieldValue(pool, "pool", context); invoke("init", context); return context; }
今後は、サイクルごとにリロードされるオブジェクトとクラスを「リロード可能スペース」と呼び、リロードサイクル中にリサイクルも更新もされないオブジェクトとクラスを「永続スペース」と呼びます。 どのオブジェクトまたはクラスがどのスペースにとどまるかを非常に明確にする必要があります。したがって、これら2つのスペースの間に分離線を描画します。
図からわかるように、 Context
オブジェクトとUserService
オブジェクトはConnectionPool
オブジェクトを参照しているだけでなく、 Context
クラスとUserService
クラスもConnectionPool
クラスを参照しています。 これは非常に危険な状況であり、混乱や失敗につながることがよくあります。 ConnectionPool
クラスは、 DynamicClassLoader
によってロードされてはなりません。メモリにはConnectionPool
クラスが1つだけ存在する必要があります。これは、デフォルトのClassLoader
によってロードされるクラスです。 これは、Javaでクラス再ロードアーキテクチャを設計するときに注意することが非常に重要である理由の一例です。
DynamicClassLoader
が誤ってConnectionPool
クラスをロードした場合はどうなりますか? 次に、永続化されたスペースのConnectionPool
オブジェクトをContext
オブジェクトに渡すことはできません。これは、 Context
オブジェクトが、 ConnectionPool
という名前の別のクラスのオブジェクトを予期しているためですが、実際には別のクラスです。
では、 DynamicClassLoader
がConnectionPool
クラスをロードしないようにするにはどうすればよいでしょうか。 この例では、 DynamicClassLoader
を使用する代わりに、 ExceptingClassLoader
という名前のサブクラスを使用します。これにより、条件関数に基づいてロードがスーパークラスローダーに渡されます。
(className) -> className.contains("$Connection")
ここでExceptingClassLoader
使用しない場合、 DynamicClassLoader
はConnectionPool
クラスをロードします。これは、そのクラスが「 target/classes
」フォルダーにあるためです。 ConnectionPool
クラスがDynamicClassLoader
によって取得されないようにする別の方法は、 ConnectionPool
クラスを別のフォルダー(おそらく別のモジュール)にコンパイルすることです。これは個別にコンパイルされます。
スペースを選択するためのルール
さて、Javaクラスのロードジョブは本当に混乱します。 どのクラスを永続スペースに配置し、どのクラスを再読み込み可能スペースに配置するかをどのように決定しますか? ルールは次のとおりです。
- リロード可能スペース内のクラスは、永続化スペース内のクラスを参照できますが、永続化スペース内のクラスは、リロード可能スペース内のクラスを参照することはできません。 前の例では、リロード可能な
Context
クラスは永続化されたConnectionPool
クラスを参照していますが、ConnectionPool
はContext
を参照していません - クラスは、他のスペースのクラスを参照しない場合、どちらのスペースにも存在できます。 たとえば、
StringUtils
などのすべての静的メソッドを含むユーティリティクラスは、永続化されたスペースに1回ロードし、再ロード可能なスペースに個別にロードできます。
したがって、ルールはそれほど制限的ではないことがわかります。 2つのスペース間で参照されるオブジェクトを持つ交差クラスを除いて、他のすべてのクラスは、永続化されたスペースまたは再ロード可能なスペース、あるいはその両方で自由に使用できます。 もちろん、リロード可能なスペースのクラスだけが、リロードサイクルでリロードされるのを楽しみます。
したがって、クラスのリロードに関する最も困難な問題が処理されます。 次の例では、この手法を単純なWebアプリケーションに適用して、他のスクリプト言語と同じようにJavaクラスをリロードしてみます。
例5:小さな電話帳
これがソースコードです。
この例は、通常のWebアプリケーションの外観と非常によく似ています。 これは、AngularJS、SQLite、Maven、およびJetty EmbeddedWebServerを備えたシングルページアプリケーションです。
Webサーバーの構造内の再読み込み可能なスペースは次のとおりです。
Webサーバーは、実際のサーブレットへの参照を保持しません。実際のサーブレットは、再ロードされるために、再ロード可能なスペースにとどまる必要があります。 保持しているのはスタブサーブレットです。これは、サービスメソッドを呼び出すたびに、実行する実際のコンテキストで実際のサーブレットを解決します。
この例では、新しいオブジェクトReloadingWebContext
も導入しています。これは、通常のContextのようにすべての値をWebサーバーに提供しますが、 DynamicClassLoader
によって再ロードできる実際のコンテキストオブジェクトへの参照を内部的に保持します。 Webサーバーにスタブサーブレットを提供するのはこのReloadingWebContext
です。
ReloadingWebContext
は、実際のコンテキストのラッパーになります。
- 「/」へのHTTPGETが呼び出されたときに、実際のコンテキストをリロードします。
- Webサーバーにスタブサーブレットを提供します。
- 実際のコンテキストが初期化または破棄されるたびに、値を設定してメソッドを呼び出します。
- コンテキストをリロードするかどうか、およびリロードに使用されるクラスローダーを構成できます。 これは、アプリケーションを本番環境で実行するときに役立ちます。
永続スペースとリロード可能スペースを分離する方法を理解することは非常に重要であるため、2つのスペース間で交差している2つのクラスを次に示します。
Context
内のオブジェクトpublicF0 public F0<Connection> connF
のクラスqj.util.funct.F0
- 関数オブジェクトは、関数が呼び出されるたびに接続を返します。 このクラスは、
DynamicClassLoader
から除外されているqj.utilパッケージにあります。
Context
内のオブジェクトpublic F0<Connection> connF
のクラスjava.sql.Connection
- 通常のSQL接続オブジェクト。 このクラスは
DynamicClassLoader
のクラスパスに存在しないため、取得されません。
概要
このJavaクラスのチュートリアルでは、単一のクラスをリロードし、単一のクラスを継続的にリロードし、複数のクラスのスペース全体をリロードし、永続化する必要のあるクラスとは別に複数のクラスをリロードする方法を説明しました。 これらのツールを使用して、信頼性の高いクラスのリロードを実現するための重要な要素は、非常にクリーンな設計にすることです。 次に、クラスとJVM全体を自由に操作できます。
Javaクラスのリロードを実装することは、世界で最も簡単なことではありません。 しかし、あなたがそれを試してみて、ある時点であなたのクラスがその場でロードされているのを見つけたら、あなたはもうほとんどそこにいます。 システムの完全に優れたクリーンな設計を実現する前に、やるべきことはほとんどありません。
私の友達を頑張って、あなたの新しく見つけた超大国を楽しんでください!