チャットbotお試し用アプリ開発_9日目


# 目次

・本日の成果・考え
・最後に

### 本日の成果
SettingLoaderクラスを作成する上で、メソッドを起動させるための設定ファイル（.properties）をどうやって読み込むかの設計を考えました。

::: note info
このとき、開発環境で読み込みができるのは当たり前ですが、今後jarファイルとして配布した後の端末でも問題なく読み込める必要があります。
:::

Oracle公式ドキュメント（クラスパスについて）

https://docs.oracle.com/javase/tutorial/deployment/jar/downman.html

ということで、上記を参考にした限り、
読み込み対象にしたいファイルまたはフォルダをクラスパスに指定すれば、jarファイルにしたときに、その場所を読みに行くようにできるみたいです。

早速、resorcesフォルダをクラスパスに設定します。

画像にも記載していますが、
①Javaプロジェクトを右クリック
②「プロパティ」を選択
![スクリーンショット 2025-06-20 20.49.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3338426/a83c9c02-7423-47f6-98ec-61918385308c.png)


③「Javaのビルド・パス」をクリックする
④「ソース」タブを選択。
⑤「フォルダーの追加」をクリック
⑥下記画像のようになるので、対象を選択してOK
![スクリーンショット 2025-06-20 20.52.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3338426/a50ff42d-f84c-4bea-b8bd-04e2f85462c0.png)

#### 設定ファイルを読み込む処理
当初Loaderクラス内で①設定ファイル読込処理＋Factoryクラス呼び出し処理の2つをもつように考えていました。
しかし、設定ファイルは今後別の機能が追加になったときにも設定値を保持するファイルとして使用する可能性が高いと考え、設定ファイル読み込み処理を共通関数化することにしました。

::: note info
設計の時は、とりあえず今使えるようになれば良いと考えていましたが、将来性を考慮した構成にすると、なんだか「設計しているなぁ」と感じます。
※まあ基礎も基礎のレベルなんですが笑
:::

``` CommonFunction.java
    /**
     * 設定ファイルを読み込む（プロパティ読み込みのみ）
     * @param filePath 設定ファイルのパス
     * @return 
     * @throws IOException 読み込み失敗時
     */
	public static Properties load(String proFile ) throws IOException {
		Properties props = new Properties();
		try (InputStream proIn = CommonFunction.class.getClass().getClassLoader().getResourceAsStream(proFile)) {
			if(proIn == null) {
				throw new IllegalArgumentException(proFile+":読み込みファイルが見つかりません。");
			}
			props.load(proIn);
			return props;
		}
	}
```
::: note info
引数はクラスパスで指定したフォルダ内のファイル名になります。
将来的には、①ファイル形式に応じた読み込み処理　②アプリ起動時の選択に応じた読み込みファイルの変更　を考えています。
※Difyのユーザー用URLとチャットbotを調整する管理者用URLを使い分けるようになれば便利かなと考えました。
:::



では、Loaderクラスのソースになります。

