かんがるーさんの日記

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

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その15 )( ログイン画面の作成6 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その14 )( Spring Session を使用する3 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。

    • ログイン画面・ログアウト機能のテストクラスの作成 ( 2回に分けます )
  • JavaのテストにはAssertJがオススメ の記事を読んで AssertJ に興味が湧いたので、今回からテスト結果の検証には JUnit の AssertThat ではなく AssertJ を使用してみます。

参照したサイト・書籍

  1. JavaのテストにはAssertJがオススメ
    http://qiita.com/ikemo/items/165f01740995245f9009

  2. AssertJ
    http://joel-costigliola.github.io/assertj/

  3. assertj examples tests
    https://github.com/joel-costigliola/assertj-examples/tree/master/assertions-examples/src/test/java/org/assertj/examples

目次

  1. 1.0.x-make-test-login ブランチの作成
  2. AssertJ を使用できるようにする
  3. テストデータ配置用ディレクトリの作成
  4. テストデータの作成
  5. TestDataResource クラスの準備
  6. UserInfoServiceTest クラスの作成
  7. LendingUserDetailsTest クラスの作成
  8. LendingUserDetailsServiceTest クラスの作成
  9. メモ書き
    1. AssertJ を使ってみて

手順

1.0.x-make-test-login ブランチの作成

  1. IntelliJ IDEA で 1.0.x-make-test-login ブランチを作成します。

AssertJ を使用できるようにする

  1. build.gradle を リンク先の内容 に変更します。

  2. Gradle projects View の左上にある「Refresh all Gradle projects」アイコンをクリックして、変更した build.gradle の内容を反映します。

テストデータ配置用ディレクトリの作成

  1. src/test/resources の下に testdata/base ディレクトリを作成します。

テストデータの作成

  1. user_info, user_role テーブルに以下の画像のデータを登録します。

    f:id:ksby:20150808160556p:plain

    • テストデータは以下の内容です。

      user_id テストデータの内容
      1 ログイン可能、role が全てある
      2 ログイン可能、role は ROLE_USER のみ
      3 ログイン不可能、enabled = 0
      4 ログイン不可能、アカウントの有効期限(expired_account)切れ
      5 ログイン不可能、パスワードの有効期限(expired_password)切れ
      6 ログイン不可能、role が何も登録されていない
      6 ログイン不可能、全部NG ( enabled = 0、アカウント・パスワード有効期限切れ、IDロック中、roleが何も登録されていない )
    • user_info.password には http://localhost:8080/encode?password=[名前(例:taro)] で出力された文字列をセットしています。

  2. CSVファイル出力時にカラム名も出力されるようにします。Database View の user_info, user_role テーブルでコンテキストメニューを表示し、「Save To File」->「Configure Extractors...」を選択します。

    f:id:ksby:20150807005644p:plain

    「Data Extractors」ダイアログが表示されますので、画面左側のリストの「Comma-separated Values (CSV)」が選択されていることを確認した後、画面右側の「Include column names」をチェックして「OK」ボタンをクリックします。

    f:id:ksby:20150807005907p:plain

  3. CSVファイルを出力します。Database View の user_info, user_role テーブルでコンテキストメニューを表示し、「Save To File」->「Comma-separated Values (CSV)」を選択します。

    f:id:ksby:20150805225839p:plain

    「Save Data To File」ダイアログが表示されますので、src/test/resources/testdata/base の下に [テーブル名].csv のファイル名で保存します。

    f:id:ksby:20150805230141p:plain

  4. src/test/resources/testdata/base の下に table-ordering.txt を作成します。作成後、リンク先の内容 に変更します。

  5. この時点で src/test/resources は以下の画像の状態になります。

    f:id:ksby:20150805232522p:plain

TestDataResource クラスの準備

  1. src/test/java/ksbysample/common/test の下の TestDataResource.javaリンク先の内容 に変更します。

UserInfoServiceTest クラスの作成

  1. src/main/java/ksbysample/webapp/lending/service の下の UserInfoService.java を開いて「Create Test」ダイアログを表示し、テストクラスを作成します。

    f:id:ksby:20150808062119p:plain

    src/test/java/ksbysample/webapp/lending/service の下に UserInfoServiceTest.java が作成されますので、リンク先のその1の内容 に変更します。

  2. テストを実行してみます。UserInfoServiceTest のクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'UserInfoServiceTest' with Coverage」を選択します。

    Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [ksbysample.common.test.TestDataResource] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.junit.Rule(), @org.springframework.beans.factory.annotation.Autowired(required=true)} というエラーメッセージが表示されて、テストは失敗しました。原因を調査します。

    f:id:ksby:20150808063055p:plain

  3. 原因は、今回からテストで利用するクラスを src/test/ksbysample/common/test の下に移動していたのですが、Spring Boot でデフォルトで ComponentScan の対象になるのは Application.java のあるパッケージ ( ksbysample.webapp.lending ) から下だけなので、ksbysample.common.test パッケージの下のクラスが ComponentScan の対象外になっていたためでした。

  4. ksbysample パッケージの下全てが ComponentScan の対象になるようにします。src/main/ksbysample/webapp/lending の下の Application.javaリンク先の内容 に変更します。

    追加した @ComponentScan("ksbysample") の左側に表示されるアイコンをクリックして ComponentScan されているクラスを表示すると、TestDataResource.java が含まれていることが確認できます。

    f:id:ksby:20150808064104p:plain

  5. 再度テストを実行します。今度は testIncCntBadcredentials メソッドだけテストが失敗しました。テスト結果を比較する値が Short ではなく Integer と判断されているためのようです。

    f:id:ksby:20150808064925p:plain

  6. テスト結果を比較する値が Short と判断されるようにします。src/test/java/ksbysample/webapp/lending/service の下の UserInfoServiceTest.javaリンク先のその2の内容 に変更します。

  7. 再度テストを実行し、今度は成功することが確認できました。

    f:id:ksby:20150808065715p:plain

  8. 一旦ここまでで Git - Add した後、commit しておきます。

    • Git - Add しているのはテストデータの CSV ファイルを追加するためです。

