高度なJavaクラスチュートリアル:クラスのリロードのガイド

公開: 2022-03-11

Java開発プロジェクトでは、一般的なワークフローでは、クラスが変更されるたびにサーバーを再起動する必要があり、誰もそれについて不満を言うことはありません。 それは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のデフォルトのクラスローダーによってロードされ、 userClass2DynamicClassLoaderを使用してロードされます。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クラスが使用されます。

デフォルトのJavaClassLoaderとDynamicClassLoaderの動作を調べることは、このJavaクラスのチュートリアルを活用するための鍵です。

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クラスはリンクが解除され、ガベージコレクターの対象になります。

高度なJava開発者は、アクティブであるかリンクされていないかにかかわらず、動的なクラスの再ロードを理解することが重要です。

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クラスを、リンクされているすべてのクラスと一緒にリロードできることは、この手法を実際の生活に適用するための大きな一歩です。

高度なJavaエンジニアでさえ、Javaクラスの再ロードは困難です。

クラスとオブジェクトの数が増えると、「古いバージョンを削除する」という手順もより複雑になります。 これは、クラスのリロードが非常に難しい最大の理由でもあります。 古いバージョンを削除するには、新しいコンテキストが作成されたら、古いクラスとオブジェクトへのすべての参照が削除されていることを確認する必要があります。 これにどのようにエレガントに対処しますか?

ここでの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つのスペースの間に分離線を描画します。

適切に処理されない限り、このJavaクラスのロードの分離は失敗につながる可能性があります。

図からわかるように、 ContextオブジェクトとUserServiceオブジェクトはConnectionPoolオブジェクトを参照しているだけでなく、 ContextクラスとUserServiceクラスもConnectionPoolクラスを参照しています。 これは非常に危険な状況であり、混乱や失敗につながることがよくあります。 ConnectionPoolクラスは、 DynamicClassLoaderによってロードされてはなりません。メモリにはConnectionPoolクラスが1つだけ存在する必要があります。これは、デフォルトのClassLoaderによってロードされるクラスです。 これは、Javaでクラス再ロードアーキテクチャを設計するときに注意することが非常に重要である理由の一例です。

DynamicClassLoaderが誤ってConnectionPoolクラスをロードした場合はどうなりますか? 次に、永続化されたスペースのConnectionPoolオブジェクトをContextオブジェクトに渡すことはできません。これは、 Contextオブジェクトが、 ConnectionPoolという名前の別のクラスのオブジェクトを予期しているためですが、実際には別のクラスです。

では、 DynamicClassLoaderConnectionPoolクラスをロードしないようにするにはどうすればよいでしょうか。 この例では、 DynamicClassLoaderを使用する代わりに、 ExceptingClassLoaderという名前のサブクラスを使用します。これにより、条件関数に基づいてロードがスーパークラスローダーに渡されます。

 (className) -> className.contains("$Connection")

ここでExceptingClassLoader使用しない場合、 DynamicClassLoaderConnectionPoolクラスをロードします。これは、そのクラスが「 target/classes 」フォルダーにあるためです。 ConnectionPoolクラスがDynamicClassLoaderによって取得されないようにする別の方法は、 ConnectionPoolクラスを別のフォルダー(おそらく別のモジュール)にコンパイルすることです。これは個別にコンパイルされます。

スペースを選択するためのルール

さて、Javaクラスのロードジョブは本当に混乱します。 どのクラスを永続スペースに配置し、どのクラスを再読み込み可能スペースに配置するかをどのように決定しますか? ルールは次のとおりです。

  1. リロード可能スペース内のクラスは、永続化スペース内のクラスを参照できますが、永続化スペース内のクラスは、リロード可能スペース内のクラスを参照することはできません。 前の例では、リロード可能なContextクラスは永続化されたConnectionPoolクラスを参照していますが、 ConnectionPoolContextを参照していません
  2. クラスは、他のスペースのクラスを参照しない場合、どちらのスペースにも存在できます。 たとえば、 StringUtilsなどのすべての静的メソッドを含むユーティリティクラスは、永続化されたスペースに1回ロードし、再ロード可能なスペースに個別にロードできます。

したがって、ルールはそれほど制限的ではないことがわかります。 2つのスペース間で参照されるオブジェクトを持つ交差クラスを除いて、他のすべてのクラスは、永続化されたスペースまたは再ロード可能なスペース、あるいはその両方で自由に使用できます。 もちろん、リロード可能なスペースのクラスだけが、リロードサイクルでリロードされるのを楽しみます。

したがって、クラスのリロードに関する最も困難な問題が処理されます。 次の例では、この手法を単純なWebアプリケーションに適用して、他のスクリプト言語と同じようにJavaクラスをリロードしてみます。

例5:小さな電話帳

これがソースコードです。

この例は、通常のWebアプリケーションの外観と非常によく似ています。 これは、AngularJS、SQLite、Maven、およびJetty EmbeddedWebServerを備えたシングルページアプリケーションです。

Webサーバーの構造内の再読み込み可能なスペースは次のとおりです。

Webサーバーの構造内の再読み込み可能なスペースを完全に理解すると、Javaクラスの読み込みをマスターするのに役立ちます。

Webサーバーは、実際のサーブレットへの参照を保持しません。実際のサーブレットは、再ロードされるために、再ロード可能なスペースにとどまる必要があります。 保持しているのはスタブサーブレットです。これは、サービスメソッドを呼び出すたびに、実行する実際のコンテキストで実際のサーブレットを解決します。

この例では、新しいオブジェクトReloadingWebContextも導入しています。これは、通常のContextのようにすべての値をWebサーバーに提供しますが、 DynamicClassLoaderによって再ロードできる実際のコンテキストオブジェクトへの参照を内部的に保持します。 Webサーバーにスタブサーブレットを提供するのはこのReloadingWebContextです。

ReloadingWebContextは、JavaクラスのリロードプロセスでWebサーバーへのスタブサーブレットを処理します。

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クラスのリロードを実装することは、世界で最も簡単なことではありません。 しかし、あなたがそれを試してみて、ある時点であなたのクラスがその場でロードされているのを見つけたら、あなたはもうほとんどそこにいます。 システムの完全に優れたクリーンな設計を実現する前に、やるべきことはほとんどありません。

私の友達を頑張って、あなたの新しく見つけた超大国を楽しんでください!