Javaでプロパティファイルの読み込みでMissingResourceExceptionが起きたんだが...

f:id:ts0818:20201218210059p:plain

www.itmedia.co.jp

 窪田製薬ホールディングスは12月17日、近視を抑制、治療する眼鏡型デバイス「クボタメガネ」のプロトタイプを、子会社の米クボタビジョンが完成させたと発表した。

近視を治すメガネ、プロトタイプが完成 網膜に特殊な光を当てて刺激 - ITmedia NEWS

⇧ な、何やて~!

本当に治療効果があるとすれば、衝撃なんですけど~! 見えなかったものが見えちゃうようになる時代が来ちゃいますか~?

というわけで、今回もJavaについてです。

レッツトライ~。 

 

ResourceBundle.getBundleの引数baseNameの指定が分かり辛い...

今回も、以下サイト様を参考にさせていただきました。

github.com

⇧「038:国際化」って章ですかね。

で、「.propeties」ファイルが見つからんってエラーが起きましてね...

Exception in thread "main" java.util.MissingResourceException: Can't find bundle for base name language, locale ja_JP
        at java.base/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2055)
        at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1689)
        at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1593)
        at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1556)
        at java.base/java.util.ResourceBundle.getBundle(ResourceBundle.java:932)
        at java_one_hundred_nock.thirtyeight.Answer038.main(Answer038.java:23)

Javaプロジェクトの構成としては、以下のようにしてみたんですが。

f:id:ts0818:20201218124133p:plain

Javaのコーディングは以下のような感じにしたんですが、「ResourceBundle.getBundle」の引数に指定した「fileName」が見つからんと...

package java_one_hundred_nock.thirtyeight;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;

public class Answer038 {

	public static void main(String[] args) {
		// TODO 自動生成されたメソッド・スタブ
		if (args.length < 1) {
			System.err.println("引数に置換後文字列を入力し、再実行してください");
			System.out.println(1);
		}

		final String afterWord = args[0];

		final ResourceBundle resource;

		final String fileName = "language";

		if ("JP".equals(Locale.getDefault().getCountry())) {
			resource = ResourceBundle.getBundle(fileName, Locale.JAPAN);
		} else {
			resource = ResourceBundle.getBundle(fileName, Locale.US);
		}
		System.out.println(MessageFormat.format(resource.getString("sentence"), afterWord));
	}

}

⇧ ってな感じなんですが、「.properties」ファイルは存在するから!って思ってもエラーになるので、致し方ないから、Javadoc(ドキュメント)を確認してみました。

docs.oracle.com

public static final ResourceBundle getBundle(String baseName,
                                             Locale locale)

パラメータ:

  • baseName - リソース・バンドルの基底名。完全指定クラス名
  • locale - リソース・バンドルが必要なロケール

https://docs.oracle.com/javase/jp/8/docs/api/java/util/ResourceBundle.html#getBundle-java.lang.String-java.util.Locale-

⇧ ってな感じで、 

  • リソース・バンドルの基底名
  • 完全指定クラス名

のどちらかでリソースファイルを取得できるはずですと。

で、ネットの情報とか見ると、「リソース・バンドルの基底名」で取得できるって情報がほとんどなんですよね、今回取得できないわけなんだけど...

で、javadoc(ドキュメント)を読んでみてもね、この「基底名」ってのが何なのかについての説明がね、無いんですよ...

リソース・バンドルはファミリに属しています。そのファミリでは、メンバーは共通の基底名を共有していますが、ファミリ名にはロケールを識別する追加コンポーネントも含まれています。たとえば、リソース・バンドルのファミリの基底名は「MyResources」です。ファミリは、ファミリと同じ名前(MyResources)で、特定のロケールがサポートされない場合は、最後の手段のバンドルとして使用される、デフォルトのリソース・バントルを持つ必要があります。また、ファミリは、必要なだけの数のロケール固有のメンバーを提供することができます。たとえば、「MyResources_de」と名付けられたドイツのメンバーなどです。

