「only available on JDK 1.5 and higher」エラー対応
概要
古いバージョンのフレームワークを利用すると想定してないところで問題が発生したりしてパッチを作成する必要があったりします。本記事ではその中で「Context namespace element 'annotation-config' and its parser class [org.springframework.context.annotation.AnnotationConfigBeanDefinitionParser] are only available on JDK 1.5 and higher」エラーの紹介とその解決案をまとめてみました。
発生ケース
以下のバージョンで動いているウェブサービスを想定してみました。
- Java:openjdk-8-jdk
- Spring:2.5
- Tomcat:9
エラー内容と原因
エラー内容
Tomcatを起動で定義ファイルを読み込む際に以下のエラーメッセージが出てしまいます。
Misssing resource:/xxxxxxx/xxxxx.xml org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception parsing XML document from file [/xxxxx/xxxxx.xml]; nested exception is java.lang.IllegalStateException: Context namespace element 'annotation-config' and its parser class [org.springframework.context.annotation.AnnotationConfigBeanDefinitionParser] are only available on JDK 1.5 and higher
JDK 8を使用しているのにJDK1.5以降が必要と出ています。
原因
以下のSpring 2.5で利用しているJavaバージョンを判定している実装のJdkVersionクラスの一部の内容です。
static {
javaVersion = System.getProperty("java.version");
// version String should look like "1.4.2_10"
if (javaVersion.indexOf("1.7.") != -1) {
majorJavaVersion = JAVA_17;
}
else if (javaVersion.indexOf("1.6.") != -1) {
majorJavaVersion = JAVA_16;
}
else if (javaVersion.indexOf("1.5.") != -1) {
majorJavaVersion = JAVA_15;
}
else {
// else leave 1.4 as default (it's either 1.4 or unknown)
majorJavaVersion = JAVA_14;
}
}
このように、Spring 2.5(かなり古い)がリリースされた当初は、JDK1.7より後のJavaバージョンで実行されることを想定していなかったので。Java 8以降では上記のコードはデフォルトの1.4バージョンを想定しています。このため、アノテーション部分でエラーが発生されます。
対応方法
[!TIP] 対応方法については今回のエラーと直接関係なく、jaraファイルの設定方法なので、興味がある方のみご覧になってください。
この実行環境ではTomcatで動くウェブサービスのことなので、先ずはTomcatのクラスローディングについて先に説明したいと思います。
Tomcatのクラスローディング順序
Apache Tomcatは複数のクラスローダを使用して、アプリケーションごとにライブラリの読み込みを制御して行います。
主なクラスローダ(上から順に親)
| クラスローダ | 説明 |
|---|---|
| Bootstrap ClassLoader | JDKの標準クラス |
| System ClassLoader | $JAVA_HOME/lib/extのクラスなど |
| Common ClassLoader | shared.loaderに設定されている${CATALINA_HOME}/lib, shared/libなど |
| Webapp ClassLoader | WEB-INF/lib, pom.xmlの依存 |
Tomcatはデフォルトで「親ローダ優先(parent-first)」の戦略を取るため、例えばTomcatディレクトリshared/lib(= Commonクラスローダ)にあるクラスがwebアプリ(war)内のクラスやライブラリよりも先にロードされる仕組みになっています。
具体例での挙動
例えば以下のような状況を考えてみます:
shared/lib/some-lib.jarにcom.example.MyClassがあるwebapps/yourapp/WEB-INF/lib/some-lib.jarにも同じ名前のcom.example.MyClassがある
この場合、アプリケーション起動時にクラスローダは親から探索するため、shared/libのバージョンのMyClassがロードされ、Webアプリ内部のものは 無視されます(クラス競合は発生しませんが、上書きもされません)。
対応
パッチの作成
使用しているSpringのライブラリ中のJdkVersionクラスを上書きして新たにライブラリを作成します。
先ずは仕様しているjarファイルを利用して作業フォルダを作成します。
mkdir spring-modifier cd spring-modifier jar xvf ../spring-2.5.x.jar
作業フォルダでorg/springframework/core/JdkVersion.javaファイルを作成します。
package org.springframework.core;
public abstract class JdkVersion
{
public static final int JAVA_13 = 0;
public static final int JAVA_14 = 1;
public static final int JAVA_15 = 2;
public static final int JAVA_16 = 3;
public static final int JAVA_17 = 4;
private static final String javaVersion;
private static final int majorJavaVersion;
static
{
javaVersion = System.getProperty("java.version");
if (javaVersion.indexOf("1.7.") != -1)
{
majorJavaVersion = JAVA_17;
} else if (javaVersion.indexOf("1.6.") != -1) {
majorJavaVersion = JAVA_16;
} else if (javaVersion.indexOf("1.5.") != -1) {
majorJavaVersion = JAVA_15;
} else if (javaVersion.indexOf("1.4.") != -1) { // new
majorJavaVersion = JAVA_14; // new
} else {
majorJavaVersion = JAVA_17; // changed from JAVA_14
}
}
public static String getJavaVersion()
{
return javaVersion;
}
public static int getMajorJavaVersion()
{
return majorJavaVersion;
}
public static boolean isAtLeastJava14()
{
return true;
}
public static boolean isAtLeastJava15()
{
return getMajorJavaVersion() >= JAVA_15;
}
public static boolean isAtLeastJava16()
{
return getMajorJavaVersion() >= JAVA_16;
}
}
新しいclassファイルをJava1.4でコンパイルします。
javac -source 1.4 org/springframework/core/JdkVersion.java
上書きしたclassファイルを含むjarファイルを作成します。
jar Mcf ../spring-modified.jar *
サービスに反映方法①
Tomcatのconf/catalina.propertiesファイルにあるshared.loaderプロパティを使って、Tomcatが起動時に共通で読み込むJARを指定する方法です。
shared.loader=${catalina.base}/shared/lib/*.jar
この設定を有効にすることで、shared/lib以下に配置したJARは、各Webアプリのクラスローダーから親クラスローダーとして読み込まれるため、Webアプリ内の同名JARより優先されるという特性を活かして、パッチを反映できます。
利点
- Webアプリ側のWARファイルを修正しなくてよい。
- Tomcatを再起動すれば反映される。
- 複数アプリにまたがって共通化できる。
注意点
- 依存関係の整合性が崩れやすい。特にWebアプリ側のJARと競合した場合、意図しない動作になる可能性あり。
- shared.loader が未設定のTomcatディストリビューションもあるので明示的に有効化が必要。
サービスに反映方法②
Mavenプロジェクトのpom.xmlで該当ライブラリの依存定義をバージョンアップまたはsystemスコープでローカルのパッチJARを指定する方法です。
例:特定バージョンへ差し替え
<dependency> <groupId>org.springframework</groupId> <artifactId>spring</artifactId> <version>2.5.x-modified</version> </dependency>
例:パッチJARを明示的に指定
<dependency> <groupId>org.springframework</groupId> <artifactId>spring</artifactId> <version>2.5.x-modified</version> <scope>system</scope> <systemPath>${project.basedir}/lib/spring-modified.jar</systemPath> </dependency>
一度、以下のようにローカルリポジトリにインストールして利用する方法もあります。
mvn install:install-file \ -Dfile=lib/spring-2.5.0-modified.jar \ -DgroupId=org.springframework \ -DartifactId=spring \ -Dversion=2.5.0-modified \ -Dpackaging=jar
利点
- アプリ単位で完結するので他アプリへの影響がない。
- CircleCI等によるCI/CDにそのまま組み込める。
- WARファイルに含まれるため、再現性が高い。
注意点
- WARファイルの再ビルドが必要。
- systemPathの利用は非推奨(将来のMavenでサポートされない可能性がある)。
- 正確に依存性を管理しないと元のJARと共存して重複エラーが発生することもある。
他の反映方法
以下のようにTomcat起動時にクラスパスの順序制御する方法等があります。
export CLASSPATH="/path/to/your/spring-modified.jar:$CLASSPATH"
Tomcatの起動スクリプトでパッチJARを先に読み込ませることで、デフォルトのクラスより優先できます。
注意点
shared.loaderに似た挙動だが、やや非標準。- 設定ミスや環境依存の不具合が起きやすい。
参考元
EasyMockでcmp、capture
概要
EasyMockを利用してテストケースを作成する際に以下の内容で対応方法を調べていました。
- mainのソースのクラスのequalsをオーバーライドする等、テストの為に修正はしたくない
- メソッド内部の変数をテスト内容に入れないので、expectで確認するメソッドのパラメターを判定したい
例のソース
Map<String, String> expectMap = new HashMap<String, String>(); ... sampleClass.test(eq(stringA), eq(intB), eq(mapC), eq(booleanD));
確認したい内容
- mapCのキー&値が一致
- mapCの特定のキーについては除外
eqのような判定をカスタマイズ
eqと比較内容をカスタマイズすれば「確認したい内容」のみ定義できるかと思いました調べたところ以下のような内容がありました。
こんな感じで作ってみました。expectMapにあるキーを基準で比較するのを想定しました。
// 比較内容を定義 private class MapComparator implements Comparator<Map<String, String>> { @Override public int compare(Map<String, String> expected, Map<String, String> actual) { for (String key : expected.keySet()) { if (!actual.containsKey(key)) return -1; if (!StringUtils.equals(expected.get(key), actual.get(key))) return 1; } return 0; } } ... // 使用例 Map<String, String> expectMap = new HashMap<String, String>(); expectMap.put("key1", "value1"); expectMap.put("key2", "value2"); MapComparator mapComparator = new MapComparator(); sampleClass.test(eq(stringA), eq(intB), EasyMock.cmp(expectMap, mapComparator, LogicalOperator.EQUAL), eq(booleanD)); EasyMock.expectLastCall(); ...
結果
結果としてダメでした。MapComparator#compareがテストメソッドのパラメーター分繰り返して呼ばれて、actualにtestメソッドの第一パラメーターから第四パラメーターが順番で入ってくることになります。
それでcast exceptionが発生されます。これを使えるのはパラメーターが一つの場合のみかと思います。
capture
cmpでパラメーターが複数の時の対応が出来ないことで調査を続けるとEasyMock.captureというものがありました。テストメソッドが呼ばれる時の値を指定した変数に格納(?)してくれるので後で値を比較が可能になります。
... // sampleClass.testが実行する時、第3番目のパラメターがcaptureFeatureSetTagへ格納される。 Capture<Map<String, String>> actualCapture = new Capture<Map<String,String>>(); sampleClass.test(eq(stringA), eq(intB), EasyMock.capture(actualCapture), eq(booleanD)); EasyMock.expectLastCall(); // テスト対象のメソッドを実行 replayAll(); testClass.excute(parameter); verifyAll(); // 使用例 Map<String, String> expectMap = new HashMap<String, String>(); expectMap.put("key1", "value1"); expectMap.put("key2", "value2"); // captureFeatureSetTagと比較("key3"を除外) Map<String, String> actualMap = actualCapture.getValue(); actualMap.remove("key3"); assertEquals(expectMap, actualMap); ...
結論
今回の対応ではEasyMock.captureを利用して対応ができましたが、個人的にはcmpもやってみたいなと思いました。
VMWareへWindows 11をインストール
概要
開発でテスト用で利用する目的でWindows 11が必要になってVMWareへインストールした手順や問題を整理してみました。
ISOをダウンロード
ダウンロード先の一番したにある「Windows 11 ディスク イメージ (ISO) をダウンロードする」からダウンロードします。

VMWareの設定
- HDD:80G
- Memory:4G
設定によりと思いますが、CMOSでCDからBootされるようにしている必要があります。VMを起動して直ぐEscキーを押したら入りました。
インストール
問題発生
普通にインストール手順で設定してインストールOSの種類を選択した後に以下の「このPCは、このバージョンのWindowsをインストールするために最小システム要件を満たしません。詳細については、https://aka.ms/WindowsSysReqを参照してください。」とメッセージが出てインストールが出来ない状態になりました。

ここで何を選択しても以下のメッセージが出ます。

問題解決
メッセージが出たら「Shift + F10」でコマンドプロンプトを立ち上げて「regedit」と入力してエンターを押すとレジストリエディターが立ち上がります。

レジストリエディターが立ち上がったら、「HKEY_LOCAL_MACHINE\SYSTEM\Setup」の下に「LabConfig」というキーを追加します。そのキーの中にDWORDで「BypassTPMCheck」と「BypassSecureBootCheck」を作成します。

両方の値もダブルクリックで「1」を設定しておきます。ここでのレジストリはインストーラ用の一時的なもので、再起動などで消えるので間違えたりしても影響はありません。
再起動はせずに追加できたらすべて画面を閉じてインストールを進めていきます。
完了
インストールできました。

Vagrantにmailhogの構築
概要
開発環境(Vagrant + ubuntu/trusty64)にメールの確認用でMailhogをインストールした話をまとめてみました。
Go言語のインストール
MailhogをインストールするためにGo言語をインストールします。
失敗
GUEST> sudo apt-get install golang-go
この方法でインストールするとOSのバージョンが低いせいかgoが1.2.1がインストールされてしまいます。このバージョンだとMailhogをインストールする際に以下のエラーが発生します。
package github.com/mailhog/MailHog
imports github.com/gorilla/pat
imports github.com/gorilla/context
imports github.com/gorilla/mux
imports context: unrecognized import path "context"
package github.com/mailhog/MailHog
imports github.com/gorilla/pat
imports github.com/gorilla/context
imports github.com/gorilla/mux
imports github.com/ian-kent/envconf
imports github.com/ian-kent/go-log/log
imports github.com/t-k/fluent-logger-golang/fluent
imports github.com/tinylib/msgp/msgp
imports github.com/philhofer/fwd
imports github.com/mailhog/MailHog-Server/api
imports github.com/gorilla/websocket
imports net/http/httptrace: unrecognized import path "net/http/httptrace"
成功した物
使用する基本ツールをインストールします。
GUEST> sudo DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" install git
GO言語をダウンロードします。
GUEST> curl -O https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz
圧縮ファイルを解凍して/usr/localへ移動します。
GUEST> tar -xvf go1.9.1.linux-amd64.tar.gz GUEST> sudo mv go /usr/local GUEST> rm go1.9.1.linux-amd64.tar.gz
環境設定を行います。
GUEST> touch /home/vagrant/.bash_profile GUEST> echo "export PATH=$PATH:/usr/local/go/bin" >> /home/vagrant/.bash_profile GUEST> echo `export GOPATH=/home/vagrant/workspace:$PATH` >> /home/vagrant/.bash_profile GUEST> export GOPATH=/home/vagrant/workspace GUEST> mkdir -p "$GOPATH/bin"
Mailhogをインストール
Mailhogとmhsendmailをインストールします。
GUEST> go get github.com/mailhog/MailHog GUEST> go get github.com/mailhog/mhsendmail GUEST> sudo cp /home/juampy/gocode/bin/MailHog /usr/local/bin/mailhog GUEST> sudo cp /home/juampy/gocode/bin/mhsendmail /usr/local/bin/mhsendmail
サービス登録ファイルを作成します。
GUEST> sudo vi /etc/init/mailhog.conf description "mailhog-service" start on started networking stop on runlevel [!2345] exec /usr/local/bin/mailhog -api-bind-addr 192.168.33.10:8025 -ui-bind-addr 192.168.33.10:8025 -smtp-bind-addr 127.0.0.1:1025
*「192.168.33.10」はホストからGUESTへ接続可能なIPです。127.0.0.1で設定するとHOSTから接続できないことを注意してください。
サーバーが起動時に実行するように設定します。
GUEST> sudo sysv-rc-conf mailhog on GUEST> sudo sysv-rc-conf -list | grep mailhog
起動します。
GUEST> sudo service mailhog start
確認
「http://192.168.33.10:8025」で接続してみます。

Vagrantの時刻のズレ
概要
Vagrantで開発環境を構成して起動時間が長くなるとPCのスリープなったりしたのが原因なのか時刻がずれたりしましたので、その対応方法を調べてみました。
ホスト川に時刻を同期させる方法
色々試しましたが、一番良い方法だと思います。Vagrantfileに以下の内容を追記し再起動します。
config.vm.provider :virtualbox do |vb| vb.customize ["setextradata", :id, "VBoxInternal/Devices/VMMDev/0/Config/GetHostTimeDisabled", 0] end
その他
Dockerの環境はどうなったか。。。