Spring Boot + Spring Integration でいろいろ試してみる ( その5 )( 監視しているディレクトリに置かれた Excel ファイルのデータを DB に登録する → 処理が終わったら Excel ファイルを削除/移動する2 )
概要
記事一覧はこちらです。
参照したサイト・書籍
目次
- user_info, user_role テーブルに登録する処理を実装する
- なぜ transactionManager Bean が自動生成されなかったのか?
- transactionManager Bean を生成する
- 再度 UserInfoServiceTest#loadUserInfoFromExcel テストメソッドを実行する
- FileProcessor#process から UserInfoService#loadUserInfoFromExcel を呼び出すよう実装する
- 動作確認
手順
user_info, user_role テーブルに登録する処理を実装する
src/main/java/ksbysample/eipapp/dirchecker/service/userinfo の下の UserInfoService.java を リンク先の内容 に変更します。
ksbysample-webapp-lending から以下のファイルをコピーします。
コピーしてきた src/main/resources の下の applicationContext.xml を リンク先の内容 に変更します。
DBテスト用のライブラリを利用したいので、ksbysample-webapp-lending から src/test/java/ksbysample/common/test/rule/db をフォルダ毎コピーします。
コピーしてきた src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.java を リンク先の内容 に変更します。
ksbysample/eipapp/dirchecker の下の Application.java を リンク先の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして build.gradle を反映します。
src/test/resources の下に testdata/base ディレクトリを作成します。
src/test/resources/testdata/base の下に user_info.csv, user_role.csv, lending_app.csv, lending_book.csv, table-ordering.txt を作成します。作成後、リンク先の内容 に変更します。
src/test/resources/ksbysample/eipapp/dirchecker/service/userinfo の下に testdata/001 と assertdata/001 ディレクトリを作成します。
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 のデータは不要なのですが、テスト用ライブラリの使い方を思い出すために用意しています。
src/test/resources/ksbysample/eipapp/dirchecker/service/userinfo/assertdata/001 の下に user_info.csv, user_role.csv, table-ordering.txt を作成します。作成後、リンク先の内容 に変更します。
UserInfoService#loadUserInfoFromExcel メソッドのテストを作成します。src/test/java/ksbysample/eipapp/dirchecker/service/userinfo の下の UserInfoServiceTest.java を リンク先の内容 に変更します。
UserInfoServiceTest#loadUserInfoFromExcel テストメソッドを実行します。が、
transactionManager
Bean の NoSuchBeanDefinitionException でエラーになりました。通常自動で生成されるはずなのですが。。。?
なぜ 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 インターフェースの実装クラスですね。
よって結論は、アプリケーション内で PlatformTransactionManager インターフェースの実装クラスである PseudoTransactionManager クラスの Bean を生成していたため、Spring Boot の AutoConfiguration で transactionManager Bean が自動生成されなかった、という仕組みでした。
transactionManager Bean を生成する
transactionManager Bean をアプリケーション内で生成するようにします。
- IntelliJ IDEA で Shift キーを2回押して検索ウィンドウを表示した後 “TransactionManager” で検索すると、DataSourceTransactionManager というクラスが見つかりました。通常自動生成される TransactionManager はこのクラスですので、このクラスで transactionManager Bean を生成します。
再度 UserInfoServiceTest#loadUserInfoFromExcel テストメソッドを実行する
UserInfoServiceTest#loadUserInfoFromExcel テストメソッドを実行すると今度は成功しました。
FileProcessor#process から UserInfoService#loadUserInfoFromExcel を呼び出すよう実装する
動作確認
確認前の user_info, user_role のデータは以下の状態です。
clean タスク実行 → Rebuild Project 実行をした後に bootRun タスクを実行して Tomcat を起動します。
src/test/resources/ksbysample/eipapp/dirchecker/service/userinfo の下の TestData01.xlsx を in ディレクトリにコピーすると、すぐにファイルがなくなりました。error ディレクトリには移動していません。
↓↓↓
user_info, user_role には以下のようにデータが登録されていました。
登録されたデータは削除し、Ctrl+F2 を押して Tomcat を停止します。
次に例外発生持に error ディレクトリに移動するか試します。UserInfoService#loadUserInfoFromExcel 内で以下のように RuntimeException が throw されるように変更します。
bootRun タスクを実行して Tomcat を起動します。
TestData01.xlsx を in ディレクトリにコピーすると、すぐに error ディレクトリにファイルが移動しました。
↓↓↓
user_info, user_role テーブルにもデータは登録されていませんでした。
無事正常に動作しているようです。
ただしこのまま Tomcat を起動している状態で error ディレクトリの TestData01.xlsx を削除してから再度 in ディレクトリに TestData01.xlsx をコピーしてもファイルは削除も移動もされないんですよね。。。
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
初版発行。