https://docs.oracle.com/javase/jp/8/docs/api/java/util/ResourceBundle.html

⇧「ファミリ」ってのも唐突に出てくるしね...

何やねん「ファミリ」って...「シルバニアファミリー」か!

っていう心のツッコミはさておき、本当、何だろう、この何の説明もしないことをされちゃうとドキュメントの意味ないし、ドキュメントを読む気がなくなるので止めて欲しい...

ちなみに、 

www.symmetric.co.jp

TomcatはWebアプリケーション専用のクラスローダが使われ、直接起動ではSun JVMのアプリケーション起動用クラスローダが使われている。クラスローダに違いがあることが分かったが、どちらもURLClassLoaderのサブクラスという点では同じ。

MissingResourceExceptionの解決法│Java│プログラミング│SYMMETRICソフトウェア開発ブログ

ServletJSPなどから呼び出されたコードと、mainメソッドから呼び出されたコードとの間には、動作に微妙な違いがあることが分かった。この違いは些細な問題に見えるかもしれないけど、Webアプリケーション開発者にとってはまさに衝撃的!深刻な問題が懸念される。

MissingResourceExceptionの解決法│Java│プログラミング│SYMMETRICソフトウェア開発ブログ

⇧「アプリケーションサーバー」で実施した場合で挙動が代わってくるそうな...

確かに衝撃ですな、「単体テスト」の信頼性が地に墜ちたということですかね...

 

完全指定クラス名で動いた

結局、ResourceBundle.getBundle の「リソース検索」の挙動が謎過ぎるので「リソース・バンドルの基底名」で「MissingResourceException」になってしまう原因が分からないのだが、「完全指定クラス名」にしたら動きました。

ちなみに、

注: baseName引数は完全指定されたクラス名である必要があります。ただし、以前のバージョンとの互換性を保つために、SunのJava SE Runtime Environmentはそのことを検証しません。そのため、完全指定されたクラス名(「.」を使用)の代わりにパス名(「/」を使用)を指定して、PropertyResourceBundleにアクセスすることができます。

https://docs.oracle.com/javase/jp/8/docs/api/java/util/ResourceBundle.html#getBundle-java.lang.String-java.util.Locale-java.lang.ClassLoader-

⇧ フォーマットは「.」「/」のどちらでも良いみたい。

というわけで、今回は「Javaプロジェクトの構成」が以下となっているのと、

f:id:ts0818:20201218182333p:plain

Eclipseを使ってる場合は「クラスパス」が「[Javaプロジェクト名]/.classpath」ってファイルに記載があり(「Mavenプロジェクト」「Gradleプロジェクト」とかだとまた違ってくるとは思うけど)、

<?xml version="1.0" encoding="UTF-8"?>
<classpath>
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
		<attributes>
			<attribute name="module" value="true"/>
		</attributes>
	</classpathentry>
	<classpathentry kind="src" path="src"/>
	<classpathentry kind="output" path="bin"/>
</classpath>

⇧ となっていてるので、「[Javaプロジェクト名]/src」配下の「java_one_hundred_nock.thirtyeight」パッケージ配下に「.propeties」ファイルを配置しているので、今回の「.propeties」ファイルの「完全指定クラス名」は、

java_one_hundred_nock.thirtyeight.language    

⇧ か、

java_one_hundred_nock/thirtyeight/language    

⇧ のようになるみたいね。

なので、改めて、「.java」ファイルのソースコードを修正して、以下のような感じにしたら動きました。

java_one_hundred_nock.thirtyeight;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;

public class Answer038 {