LendingUserDetailsTest クラスの作成

  1. src/main/java/ksbysample/webapp/lending/security の下の LendingUserDetails.java を開いて「Create Test」ダイアログを表示し、テストクラスを作成します。

    f:id:ksby:20150808074907p:plain

    • 画面下部の Member 一覧は何もチェックしません。自分でテストメソッドを書きます。

    src/test/java/ksbysample/webapp/lending/security の下に LendingUserDetailsTest.java が作成されますので、リンク先の内容 に変更します。

  2. テストを実行してみます。LendingUserDetailsTest のクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'UserInfoServiceTe..' with Coverage」を選択します。

    テストが全て成功することが確認できます。

    f:id:ksby:20150808180725p:plain

  3. commit します。

LendingUserDetailsServiceTest クラスの作成

  1. src/main/java/ksbysample/webapp/lending/security の下の LendingUserDetailsService.java を開いて「Create Test」ダイアログを表示し、テストクラスを作成します。

    f:id:ksby:20150808182935p:plain

    • 画面下部の Member 一覧は何もチェックしません。自分でテストメソッドを書きます。

    src/test/java/ksbysample/webapp/lending/security の下に LendingUserDetailsServiceTest.java が作成されますので、リンク先の内容 に変更します。

  2. テストを実行してみます。LendingUserDetailsServiceTest のクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'LendingUserDetailsSe...' with Coverage」を選択します。

    テストが全て成功することが確認できます。

    f:id:ksby:20150808194518p:plain

  3. commit します。

メモ書き

AssertJ を使ってみて

  • JUnit の assertThat の場合には使用できる Matcher を本や Web で調べましたが、AssertJ であれば補完機能でメソッド一覧が表示されるので調べる手間が減りそうです。

    f:id:ksby:20150808073633p:plain

  • 用意されているメソッドが分かりやすく、かつ使いやすい印象を受けました。公式サイトやサンプルも充実していると思います。Java 8 対応もされているのがいいですね。

  • ちょっと使ってみただけですが、個人的には JUnit の assertThat を使うより全然便利です。JavaのテストにはAssertJがオススメ の記事だと「びっくりするくらいマイナー」らしいですが、本当に使われていないのでしょうか?

ソースコード

build.gradle

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:9.4-1201-jdbc41"

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されるもの
    // Appendix E. Dependency versions ( http://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity3")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-velocity")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-redis")
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.springframework.security:spring-security-test:4.0.1.RELEASE")
    testCompile("org.yaml:snakeyaml")

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの
    compile("${jdbcDriver}")
    compile("org.seasar.doma:doma:2.3.1")
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile("org.apache.commons:commons-lang3:3.4")
    compile("org.projectlombok:lombok:1.16.4")
    compile("com.google.guava:guava:18.0")
    compile("org.springframework.session:spring-session:1.0.1.RELEASE")
    testCompile("org.dbunit:dbunit:2.5.1")
    testCompile("com.icegreen:greenmail:1.4.1")
    testCompile("org.assertj:assertj-core:3.1.0")

    // for Doma-Gen
    domaGenRuntime("org.seasar.doma:doma-gen:2.3.1")
    domaGenRuntime("${jdbcDriver}")
}
  • testCompile("org.assertj:assertj-core:3.1.0") を追加します。

table-ordering.txt

user_info
user_role

TestDataResource.java

package ksbysample.common.test;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.rules.ExternalResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;

