かんがるーさんの日記

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

Spring Boot + Spring Integration でいろいろ試してみる ( その5 )( 監視しているディレクトリに置かれた Excel ファイルのデータを DB に登録する → 処理が終わったら Excel ファイルを削除/移動する2 )

概要

記事一覧はこちらです。

Spring Boot + Spring Integration でいろいろ試してみる ( その4 )( 監視しているディレクトリに置かれた Excel ファイルのデータを DB に登録する → 処理が終わったら Excel ファイルを削除/移動する ) の続きです。

参照したサイト・書籍

目次

  1. user_info, user_role テーブルに登録する処理を実装する
  2. なぜ transactionManager Bean が自動生成されなかったのか?
  3. transactionManager Bean を生成する
  4. 再度 UserInfoServiceTest#loadUserInfoFromExcel テストメソッドを実行する
  5. FileProcessor#process から UserInfoService#loadUserInfoFromExcel を呼び出すよう実装する
  6. 動作確認

手順

user_info, user_role テーブルに登録する処理を実装する

  1. src/main/java/ksbysample/eipapp/dirchecker/service/userinfo の下の UserInfoService.javaリンク先の内容 に変更します。

  2. ksbysample-webapp-lending から以下のファイルをコピーします。

    • src/main/resources/applicationContext-develop.xml
      → src/main/resources/applicationContext.xml
  3. コピーしてきた src/main/resources の下の applicationContext.xmlリンク先の内容 に変更します。

  4. DBテスト用のライブラリを利用したいので、ksbysample-webapp-lending から src/test/java/ksbysample/common/test/rule/db をフォルダ毎コピーします。

  5. コピーしてきた src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.javaリンク先の内容 に変更します。

  6. ksbysample/eipapp/dirchecker の下の Application.javaリンク先の内容 に変更します。

  7. DbUnit を利用したいので build.gradle を リンク先の内容 に変更します。

  8. Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして build.gradle を反映します。

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

  10. src/test/resources/testdata/base の下に user_info.csv, user_role.csv, lending_app.csv, lending_book.csv, table-ordering.txt を作成します。作成後、リンク先の内容 に変更します。

  11. src/test/resources/ksbysample/eipapp/dirchecker/service/userinfo の下に testdata/001 と assertdata/001 ディレクトリを作成します。

  12. src/test/resources/ksbysample/eipapp/dirchecker/service/userinfo/testdata/001 の下に user_info.csv, user_role.csv, table-ordering.txt を作成します。作成後、リンク先の内容 に変更します。

    ※実際には src/test/resources/testdata/base のデータでクリアされるので src/test/resources/ksbysample/eipapp/dirchecker/service/userinfo/testdata/001 のデータは不要なのですが、テスト用ライブラリの使い方を思い出すために用意しています。

  13. src/test/resources/ksbysample/eipapp/dirchecker/service/userinfo/assertdata/001 の下に user_info.csv, user_role.csv, table-ordering.txt を作成します。作成後、リンク先の内容 に変更します。

  14. UserInfoService#loadUserInfoFromExcel メソッドのテストを作成します。src/test/java/ksbysample/eipapp/dirchecker/service/userinfo の下の UserInfoServiceTest.javaリンク先の内容 に変更します。

  15. UserInfoServiceTest#loadUserInfoFromExcel テストメソッドを実行します。が、transactionManager Bean の NoSuchBeanDefinitionException でエラーになりました。通常自動で生成されるはずなのですが。。。?

    f:id:ksby:20160831071255p:plain

なぜ transactionManager Bean が自動生成されなかったのか?