``` SettingLoader.java
package app.windowView.config;

import java.util.Properties;

import app.util.CommonFunction;

public class SettingLoader {

	/**
	 * 読み込んだプロパティからDTOを生成し、Factoryに登録する
	 * @param props　呼び出し元で生成したPropertiesオブジェクト
	 * @return void
	 */
	public static void startSettings(Properties props) {
		int windWidth;
		int windHeight;
		if (props == null) {
			throw new IllegalStateException("設置ファイルが読み込まれていません。");
		}
		String logPass = props.getProperty("LOGPASS");
		String logFileExtension = props.getProperty("LOGFILEEX");
		String title = props.getProperty("WINDOW_TITLE");
		String windUrl = props.getProperty("WINDOW_URL");
		try {
			String windWidthStr = props.getProperty("WINDOW_WIDTH");
			String windHeightStr = props.getProperty("WINDOW_HEIGHT");
			windWidth = Integer.parseInt(windWidthStr);
			windHeight = Integer.parseInt(windHeightStr);
		} catch (NullPointerException | NumberFormatException e) {
			throw new IllegalStateException("エラーの原因：" + e);
		}

		//引数のNULLおよび空白チェック。int型は呼び出し元のLoaderクラスのメソッドでint型と制限しているのでnull混入はコンパイルで弾ける。
		//例外キャッチは呼び出し元。
		CommonFunction.checkNullBlank(logPass, logFileExtension, title, windUrl);

		AppSettingFactory factory = new AppSettingFactory();
		factory.createInstance(logPass, logFileExtension, title, windUrl, windWidth, windHeight);
	}
}

```
::: note info
処理としては、呼び出し元で設定ファイルの中身をPropertiesオブジェクトをsettingStartメソッドに渡すことで、FactoryのcreateInstanceメソッドが起動するようになっています。
:::
:::note warn
このSettingLoaderクラスですが、早速ある点に気づきました。
* AppSettingFactoryのcreateInstanceでインスタンス生成したが、SettingLoaderクラス内でnew しているから、<font color= red>このメソッド内でしかアクセスできない！</font>
解決策として、DtoインスタンスをSingletonにするという案がありましたが、以前不採用にした経緯もありますので不採用。</n>
またFactoryクラスのUTやり直しになるのが嫌なので、SettingLoaderだけを修正する方針にしました。
:::

というわけで、以下のように修正しました。

``` SettingLoader.java
package app.windowView.config;

import java.util.Properties;

import app.util.CommonFunction;

public class SettingLoader {

	/**
	 * 読み込んだプロパティからDTOを生成し、Factoryに登録する
	 * @param props　呼び出し元で生成したPropertiesオブジェクト
	 * @return void
	 */
	public static void startSettings(Properties props,AppSettingFactory fact) {
		int windWidth;
		int windHeight;
		if (props == null) {
			throw new IllegalStateException("設置ファイルが読み込まれていません。");
		}
		String logPass = props.getProperty("LOGPASS");
		String logFileExtension = props.getProperty("LOGFILEEX");
		String title = props.getProperty("WINDOW_TITLE");
		String windUrl = props.getProperty("WINDOW_URL");
		try {
			String windWidthStr = props.getProperty("WINDOW_WIDTH");
			String windHeightStr = props.getProperty("WINDOW_HEIGHT");
			windWidth = Integer.parseInt(windWidthStr);
			windHeight = Integer.parseInt(windHeightStr);
		} catch (NullPointerException | NumberFormatException e) {
			throw new IllegalStateException("エラーの原因：" + e);
		}

		//引数のNULLおよび空白チェック。int型は呼び出し元のLoaderクラスのメソッドでint型と制限しているのでnull混入はコンパイルで弾ける。
		//例外キャッチは呼び出し元。
		CommonFunction.checkNullBlank(logPass, logFileExtension, title, windUrl);

		fact.createInstance(logPass, logFileExtension, title, windUrl, windWidth, windHeight);
	}
}

```

#### 共通関数のloadメソッドのUTソース作成
テストパターンとしては以下を想定。
1. 引数のファイル名が正しく認識でき、Propertiesオブジェクトを返す。
※オブジェクトのNullチェックを行います。
※キーとバリューのチェックはSettingLoaderで全部行います。
1. 引数のファイル名が存在しないため、エラーとなる。
1. 引数のファイル名がNullのため、エラーとなる。
:::note info
このloadメソッドの責任としては、</n>
「設定ファイルの読み込み」としており、Propertiesオブジェクトの中身に関しては、実際に取り出すSettingLoaderクラスが責任を持つようにしようと考えました。</n>
共通関数なんで、ただ読み込むだけの処理にしたいなぁと思ってます。
:::

では、loadメソッドのテストソースです。

