クリエイター:メタボ兔

ウェブやアプリの開発者で利用する色な技術やサーバーや開発環境の設定について共有する場

「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.jarcom.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に似た挙動だが、やや非標準。
  • 設定ミスや環境依存の不具合が起きやすい。

参考元

https://stackoverflow.com/questions/23813369/spring-java-error-namespace-element-annotation-config-on-jdk-1-5-and-high

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と比較内容をカスタマイズすれば「確認したい内容」のみ定義できるかと思いました調べたところ以下のような内容がありました。

stackoverflow.com

こんな感じで作ってみました。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というものがありました。テストメソッドが呼ばれる時の値を指定した変数に格納(?)してくれるので後で値を比較が可能になります。

www.programcreek.com

...
    // 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) をダウンロードする」からダウンロードします。

www.microsoft.com

f:id:FattyRabbit:20211221114428p:plain

VMWareの設定

  • HDD:80G
  • Memory:4G

設定によりと思いますが、CMOSでCDからBootされるようにしている必要があります。VMを起動して直ぐEscキーを押したら入りました。

インストール

問題発生

普通にインストール手順で設定してインストールOSの種類を選択した後に以下の「このPCは、このバージョンのWindowsをインストールするために最小システム要件を満たしません。詳細については、https://aka.ms/WindowsSysReqを参照してください。」とメッセージが出てインストールが出来ない状態になりました。

f:id:FattyRabbit:20211221115613p:plain

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

f:id:FattyRabbit:20211221115642p:plain

問題解決

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

f:id:FattyRabbit:20211221120028p:plain

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

f:id:FattyRabbit:20211221120052p:plain

両方の値もダブルクリックで「1」を設定しておきます。ここでのレジストリインストーラ用の一時的なもので、再起動などで消えるので間違えたりしても影響はありません。

再起動はせずに追加できたらすべて画面を閉じてインストールを進めていきます。

完了

インストールできました。

f:id:FattyRabbit:20211221120349p:plain

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」で接続してみます。

f:id:FattyRabbit:20210802233607p:plain

Vagrantの時刻のズレ

概要

Vagrantで開発環境を構成して起動時間が長くなるとPCのスリープなったりしたのが原因なのか時刻がずれたりしましたので、その対応方法を調べてみました。

ホスト川に時刻を同期させる方法

色々試しましたが、一番良い方法だと思います。Vagrantfileに以下の内容を追記し再起動します。

config.vm.provider :virtualbox do |vb|
  vb.customize ["setextradata", :id, "VBoxInternal/Devices/VMMDev/0/Config/GetHostTimeDisabled", 0]
end

その他

Dockerの環境はどうなったか。。。