@Component
public class TestDataResource extends ExternalResource {

    private final String TESTDATA_DIR = "src/test/resources/testdata/base";
    private final String BACKUP_FILE_NAME = "ksbylending_backup";
    private final List<String> BACKUP_TABLES = Arrays.asList(
            "user_info"
            , "user_role"
    );

    @Autowired
    private DataSource dataSource;

    private File backupFile;
    
    @Override
    protected void before() throws Exception {
        IDatabaseConnection conn = null;
        try {
            conn = new DatabaseConnection(dataSource.getConnection());

            // バックアップを取得する
            QueryDataSet partialDataSet = new QueryDataSet(conn);
            for (String backupTable : BACKUP_TABLES) {
                partialDataSet.addTable(backupTable);
            }
            ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet);
            replacementDatasetBackup.addReplacementObject("", "[null]");
            backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml");
            try (FileOutputStream fos = new FileOutputStream(backupFile)) {
                FlatXmlDataSet.write(replacementDatasetBackup, fos);
            }

            // テストデータに入れ替える
            IDataSet dataSet = new CsvDataSet(new File(TESTDATA_DIR));
            ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet);
            replacementDataset.addReplacementObject("[null]", null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDataset);
        } finally {
            if (conn != null) conn.close();
        }
    }

    @Override
    protected void after() {
        try {
            IDatabaseConnection conn = null;
            try {
                conn = new DatabaseConnection(dataSource.getConnection());

                // バックアップからリストアする
                if (backupFile != null) {
                    IDataSet dataSet = new FlatXmlDataSetBuilder().build(backupFile);
                    ReplacementDataSet replacementDatasetRestore = new ReplacementDataSet(dataSet);
                    replacementDatasetRestore.addReplacementObject("[null]", null);
                    DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDatasetRestore);
                }
            } finally {
                if (backupFile != null) {
                    Files.delete(backupFile.toPath());
                    backupFile = null;
                }
                try {
                    if (conn != null) conn.close();
                } catch (Exception ignored) {
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
  • private final String TESTDATA_DIR = "src/test/resources/testdata/base"; を追加し、before メソッド内ではテストデータの位置をこの定数で指定するように変更します。
  • private final List<String> BACKUP_TABLES にバックアップするテーブルとして user_info, user_role を列挙します。

UserInfoServiceTest.java

■その1

package ksbysample.webapp.lending.service;

import ksbysample.common.test.TestDataResource;
import ksbysample.webapp.lending.Application;
import ksbysample.webapp.lending.dao.UserInfoDao;
import ksbysample.webapp.lending.entity.UserInfo;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class UserInfoServiceTest {

    private final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com";

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Autowired
    private UserInfoDao userInfoDao;

    @Autowired
    private UserInfoService userInfoService;

    @Test
    public void testIncCntBadcredentials() throws Exception {
        UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO);
        assertThat(userInfo.getCntBadcredentials()).isZero();

        userInfoService.incCntBadcredentials(MAILADDR_TANAKA_TARO);

        UserInfo userInfo2 = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO);
        assertThat(userInfo2.getCntBadcredentials()).isEqualTo(userInfo.getCntBadcredentials() + 1);
    }

    @Test
    public void testInitCntBadcredentials() throws Exception {
        userInfoService.incCntBadcredentials(MAILADDR_TANAKA_TARO);
        UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO);
        assertThat(userInfo.getCntBadcredentials()).isNotZero();

        userInfoService.initCntBadcredentials(MAILADDR_TANAKA_TARO);
        userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO);
        assertThat(userInfo.getCntBadcredentials()).isZero();
    }

}

■その2

    @Test
    public void testIncCntBadcredentials() throws Exception {
        UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO);
        assertThat(userInfo.getCntBadcredentials()).isZero();

        userInfoService.incCntBadcredentials(MAILADDR_TANAKA_TARO);

        UserInfo userInfo2 = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO);
        assertThat(userInfo2.getCntBadcredentials()).isEqualTo((short)(userInfo.getCntBadcredentials() + 1));
    }
  • .isEqualTo(userInfo.getCntBadcredentials() + 1);.isEqualTo((short)(userInfo.getCntBadcredentials() + 1)); へ変更します。

Application.java

package ksbysample.webapp.lending;

import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

import java.io.Serializable;
import java.text.MessageFormat;

@ImportResource("classpath:applicationContext-${spring.profiles.active}.xml")
@SpringBootApplication
@ComponentScan("ksbysample")
@EnableRedisHttpSession
public class Application {