``` CommonFunctionTest.java
	/**
     * 設定ファイル読み込み処理のテスト
     */
	//テストパターン　引数のファイル名が正しく認識でき、Propertiesオブジェクトを返す。
	@Test
	public void test2_1() {
		String className = new Object() {
		}.getClass().getName();
		String resultOutput = className + "のloadメソッドのテストパターン2";
		String testFile = ".properties";
		try {
			out.println("**********************************************");
			out.println(resultOutput + "が開始されました。");
			Properties testProp =CommonFunction.load(testFile);
			if (testProp == null) {
				throw new IllegalStateException("load処理は実行されましたが、中身は存在しません。");
			}
			out.println(resultOutput + "が正常終了しました。");

		} catch (Exception e) {
			String resultError = String.format("エラーが発生しました。内容は{%s}", e);
			out.println(resultError);
			fail("テストパターン1失敗。");
		} finally {
			out.println(resultOutput + "が終了しました。");
			out.println("**********************************************");
		}
	}
	
	//テストパターン　引数のファイル名が存在しないため、エラーとなる。
	@Test
	public void test2_2() {
		String className = new Object() {
		}.getClass().getName();
		String resultOutput = className + "のloadメソッドのテストパターン2";
		String testFile = ".testProperties";
		try {
			out.println("**********************************************");
			out.println(resultOutput + "が開始されました。");
			Properties testProp =CommonFunction.load(testFile);
			if (testProp == null) {
				throw new IllegalStateException("load処理は実行されましたが、中身は存在しません。");
			}
			out.println(resultOutput + "が正常終了しました。");

		} catch (Exception e) {
			String resultError = String.format("エラーが発生しました。内容は{%s}", e);
			out.println(resultError);
		} finally {
			out.println(resultOutput + "が終了しました。");
			out.println("**********************************************");
		}
	}
	//テストパターン　引数のファイル名がNullのため、エラーとなる。
	@Test
	public void test2_3() {
		String className = new Object() {
		}.getClass().getName();
		String resultOutput = className + "のloadメソッドのテストパターン2";
		String testFile = null;
		try {
			out.println("**********************************************");
			out.println(resultOutput + "が開始されました。");
			Properties testProp =CommonFunction.load(testFile);
			if (testProp == null) {
				throw new IllegalStateException("load処理は実行されましたが、中身は存在しません。");
			}
			out.println(resultOutput + "が正常終了しました。");

		} catch (Exception e) {
			String resultError = String.format("エラーが発生しました。内容は{%s}", e);
			out.println(resultError);
		} finally {
			out.println(resultOutput + "が終了しました。");
			out.println("**********************************************");
		}
	}
```

遅れましたが、テストのために設定した.propertiesです。
```.properties
# Window表示処理の設定
LOGPATH=testLogPath
LOGFILEEX=.log
WINDOW_TITLE=チャットbot_Dify
WINDOW_URL=https://example.com
WINDOW_WIDTH=800
WINDOW_HEIGHT=600
```

さて、テスト実行。

![スクリーンショット 2025-06-21 10.13.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3338426/d959178c-efb5-4ac8-84ce-a1e6b011ce57.png)

**はい、エラーと。**

``` エラーコード. 
Cannot invoke "java.lang.ClassLoader.getResourceAsStream(String)" because the return value of "java.lang.Class.getClassLoader()" is null}
```

``` CommonFunction.java
	public static Properties load(String proFile) throws IOException {
		Properties props = new Properties();
        //ここのTryに入る前の.getClassLoader()でnullとなっている。
		try (InputStream proIn = CommonFunction.class.getClass().getClassLoader().getResourceAsStream(proFile)) {
			if(proIn == null) {
				throw new IllegalArgumentException(proFile+":読み込みファイルが見つかりません。");
			}
			props.load(proIn);
			return props;
		}
	}
```

::: note info
つまり、クラスパス設定できてないか読めない状況でエラーになっているのかなと想定。
<font color= red>※エラー解決後の私からみると、「ちゃんとエラー文をみてくれ！」と言いたくなりますが、一旦ご容赦ください。</font>
以下の点を確認しました。
・クラスパス自体の設定はできているか？
・クラスパス設定できたフォルダの出力フォルダをチェック。
:::