	public static void main(String[] args) {
		// TODO 自動生成されたメソッド・スタブ
		if (args.length < 1) {
			System.err.println("引数に置換後文字列を入力し、再実行してください");
			System.out.println(1);
		}

		final String afterWord = args[0];

		final ResourceBundle resource;

		final String fileName = "java_one_hundred_nock.thirtyeight.language";

		if ("JP".equals(Locale.getDefault().getCountry())) {
			resource = ResourceBundle.getBundle(fileName, Locale.JAPAN);
		} else {
			resource = ResourceBundle.getBundle(fileName, Locale.US);
		}
		System.out.println(MessageFormat.format(resource.getString("sentence"), afterWord));
	}

}    

一応、「.bat」ファイルも掲載。

@ECHO OFF
@REM -----------------------------
@REM java-100practies
@REM answer038.bat
@REM author: ts0818
@REM -----------------------------

@REM Java実行時の引数
SET word="Answer038"
@REM 使用するjavacのパス
SET javac="C:\Eclipse-2020-06\pleiades-2020-06-java-win-64bit-jre_20200702\pleiades\java\11\bin\javac.exe"
@REM 使用するjavaのパス
SET java="C:\Eclipse-2020-06\pleiades-2020-06-java-win-64bit-jre_20200702\pleiades\java\11\bin\java.exe"
@REM バッチ実行時のディレクトリ
SET currentDirectory=%~dp0
@REM java実行時のオプションの-classpath に指定するクラスパス
SET classPath=C:\Eclipse-2020-06\pleiades-2020-06-java-win-64bit-jre_20200702\pleiades\workspace\java_one_hundred_nock\src
@REM 実行するクラスファイルのクラスのパッケージ
SET package=java_one_hundred_nock.thirtyeight.

@REM ディレクトリの移動
cd %currentDirectory%
@REM バッチで実行した結果を書き込むファイルの出力場所
SET batchLog=C:\Eclipse-2020-06\pleiades-2020-06-java-win-64bit-jre_20200702\pleiades\workspace\java_one_hundred_nock\src\java_one_hundred_nock\thirtyeight\error.txt

@REM コンパイル
%javac% -encoding UTF-8  Answer038.java > %batchLog% 2>&1

@REM プロパティファイルの文字コードを変換
@REM Java9以降、ResourceBundle のデフォルト文字コードが UTF-8になったため、native2asciiの対応は不要(【参考】"http://yanok.net/2017/07/java-9-resourcebundle-utf-8.html")
@REM native2ascii -encoding UTF-8 language_en.properties output_en.propeties
@REM native2ascii -encoding UTF-8 language_en.properties output_en.propeties

@REM プログラムの実行
%java% -Dfile.encoding=UTF-8 -classpath %classPath% java_one_hundred_nock.thirtyeight.Answer038 %word% > %batchLog% 2>&1

IF ERRORLEVEL 1 GOTO FAILURE

GOTO SUCCESS

:FAILURE
EXIT /B %ERRORLEVEL%

:SUCCESS

で、コマンドプロンプトで実施。

Windowsの「エクスプローラー」から「バッチファイル」をダブルクリックでもOK(今回だと「answer038.bat」が「バッチファイル」)

f:id:ts0818:20201218190753p:plain

バッチの実行結果の出力先のファイルである「error.txt」を確認してみると、問題なく「.java」ファイルのコードが動いたのが確認できました。

f:id:ts0818:20201218190952p:plain

Windowsの「エクスプローラー」でも確認してみると、「error.txt」のサイズが1KBになってるので、バッチファイルの実行結果が書き込まれてるが分かります。

f:id:ts0818:20201218200054p:plain

 

結局のところ、何故「リソース・バンドルの基底名」の指定だとエラーになるのかは分からんのだけどね...

まぁ、Eclipseが影響してそうな気もするけども、それにしても「ResourceBundle.getBundle」のjavadoc(ドキュメント)が酷い...

せっかく2020年でJava生誕25周年とか迎えたみたいなんで、標準APIjavadoc(ドキュメント)をもうちょい何とかしてくれないもんですかね...

今回はこのへんで。