    public static void main(String[] args) {
        String springProfilesActive = System.getProperty("spring.profiles.active");
        if (!StringUtils.equals(springProfilesActive, "product")
                && !StringUtils.equals(springProfilesActive, "develop")
                && !StringUtils.equals(springProfilesActive, "unittest")) {
            throw new UnsupportedOperationException(MessageFormat.format("JVMの起動時引数 -Dspring.profiles.active で develop か unittest か product を指定して下さい ( -Dspring.profiles.active={0} )。", springProfilesActive));
        }

        SpringApplication.run(Application.class, args);
    }

}
  • @ComponentScan("ksbysample") を追加します。

LendingUserDetailsTest.java

package ksbysample.webapp.lending.security;

import ksbysample.common.test.TestDataResource;
import ksbysample.webapp.lending.Application;
import ksbysample.webapp.lending.dao.UserInfoDao;
import ksbysample.webapp.lending.dao.UserRoleDao;
import ksbysample.webapp.lending.entity.UserInfo;
import ksbysample.webapp.lending.entity.UserRole;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class LendingUserDetailsTest {

    private final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com";
    private final String MAILADDR_KATO_HIROSHI = "kato.hiroshi@sample.com";

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Autowired
    private UserInfoDao userInfoDao;
    
    @Autowired
    private UserRoleDao userRoleDao;
    
    @Test
    public void 利用可能でロックされておらず有効期限切れもないユーザでのテスト() {
        UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_TANAKA_TARO);
        List<UserRole> userRoleList = userRoleDao.selectByUserId(userInfo.getUserId());
        Set<SimpleGrantedAuthority> authorities = new HashSet<>();
        authorities.addAll(
                userRoleList.stream()
                        .map(userRole -> new SimpleGrantedAuthority(userRole.getRole()))
                        .collect(Collectors.toList()));

        LendingUserDetails lendingUserDetails = new LendingUserDetails(userInfo, authorities);

        assertThat(lendingUserDetails.getUsername()).isEqualTo(userInfo.getMailAddress());
        assertThat(lendingUserDetails.getPassword()).isEqualTo(userInfo.getPassword());
        assertThat(lendingUserDetails.getName()).isEqualTo(userInfo.getUsername());
        assertThat(lendingUserDetails.getAuthorities()).extracting("authority")
                .containsOnly("ROLE_USER", "ROLE_ADMIN", "ROLE_APPROVER");
        assertThat(lendingUserDetails.isAccountNonExpired()).isTrue();
        assertThat(lendingUserDetails.isAccountNonLocked()).isTrue();
        assertThat(lendingUserDetails.isCredentialsNonExpired()).isTrue();
        assertThat(lendingUserDetails.isEnabled()).isTrue();
    }

    @Test
    public void 利用不可能でロック中で全て有効期限切れのユーザでのテスト() {
        UserInfo userInfo = userInfoDao.selectByMailAddress(MAILADDR_KATO_HIROSHI);
        LendingUserDetails lendingUserDetails = new LendingUserDetails(userInfo, null);

        assertThat(lendingUserDetails.getUsername()).isEqualTo(userInfo.getMailAddress());
        assertThat(lendingUserDetails.getPassword()).isEqualTo(userInfo.getPassword());
        assertThat(lendingUserDetails.getName()).isEqualTo(userInfo.getUsername());
        assertThat(lendingUserDetails.getAuthorities()).isNull();
        assertThat(lendingUserDetails.isAccountNonExpired()).isFalse();
        assertThat(lendingUserDetails.isAccountNonLocked()).isFalse();
        assertThat(lendingUserDetails.isCredentialsNonExpired()).isFalse();
        assertThat(lendingUserDetails.isEnabled()).isFalse();
    }
    
}

LendingUserDetailsServiceTest.java

package ksbysample.webapp.lending.security;

import ksbysample.common.test.TestDataResource;
import ksbysample.webapp.lending.Application;
import org.assertj.core.api.ThrowableAssert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.StrictAssertions.assertThatThrownBy;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class LendingUserDetailsServiceTest {

    private final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com";
    private final String MAILADDR_TEST_TARO = "test.taro@sample.com";

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private MessageSource messageSource;

    @Test
    public void 存在するユーザならばUserDetailsが返る() {
        UserDetails userDetails = userDetailsService.loadUserByUsername(MAILADDR_TANAKA_TARO);
        assertThat(userDetails).isNotNull();
        assertThat(userDetails.getUsername()).isEqualTo(MAILADDR_TANAKA_TARO);
    }

    @Test
    public void 存在しないユーザならばUsernameNotFoundExceptionが発生する() {
        assertThatThrownBy(new ThrowableAssert.ThrowingCallable() {
            @Override
            public void call() throws Throwable {
                UserDetails userDetails = userDetailsService.loadUserByUsername(MAILADDR_TEST_TARO);
            }
        }).isInstanceOf(UsernameNotFoundException.class)
                .hasMessage(messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound"
                        , null, LocaleContextHolder.getLocale()));
    }

}

履歴

2015/08/08
初版発行。