#### クラスパスチェック
Eclipseの設定がよくわからないので、.classpathで直接確認。
``` .classpath
<classpath>
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21">
		<attributes>
			<attribute name="module" value="true"/>
		</attributes>
	</classpathentry>
	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
	<classpathentry kind="src" path="src"/>
	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/JavaFX"/>
	<classpathentry kind="output" path="bin"/>
</classpath>
```

`<classpathentry kind="src" path="src"/> `
`<classpathentry kind="output" path="bin"/>`
この2つから、srcまでは、クラスパスが通っており、binフォルダにクラスパスが通っているファイルが出力されていると。

#### binフォルダ配下チェック
![スクリーンショット 2025-06-21 10.46.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3338426/41c122cd-f408-4c8f-930f-8e30f07fef37.png)

::: note warn
画像はすでに修正対応済みのもので申し訳ありません。
最初に確認した時点では、.propertiesという名前のため、bin/resources配下には何も存在していませんでした。
ファイル名を変更しました。「.properties」→「app.properties」
:::

さて、これでOKかと思い再度テスト実施。
**結果は同様でした😢**

ちょっと本当にクラスパスを読めているか確認。
```ClassPathTest.java
	    public static void main(String[] args) {
            CommonFunction.class.getResource("/resources/app.properties");
	    	System.out.println("Resource URL: " + resourceUrl);

```

```実行結果.
Resource URL: file:~~~~~~~~~~~~~~~~/bin/resources/app.properties
```
::: note info
上記の感じで出力できるのでクラスパスは問題なさそう。
:::

なら、呼び出し方の問題だろうと思い公式ドキュメントチェック

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Class.html#getClassLoader()


>パブリッククラスローダー getClassLoader()
クラスのクラスローダーを返します。一部の実装では、ブートストラップクラスのローダーを表すためにnullを使用することがあります。このクラスがブートストラップクラスローダーによってロードされた場合、このメソッドはそのような実装でnullを返します。

::: note info
ん？　ブーストラップクラスってなんだ？で調べてみると
「Javaの中核クラス（たとえば String や Integer など）は、JVMが最初から持っている特別な仕組み（ブートストラップクラスローダー）」
:::

さらに、
```
CommonFunction.class.getClass()
```
CommonFunction.class.getClass() は Class.class を返すみたいで、<font color=red>Class.class は java.lang.Class クラス自身の Class オブジェクト</font>という訳です。
つまり、String型（ブーストラップクラス）のクラスオブジェクトをgetClassLoaderしていたからnullだったと。

はい、オブジェクトの取得の方法を修正しました。

``` CommonFunction.java
    /**
     * 設定ファイルを読み込む（プロパティ読み込みのみ）
     * @param filePath 設定ファイルのパス
     * @return 
     * @throws IOException 読み込み失敗時
     */
	public static Properties load(String proFile) throws IOException {
		Properties props = new Properties();
//		try (InputStream proIn = CommonFunction.class.getClass().getClassLoader().getResourceAsStream(proFile)) {
			try (InputStream proIn = CommonFunction.class.getResourceAsStream(proFile)) {
			if(proIn == null) {
				throw new IllegalArgumentException(proFile+":読み込みファイルが見つかりません。");
			}
			props.load(proIn);
			return props;
		}
	}
```

では、再度テスト実行

![スクリーンショット 2025-06-21 11.17.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3338426/b75cb25a-8e13-4957-b68a-021291a22255.png)

テストOKです。
### 最後に
今日はLoaderもテストしようと思いましたが、一旦ここで終了します。
クラスパスの設定とかがかなり面倒でした。
しかし、ブーストラップクラスの概念とgetClassLoaderの仕様を学べました。
というか最初のエラーの原因の検討時点で、ちゃんとエラー文を読まないから無駄な検証をする羽目に😢。
ここまでお付き合いありがとうございました。