Spring Boot の GitHub ( https://github.com/spring-projects/spring-boot ) の spring-boot-autoconfigure の中に自動生成しているソースがあるはずなので確認してみます。

spring-boot/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.java が TransactionManager の AutoConfiguration のソースのようですが、これを見ると @ConditionalOnSingleCandidate(PlatformTransactionManager.class) という記述がありました。PlatformTransactionManager インターフェースの実装クラスのインスタンスが既に生成されている場合には自動生成されないようです。

TransactionManager と言えば、今回の実装では src/main/java/ksbysample/eipapp/dirchecker/eip/config の下の TransactionConfig.java で pseudoTransactionManager Bean を生成していました。

@Configuration
public class TransactionConfig {

    @Bean
    public PseudoTransactionManager pseudoTransactionManager() {
        return new PseudoTransactionManager();
    }

}

PseudoTransactionManager クラスを確認すると PlatformTransactionManager インターフェースの実装クラスですね。

f:id:ksby:20160903084826p:plain

よって結論は、アプリケーション内で PlatformTransactionManager インターフェースの実装クラスである PseudoTransactionManager クラスの Bean を生成していたため、Spring Boot の AutoConfiguration で transactionManager Bean が自動生成されなかった、という仕組みでした。

transactionManager Bean を生成する

transactionManager Bean をアプリケーション内で生成するようにします。

  1. IntelliJ IDEA で Shift キーを2回押して検索ウィンドウを表示した後 “TransactionManager” で検索すると、DataSourceTransactionManager というクラスが見つかりました。通常自動生成される TransactionManager はこのクラスですので、このクラスで transactionManager Bean を生成します。

f:id:ksby:20160903090205p:plain

  1. src/main/java/ksbysample/eipapp/dirchecker/eip/config の下の TransactionConfig.javaリンク先の内容 に変更します。

再度 UserInfoServiceTest#loadUserInfoFromExcel テストメソッドを実行する

  1. UserInfoServiceTest#loadUserInfoFromExcel テストメソッドを実行すると今度は成功しました。

    f:id:ksby:20160906010819p:plain

FileProcessor#process から UserInfoService#loadUserInfoFromExcel を呼び出すよう実装する

  1. src/main/java/ksbysample/eipapp/dirchecker/eip/endpoint の下の FileProcessor.javaリンク先の内容 に変更します。

動作確認

確認前の user_info, user_role のデータは以下の状態です。

f:id:ksby:20160907221111p:plain

in ディレクトリ、error ディレクトリは空にします。

f:id:ksby:20160907221239p:plain

clean タスク実行 → Rebuild Project 実行をした後に bootRun タスクを実行して Tomcat を起動します。

f:id:ksby:20160907221555p:plain

src/test/resources/ksbysample/eipapp/dirchecker/service/userinfo の下の TestData01.xlsx を in ディレクトリにコピーすると、すぐにファイルがなくなりました。error ディレクトリには移動していません。

f:id:ksby:20160907222318p:plain

f:id:ksby:20160907221809p:plain ↓↓↓ f:id:ksby:20160907221927p:plain

user_info, user_role には以下のようにデータが登録されていました。

f:id:ksby:20160907222548p:plain

登録されたデータは削除し、Ctrl+F2 を押して Tomcat を停止します。

次に例外発生持に error ディレクトリに移動するか試します。UserInfoService#loadUserInfoFromExcel 内で以下のように RuntimeException が throw されるように変更します。

f:id:ksby:20160907223033p:plain

bootRun タスクを実行して Tomcat を起動します。

TestData01.xlsx を in ディレクトリにコピーすると、すぐに error ディレクトリにファイルが移動しました。

f:id:ksby:20160907223629p:plain ↓↓↓ f:id:ksby:20160907223728p:plain

user_info, user_role テーブルにもデータは登録されていませんでした。

f:id:ksby:20160907223859p:plain

無事正常に動作しているようです。

ただしこのまま Tomcat を起動している状態で error ディレクトリの TestData01.xlsx を削除してから再度 in ディレクトリに TestData01.xlsx をコピーしてもファイルは削除も移動もされないんですよね。。。

f:id:ksby:20160907224458p:plain

AcceptOnceFileListFilter も入れていないのに同一ファイルが処理されなくなる仕組みが分からないので、次回調査してみます。

Ctrl+F2 を押して Tomcat を停止し、UserInfoService#loadUserInfoFromExcel を元に戻します。

ソースコード

UserInfoService.java

package ksbysample.eipapp.dirchecker.service.userinfo;

import ksbysample.eipapp.dirchecker.dao.UserInfoDao;
import ksbysample.eipapp.dirchecker.dao.UserRoleDao;
import ksbysample.eipapp.dirchecker.entity.UserInfo;
import ksbysample.eipapp.dirchecker.entity.UserRole;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.jxls.reader.ReaderBuilder;
import org.jxls.reader.XLSReadStatus;
import org.jxls.reader.XLSReader;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.xml.sax.SAXException;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class UserInfoService {

    private static final String CLASSPATH_USERINFO_EXCEL_CFG_XML
            = "ksbysample/eipapp/dirchecker/service/userinfo/userinfo-excel-cfg.xml";

    @Autowired
    private UserInfoDao userInfoDao;

    @Autowired
    private UserRoleDao userRoleDao;

    public void loadUserInfoFromExcel(File excelFile)
            throws InvalidFormatException, SAXException, IOException {
        // Excel ファイルからデータを読み込む
        List<UserInfoExcelRow> userInfoExcelRowList = loadFromExcelToList(excelFile);

        // user_info, user_role テーブルに登録する
        userInfoExcelRowList.forEach(userInfoExcelRow -> {
            UserInfo userInfo = makeUserInfo(userInfoExcelRow);
            userInfoDao.insert(userInfo);

            userInfoExcelRow.getRoleListFromRoles().forEach(role -> {
                UserRole userRole = makeUserRole(userInfo.getUserId(), role);
                userRoleDao.insert(userRole);
            });
        });
    }

    public List<UserInfoExcelRow> loadFromExcelToList(File excelFile)
            throws IOException, SAXException, InvalidFormatException {
        Resource rsExcelCfgXml = new ClassPathResource(CLASSPATH_USERINFO_EXCEL_CFG_XML);
        Resource rsUserInfoExcel = new FileSystemResource(excelFile.getAbsolutePath());

        XLSReader reader = ReaderBuilder.buildFromXML(rsExcelCfgXml.getFile());

        List<UserInfoExcelRow> userInfoExcelRowList = new ArrayList<>();
        Map<String, Object> beans = new HashMap<>();
        beans.put("userInfoExcelRow", new UserInfoExcelRow());
        beans.put("userInfoExcelRowList", userInfoExcelRowList);

        try (InputStream isUserInfoExcel = new BufferedInputStream(rsUserInfoExcel.getInputStream())) {
            XLSReadStatus status = reader.read(isUserInfoExcel, beans);
        }

        return userInfoExcelRowList;
    }

    private UserInfo makeUserInfo(UserInfoExcelRow userInfoExcelRow) {
        UserInfo userInfo = new UserInfo();
        BeanUtils.copyProperties(userInfoExcelRow, userInfo);
        userInfo.setPassword(new BCryptPasswordEncoder().encode(userInfoExcelRow.getPassword()));
        userInfo.setEnabled((short) 1);
        userInfo.setCntBadcredentials((short) 0);
        userInfo.setExpiredAccount(LocalDateTime.now().plusMonths(3));
        userInfo.setExpiredPassword(LocalDateTime.now().plusMonths(1));
        return userInfo;
    }

    private UserRole makeUserRole(Long userId, String role) {
        UserRole userRole = new UserRole();
        userRole.setUserId(userId);
        userRole.setRole(role);
        return userRole;
    }

}
  • @Autowired private UserInfoDao userInfoDao; を追加します。
  • @Autowired private UserRoleDao userRoleDao; を追加します。
  • makeUserInfo メソッド、makeUserRole メソッドを追加します。
  • loadUserInfoFromExcel メソッドを追加します。

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" rollback-for="Exception"/>
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="pointcutService" expression="execution(* ksbysample.eipapp.dirchecker..*Service.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcutService"/>
    </aop:config>

</beans>
  • <aop:pointcut id="pointcutService" expression="..."/> の expression の記述を execution(* ksbysample.webapp.lending..*Service.*(..))execution(* ksbysample.eipapp.dirchecker..*Service.*(..)) へ変更します。

TestDataResource.java

@Component
public class TestDataResource extends TestWatcher {

    private static final String BASETESTDATA_ROOT_DIR = "src/test/resources/";
    private static final String TESTDATA_ROOT_DIR = "src/test/resources/ksbysample/webapp/lending/";
    private static final String BASETESTDATA_DIR = BASETESTDATA_ROOT_DIR + "testdata/base";
    private static final String BACKUP_FILE_NAME = "ksbylending_backup";

    ..........
  • TESTDATA_ROOT_DIR の文字列を src/test/resources/ksbysample/webapp/lending/src/test/resources/ksbysample/eipapp/dirchecker/ へ変更します。

Application.java

package ksbysample.eipapp.dirchecker;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.context.annotation.ImportResource;

@ImportResource("classpath:applicationContext.xml")
@SpringBootApplication(exclude = {JpaRepositoriesAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
@ComponentScan("ksbysample")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
  • @ImportResource("classpath:applicationContext.xml") を追加します。
  • @ComponentScan("ksbysample") を追加します。

build.gradle

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:9.4.1209"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    compile('org.springframework.boot:spring-boot-starter-integration')
    compile('org.springframework.integration:spring-integration-file')
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.spockframework:spock-core")
    testCompile("org.spockframework:spock-spring")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    runtime("${jdbcDriver}")
    compile("org.seasar.doma:doma:2.12.1")
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile("org.projectlombok:lombok:1.16.10")
    compile("org.apache.commons:commons-lang3:3.4")
    compile("org.jxls:jxls-reader:2.0.2")
    testCompile("org.assertj:assertj-core:3.5.2")
    testCompile("org.dbunit:dbunit:2.5.3")

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

testdata/base/user_info.csv, user_role.csv, lending_app.csv, lending_book.csv, table-ordering.txt

■user_info.csv

user_id,username,password,mail_address,enabled,cnt_badcredentials,expired_account,expired_password

■user_role.csv

role_id,user_id,role

■lending_app.csv

lending_app_id,status,lending_user_id,approval_user_id,version

■lending_book.csv

lending_book_id,lending_app_id,isbn,book_name,lending_state,lending_app_flg,lending_app_reason,approval_result,approval_reason,version

■table-ordering.txt

user_info
user_role
lending_app
lending_book

testdata/001/user_info.csv, user_role.csv, table-ordering.txt

■user_info.csv

user_id,username,password,mail_address,enabled,cnt_badcredentials,expired_account,expired_password

■user_role.csv

role_id,user_id,role

■table-ordering.txt

user_info
user_role
  • user_info, user_role どちらにもデータは記述しません。テーブルをクリアするのが目的です。

assertdata/001/user_info.csv, user_role.csv, table-ordering.txt

■user_info.csv

user_id,username,password,mail_address,enabled,cnt_badcredentials,expired_account,expired_password
,yota takahashi,,yota.takahashi@test.co.jp,1,0,,
,aoi inoue,,aoi.inoue@sample.com,1,0,,
  • 検証に使用する username, mail_address, enabled, cnt_badcredentials のみ記述します。

■user_role.csv

role_id,user_id,role
,,ROLE_USER
,,ROLE_ADMIN
,,ROLE_USER
  • 検証に使用する role のみ記述します。

■table-ordering.txt

user_info
user_role

UserInfoServiceTest.java

package ksbysample.eipapp.dirchecker.service.userinfo;

import ksbysample.common.test.rule.db.*;
import ksbysample.eipapp.dirchecker.Application;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
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.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.sql.DataSource;
import java.io.File;
import java.util.List;

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

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

    private static final String CLASSPATH_EXCEL_FOR_TEST
            = "ksbysample/eipapp/dirchecker/service/userinfo/Book1.xlsx";

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserInfoService userInfoService;

    @Test
    @NoUseTestDataResource
    public void loadFromExcelToList() throws Exception {
        Resource resource = new ClassPathResource(CLASSPATH_EXCEL_FOR_TEST);
        List<UserInfoExcelRow> userInfoExcelRowList = userInfoService.loadFromExcelToList(resource.getFile());
        assertThat(userInfoExcelRowList).hasSize(2);
        assertThat(userInfoExcelRowList).extracting("username", "password", "mailAddress", "roles")
                .containsOnly(tuple("yota takahashi", "12345678", "yota.takahashi@test.co.jp", "ROLE_USER")
                        , tuple("aoi inoue", "abcdefgh", "aoi.inoue@sample.com", "ROLE_ADMIN,ROLE_USER"));
    }

    @Test
    @TestData("service/userinfo/testdata/001")
    public void loadUserInfoFromExcel() throws Exception {
        Resource resource = new ClassPathResource(CLASSPATH_EXCEL_FOR_TEST);
        userInfoService.loadUserInfoFromExcel(resource.getFile());

        IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/eipapp/dirchecker/service/userinfo/assertdata/001"));
        TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
        tableDataAssert.assertEqualsByQuery(
                "select username, mail_address, enabled, cnt_badcredentials from user_info order by user_id"
                , "user_info"
                , new String[]{"username", "mail_address", "enabled", "cnt_badcredentials"}
                , AssertOptions.INCLUDE_COLUMN);
        tableDataAssert.assertEqualsByQuery("select role from user_role order by user_id, role_id"
                , "user_role"
                , new String[]{"role"}
                , AssertOptions.INCLUDE_COLUMN);
    }

}
  • @Rule @Autowired public TestDataResource testDataResource; を追加します。
  • @Autowired private DataSource dataSource; を追加します。
  • loadFromExcelToList テストメソッドに @NoUseTestDataResource アノテーションを付加します。
  • loadUserInfoFromExcel テストメソッドを追加します。

TransactionConfig.java

package ksbysample.eipapp.dirchecker.eip.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.transaction.PseudoTransactionManager;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class TransactionConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public PseudoTransactionManager pseudoTransactionManager() {
        return new PseudoTransactionManager();
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(this.dataSource);
    }

}
  • @Autowired private DataSource dataSource; を追加します。
  • transactionManager メソッドを追加します。

FileProcessor.java

package ksbysample.eipapp.dirchecker.eip.endpoint;

import ksbysample.eipapp.dirchecker.service.userinfo.UserInfoService;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.IOException;

@MessageEndpoint
public class FileProcessor {

    @Autowired
    private UserInfoService userInfoService;

    @ServiceActivator(inputChannel = "excelToDbChannel")
    public void process(Message<File> message)
            throws InvalidFormatException, SAXException, IOException {
        File file = message.getPayload();
        userInfoService.loadUserInfoFromExcel(file);
    }

}
  • @Autowired private UserInfoService userInfoService; を追加します。
  • process メソッド内で userInfoService.loadUserInfoFromExcel(file); を呼び出すように変更します。

履歴

2016/09/07
初版発行。