かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( 番外編 )( values パッケージの下にサブパッケージを作成しても ValuesHelper クラスから呼び出せるようにする )

概要

現在の実装では ValuesHelper クラスと同じパッケージ内にある Values列挙型しか ValuesHelper クラスから呼び出すことが出来ませんが、サブパッケージにある Values列挙型も呼び出せるようにします。

またなぜかテストの一部が失敗するようになっていたので、その原因も調査します。

参照したサイト・書籍

  1. package 配下のクラス一覧を取得する方法いろいろ
    http://etc9.hatenablog.com/entry/2015/03/31/001620

  2. How to check if an object implements an interface?
    http://stackoverflow.com/questions/10165887/how-to-check-if-an-object-implements-an-interface

目次

  1. テストの一部が失敗する原因を調査する
  2. values パッケージの下にサブパッケージを作成しても ValuesHelper クラスから呼び出せるようにする

手順

テストの一部が失敗する原因を調査する

Gradle projects View から clean タスクの実行→「Rebuild Project」メニューの実行をした後、Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択してテストを実行したところ、テストの一部が失敗することに気づきました。

f:id:ksby:20160310000733p:plain

原因は、いろいろ試していた時に user_info テーブルのデータを変更していたのですが、失敗していたテストメソッド@Rule @Autowired public TestDataResource testDataResource; をクラスに指定していなかったり、@NoUseTestDataResource アノテーションを付加していたりして、user_info テーブルにテスト用データがロードされていなかったためでした。

SecurityMockMvcResource の mvc.noauth 以外の mvc.authTanakaTaro 等は userDetailsService.loadUserByUsername(...); を呼び出しているので、SecurityMockMvcResource を使用する場合にはテスト用データのロードが必須であることに気付いていませんでした。。。

SecurityMockMvcResource を noauth 用とそれ以外用に分けるのが正しい対応だとは思うのですが、今回は失敗しているテストメソッドから @NoUseTestDataResource アノテーションを削除するか、テストクラスに @Rule @Autowired public TestDataResource testDataResource; が記述されていない場合には追加してテスト用データがロードされるようにします。

feature/112-issue を作成して、ソースを修正します。今回は修正したソースはここには書きません。

修正後、再度 Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストを実行してテストが全て成功することを確認します。

f:id:ksby:20160310010733p:plain

commit、GitHub へ Push、feature/112-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/112-issue ブランチを削除、をします。

values パッケージの下にサブパッケージを作成しても ValuesHelper クラスから呼び出せるようにする

package 配下のクラス一覧を取得する方法いろいろ の記事によると、Guava の ClassPath クラスには getTopLevelClasses メソッド以外に getTopLevelClassesRecursive というメソッドがあり、getTopLevelClassesRecursive を使えばサブパッケージのクラスも取得できるようになるとのこと。Spring に ClassPathScanningCandidateComponentProvider というクラスが用意されているらしいですが、今回はメソッドを getTopLevelClassesRecursive に変更する対応にします。

  1. feature/111-issue ブランチを作成します。

  2. src/main/java/ksbysample/webapp/lending/values の下の ValuesHelper.javaリンク先の内容 に変更します。

  3. src/main/java/ksbysample/webapp/lending/values の下のパッケージ構成、クラス配置を以下のように変更します。

    ■変更前
    f:id:ksby:20160312100729p:plain

    ■変更後
    f:id:ksby:20160312101028p:plain

  4. 動作確認します。Gradle projects View から clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行い、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20160312101644p:plain

  5. Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20160312102013p:plain

  6. commit、GitHub へ Push、feature/111-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/111-issue ブランチを削除、をします。

ソースコード

ValuesHelper.java

package ksbysample.webapp.lending.values;

import com.google.common.reflect.ClassPath;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;
import java.util.stream.Collectors;

@Component("vh")
public class ValuesHelper {

    private final Map<String, String> valuesObjList;

    private ValuesHelper() throws IOException {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        valuesObjList = ClassPath.from(loader).getTopLevelClassesRecursive(this.getClass().getPackage().getName())
                .stream()
                .filter(classInfo -> {
                    try {
                        Class<?> clazz = Class.forName(classInfo.getName());
                        return !clazz.equals(Values.class) && Values.class.isAssignableFrom(clazz);
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                })
                .collect(Collectors.toMap(ClassPath.ClassInfo::getSimpleName, ClassPath.ClassInfo::getName));
    }

    @SuppressWarnings("unchecked")
    public <T extends Enum<T> & Values> String getValue(String classSimpleName, String valueName)
            throws ClassNotFoundException {
        Class<T> enumType = (Class<T>) Class.forName(this.valuesObjList.get(classSimpleName));
        T val = Enum.valueOf(enumType, valueName);
        return val.getValue();
    }

    public <T extends Enum<T> & Values> String getValue(Class<T> enumType, String valueName) {
        T val = Enum.valueOf(enumType, valueName);
        return val.getValue();
    }

    @SuppressWarnings("unchecked")
    public <T extends Enum<T> & Values> String getText(String classSimpleName, String value)
            throws ClassNotFoundException {
        Class<T> enumType = (Class<T>) Class.forName(this.valuesObjList.get(classSimpleName));
        String result = "";
        for (T val : enumType.getEnumConstants()) {
            if (val.getValue().equals(value)) {
                result = val.getText();
                break;
            }
        }
        return result;
    }

    public <T extends Enum<T> & Values> String getText(Class<T> enumType, String value) {
        String result = "";
        for (T val : enumType.getEnumConstants()) {
            if (val.getValue().equals(value)) {
                result = val.getText();
                break;
            }
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    public <T extends Enum<T> & Values> T[] values(String classSimpleName)
            throws ClassNotFoundException {
        Class<T> enumType = (Class<T>) Class.forName(this.valuesObjList.get(classSimpleName));
        return enumType.getEnumConstants();
    }

}
  • コンストラクタ ValuesHelper() の以下の点を変更します。
    • getTopLevelClassesgetTopLevelClassesRecursive へ変更します。
    • filter メソッドで ValuesHelper クラスだけを除外するようにしていた処理を、Values インターフェースを implements していて、かつ Values インターフェース自体は除くクラスだけを抽出する処理に変更します。

履歴

2016/03/12
初版発行。