概要
古いバージョンのフレームワークを利用すると想定してないところで問題が発生したりしてパッチを作成する必要があったりします。本記事ではその中で「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に似た挙動だが、やや非標準。- 設定ミスや環境依存の不具合が起きやすい。