Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その24 )( 貸出希望書籍 CSV ファイルアップロード画面の作成3 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その23 )( 貸出希望書籍 CSV ファイルアップロード画面の作成2 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 貸出希望書籍 CSV ファイルアップロード画面の作成
- 登録機能を作成します。
- 貸出希望書籍 CSV ファイルアップロード画面の作成
参照したサイト・書籍
- Spring AMQP x RabbitMQ
http://www.slideshare.net/keisuke69/spring-amqp-rabbitmq
目次
手順
登録機能の作成
「登録」ボタンがクリックされたら RabbitMQ に貸出状況取得タスクへの貸出状況取得依頼のメッセージを送信した後、完了画面を表示します。
fileupload.html の修正
- src/main/resources/templates/booklist の下の fileupload.html を リンク先の内容 に変更します。
RabbitMQ に貸出状況取得タスクへの貸出状況取得依頼のメッセージを送信できるようにする
以下の仕様で送信できるようにします。
- Queue の名前は InquiringStatusOfBookQueue にします。
- Queue に入れるメッセージのフォーマットは Java 以外でも使用できることを考慮して JSON にします。Spring AMQP の MessageConverter は JsonMessageConverter ( 古い org.codehaus.jackson.map.ObjectMapper が使用されています ) ではなく Jackson2JsonMessageConverter ( 新しい com.fasterxml.jackson.databind.ObjectMapper が使用されています ) を使用します。
build.gradle を リンク先の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
src/main/resources の下の application-develop.properties, application-unittest.properties, application-product.properties を リンク先の内容 に変更します。
今回使用するキューを定義します。src/main/java/ksbysample/webapp/lending/config の下の Constant.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/config の下の ApplicationConfig.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service の下に queue パッケージを作成します。
src/main/java/ksbysample/webapp/lending/service/queue の下に InquiringStatusOfBookQueueMessage.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service/queue の下に InquiringStatusOfBookQueueService.java を作成します。作成後、リンク先の内容 に変更します。
BooklistService クラスの修正
BooklistController クラスの修正
src/main/java/ksbysample/webapp/lending/web/booklist の下の RegisterBooklistForm.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/booklist の下の BooklistController.java を リンク先の内容 に変更します。
complete.html の修正
- src/main/resources/templates/booklist の下の complete.html を リンク先の内容 に変更します。
動作確認
動作確認します。
最初に RabbitMQ にキューが作成されていないことを確認します。RabbitMQ のサービスが起動していない場合には起動します。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
http://localhost:15672/ へアクセスし、RabbitMQ の管理機能のログイン画面が表示されたら guest/guest でログインします。
トップ画面が表示されて、Queue がまだ作成されていないことが確認できます。
http://localhost:8080/booklist からテスト.csv をアップロードし、確認画面が表示された後「登録」ボタンをクリックします。
貸出申請ID が表示された完了画面が表示されます。
RabbitMQ の管理画面を更新すると Queue が作成されてメッセージが蓄積されていることが確認できます。
「Queues: 1」のボタンをクリックすると Queue 一覧が表示されて InquiringStatusOfBookQueue が作成されていることが確認できます。
キュー名の「InquiringStatusOfBookQueue」リンクをクリックして Queue の画面に遷移した後、「Get messages」のところにある「Get Message(s)」ボタンを押して蓄積されているメッセージの内容を確認します。完了画面に表示された貸出申請ID のメッセージが蓄積されていることが確認できます。
「Requeue」の選択を No に変更してから「Get Message(s)」ボタンを押してメッセージを削除します。
Ctrl+F2 を押して Tomcat を停止します。
一旦 commit します。
次回は。。。
テストを作成します。
ソースコード
fileupload.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>貸出希望書籍 CSV ファイルアップロード</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <link th:replace="common/head-cssjs"/> <style type="text/css"> <!-- .box-body.no-padding { padding-bottom: 10px !important; } --> </style> </head> <!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. --> <body class="skin-blue layout-top-nav"> <div class="wrapper"> <!-- Main Header --> <div th:replace="common/mainparts :: main-header"></div> <!-- Full Width Column --> <div class="content-wrapper"> <div class="container"> <!-- Content Header (Page header) --> <section class="content-header"> <h1>貸出希望書籍 CSV ファイルアップロード</h1> </section> <!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12"> <div class="box"> <div class="box-body no-padding"> <form id="registerBooklistForm" method="post" action="/booklist/register" th:action="@{/booklist/register}" th:object="${registerBooklistForm}"> <table class="table table-hover"> <colgroup> <col width="5%"/> <col width="35%"/> <col width="60%"/> </colgroup> <thead class="bg-purple"> <tr> <th>No.</th> <th>ISBN</th> <th>書名</th> </tr> </thead> <tbody class="jp-gothic"> <tr th:each="row, iterStat : *{registerBooklistRowList}"> <th th:text="${iterStat.count}">1</th> <th th:text="${row.isbn}">978-1-4302-5908-4</th> <th th:text="${row.bookName}">Spring Recipes</th> </tr> </tbody> </table> <input type="hidden" name="lendingAppId" th:value="*{lendingAppId}"/> <div class="text-center"> <button class="btn bg-blue js-btn-register"><i class="fa fa-save"></i> 登録</button> <button class="btn bg-orange js-btn-backindex"><i class="fa fa-undo"></i> ファイルをアップロードし直す</button> </div> </form> </div> </div> </div> </div> </section> <!-- /.content --> </div> <!-- /.container --> </div> </div> <!-- ./wrapper --> <script th:replace="common/bottom-js"></script> <script type="text/javascript"> <!-- $(document).ready(function() { $(".js-btn-register").click(function(){ $("#registerBooklistForm").submit(); return false; }); $(".js-btn-backindex").click(function(){ location.href = "/booklist"; return false; }); }); --> </script> </body> </html>
<input type="hidden" th:value="*{lendingAppId}"/>
→<input type="hidden" name="lendingAppId" th:value="*{lendingAppId}"/>
へ変更します。
build.gradle
dependencies { def jdbcDriver = "org.postgresql:postgresql:9.4-1203-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.springframework.boot:spring-boot-starter-amqp") compile("org.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") // (ここから) gradle でテストを実行した場合に spring-security-test-4.0.2.RELEASE.jar しか classpath に指定されず // テストが失敗したため、3.2.8.RELEASE を明記している testCompile("org.springframework.security:spring-security-core:3.2.8.RELEASE") testCompile("org.springframework.security:spring-security-web:3.2.8.RELEASE") // (ここまで) ------------------------------------------------------------------------------------------------------ testCompile("org.springframework.security:spring-security-test:4.0.2.RELEASE") testCompile("org.yaml:snakeyaml") // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの compile("${jdbcDriver}") compile("org.seasar.doma:doma:2.4.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") compile("org.simpleframework:simple-xml:2.7.1") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.6.2") compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.6.2") compile("com.univocity:univocity-parsers:1.5.6") testCompile("org.dbunit:dbunit:2.5.1") testCompile("com.icegreen:greenmail:1.4.1") testCompile("org.assertj:assertj-core:3.2.0") testCompile("com.jayway.jsonpath:json-path:2.0.0") testCompile("org.jmockit:jmockit:1.19") // for Doma-Gen domaGenRuntime("org.seasar.doma:doma-gen:2.4.1") domaGenRuntime("${jdbcDriver}") }
compile("org.springframework.boot:spring-boot-starter-amqp")
を追加する。
application-develop.properties, application-unittest.properties, application-product.properties
■application-develop.properties
spring.datasource.url=jdbc:log4jdbc:postgresql://localhost/ksbylending spring.datasource.username=ksbylending_user spring.datasource.password=xxxxxxxx spring.datasource.driverClassName=net.sf.log4jdbc.sql.jdbcapi.DriverSpy spring.mail.host=localhost spring.mail.port=25 spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.redis.sentinel.master=mymaster spring.redis.sentinel.nodes=localhost:6381,localhost:6382,localhost:6383 spring.messages.cache-seconds=0 spring.thymeleaf.cache=false spring.velocity.cache=false
- spring.rabbitmq.host, spring.rabbitmq.port を追加します。
■application-unittest.properties
spring.datasource.url=jdbc:postgresql://localhost/ksbylending spring.datasource.username=ksbylending_user spring.datasource.password=xxxxxxxx spring.datasource.driverClassName=org.postgresql.Driver spring.mail.host=localhost spring.mail.port=25 spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.redis.sentinel.master=mymaster spring.redis.sentinel.nodes=localhost:6381,localhost:6382,localhost:6383 spring.thymeleaf.cache=true
- spring.rabbitmq.host, spring.rabbitmq.port を追加します。
■application-product.properties
server.tomcat.basedir=C:/webapps/ksbysample-webapp-lending spring.datasource.url=jdbc:postgresql://localhost/ksbylending spring.datasource.username=ksbylending_user spring.datasource.password=xxxxxxxx spring.datasource.driverClassName=org.postgresql.Driver spring.mail.host=localhost spring.mail.port=25 spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.redis.sentinel.master=mymaster spring.redis.sentinel.nodes=localhost:6381,localhost:6382,localhost:6383 spring.thymeleaf.cache=true
- spring.rabbitmq.host, spring.rabbitmq.port を追加します。
Constant.java
package ksbysample.webapp.lending.config; public class Constant { /* * RabbitMQ Queue一覧 */ public static final String QUEUE_NAME_INQUIRING_STATUSOFBOOK = "InquiringStatusOfBookQueue"; /* * URL一覧 */ public static final String URL_ADMIN_LIBRARY = "/admin/library"; /* * ログイン後ページのURL */ public static final String URL_AFTER_LOGIN_FOR_ROLE_ADMIN = URL_ADMIN_LIBRARY; }
- QUEUE_NAME_INQUIRING_STATUSOFBOOK を追加します。
ApplicationConfig.java
package ksbysample.webapp.lending.config; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ApplicationConfig { @Autowired private ConnectionFactory connectionFactory; @Bean public Queue inquiringStatusOfBookQueue() { return new Queue(Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK, false); } @Bean public RabbitTemplate rabbitTemplate() { RabbitTemplate rabbitTemplate = new RabbitTemplate(this.connectionFactory); rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); return rabbitTemplate; } }
private ConnectionFactory connectionFactory;
を追加します。- queueInquiringStatusOfBook Bean を追加します。
- rabbitTemplate Bean を追加して、中で Jackson2JsonMessageConverter をセットします。
InquiringStatusOfBookQueueMessage.java
package ksbysample.webapp.lending.service.queue; import lombok.Data; @Data public class InquiringStatusOfBookQueueMessage { private Long lendingAppId; }
InquiringStatusOfBookQueueService.java
package ksbysample.webapp.lending.service.queue; import ksbysample.webapp.lending.config.Constant; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class InquiringStatusOfBookQueueService { @Autowired private RabbitTemplate rabbitTemplate; public void sendMessage(Long lendingAppId) { InquiringStatusOfBookQueueMessage message = new InquiringStatusOfBookQueueMessage(); message.setLendingAppId(lendingAppId); rabbitTemplate.convertAndSend(Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK, message); } }
BooklistService.java
package ksbysample.webapp.lending.web.booklist; import ksbysample.webapp.lending.dao.LendingAppDao; import ksbysample.webapp.lending.dao.LendingBookDao; import ksbysample.webapp.lending.entity.LendingApp; import ksbysample.webapp.lending.entity.LendingBook; import ksbysample.webapp.lending.security.LendingUserDetails; import ksbysample.webapp.lending.service.file.BooklistCSVRecord; import ksbysample.webapp.lending.service.file.BooklistCsvFileService; import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.List; import static ksbysample.webapp.lending.values.LendingAppStatusValues.TENPORARY_SAVE; @Service public class BooklistService { @Autowired private BooklistCsvFileService booklistCsvFileService; @Autowired private LendingAppDao lendingAppDao; @Autowired private LendingBookDao lendingBookDao; @Autowired private InquiringStatusOfBookQueueService inquiringStatusOfBookQueueService; public Long temporarySaveBookListCsvFile(UploadBooklistForm uploadBooklistForm) { // アップロードされたCSVファイルのデータを List に変換する List<BooklistCSVRecord> booklistCSVRecordList = booklistCsvFileService.convertFileToList(uploadBooklistForm.getFileupload()); // 現在ログインしているユーザ情報を取得する Authentication auth = SecurityContextHolder.getContext().getAuthentication(); LendingUserDetails lendingUserDetails = (LendingUserDetails) auth.getPrincipal(); // lending_app テーブルにデータを保存する LendingApp lendingApp = new LendingApp(); lendingApp.setStatus(TENPORARY_SAVE.getValue()); lendingApp.setLendingUserId(lendingUserDetails.getUserId()); lendingAppDao.insert(lendingApp); // lending_book テーブルにデータを保存する LendingBook lendingBook; for (BooklistCSVRecord booklistCSVRecord : booklistCSVRecordList) { lendingBook = new LendingBook(); BeanUtils.copyProperties(booklistCSVRecord, lendingBook); lendingBook.setLendingAppId(lendingApp.getLendingAppId()); lendingBookDao.insert(lendingBook); } return lendingApp.getLendingAppId(); } public List<LendingBook> getLendingBookList(Long lendingAppId) { List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingAppId); return lendingBookList; } public void updateLendingAppStatusToPending(RegisterBooklistForm registerBooklistForm) { inquiringStatusOfBookQueueService.sendMessage(registerBooklistForm.getLendingAppId()); } }
- updateLendingAppStatusToPending メソッドを追加します。
RegisterBooklistForm.java
package ksbysample.webapp.lending.web.booklist; import ksbysample.webapp.lending.entity.LendingBook; import lombok.Data; import org.springframework.beans.BeanUtils; import java.util.List; import java.util.stream.Collector; import java.util.stream.Collectors; @Data public class RegisterBooklistForm { private List<RegisterBooklistRow> registerBooklistRowList; private Long lendingAppId; public RegisterBooklistForm() { } public RegisterBooklistForm(List<LendingBook> lendingBookList, Long lendingAppId) { this.registerBooklistRowList = lendingBookList.stream() .map(RegisterBooklistRow::new) .collect(Collectors.toList()); this.lendingAppId = lendingAppId; } @Data public class RegisterBooklistRow { private String isbn; private String bookName; public RegisterBooklistRow(LendingBook lendingBook) { BeanUtils.copyProperties(lendingBook, this); } } }
- 引数のないコンストラクタ
RegisterBooklistForm()
を追加します。
BooklistController.java
package ksbysample.webapp.lending.web.booklist; import ksbysample.webapp.lending.entity.LendingBook; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import java.util.List; @Controller @RequestMapping("/booklist") public class BooklistController { @Autowired private UploadBooklistFormValidator uploadBooklistFormValidator; @Autowired private BooklistService booklistService; @InitBinder("uploadBooklistForm") public void initBinder(WebDataBinder binder) { binder.addValidators(uploadBooklistFormValidator); } @RequestMapping public String index(UploadBooklistForm uploadBooklistForm) { return "booklist/booklist"; } @RequestMapping("/fileupload") public String fileupload(@Validated UploadBooklistForm uploadBooklistForm , BindingResult bindingResult , Model model) { if (bindingResult.hasErrors()) { return "booklist/booklist"; } // アップロードされたCSVファイルのデータをDBに保存する Long lendingAppId = booklistService.temporarySaveBookListCsvFile(uploadBooklistForm); // 確認画面に表示するデータを取得する List<LendingBook> lendingBookList = booklistService.getLendingBookList(lendingAppId); RegisterBooklistForm registerBooklistForm = new RegisterBooklistForm(lendingBookList, lendingAppId); model.addAttribute("registerBooklistForm", registerBooklistForm); return "booklist/fileupload"; } @RequestMapping("/register") public String register(RegisterBooklistForm registerBooklistForm , RedirectAttributes redirectAttributes) { booklistService.updateLendingAppStatusToPending(registerBooklistForm); redirectAttributes.addFlashAttribute("lendingAppId", registerBooklistForm.getLendingAppId()); return "redirect:/booklist/complete"; } @RequestMapping("/complete") public String complete() { return "booklist/complete"; } }
- register メソッドの以下の点を変更します。
complete.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>貸出希望書籍 CSV ファイルアップロード</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <link th:replace="common/head-cssjs"/> <style type="text/css"> <!-- .lending-oneline-msgbox { height: 70px; padding-top: 10px; } --> </style> </head> <!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. --> <body class="skin-blue layout-top-nav"> <div class="wrapper"> <!-- Main Header --> <div th:replace="common/mainparts :: main-header"></div> <!-- Full Width Column --> <div class="content-wrapper"> <div class="container"> <!-- Content Header (Page header) --> <section class="content-header"> <h1>貸出希望書籍 CSV ファイルアップロード</h1> </section> <!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12"> <div class="lending-oneline-msgbox"> <p><span class="text-bold">貸出申請ID:<span th:text="${lendingAppId}">1</span></span><br/> で貸出希望書籍を登録しました。選択中の図書館に蔵書の有無と貸出状況を問い合わせます。</p> </div> <button class="btn bg-blue js-btn-moveindex"><i class="fa fa-file-text"></i> 別の貸出希望書籍を登録する</button> </div> </div> </section> <!-- /.content --> </div> <!-- /.container --> </div> </div> <!-- ./wrapper --> <script th:replace="common/bottom-js"></script> <script type="text/javascript"> <!-- $(document).ready(function() { $(".js-btn-moveindex").click(function(){ location.href = "/booklist"; return false; }); }); --> </script> </body> </html>
.lending-oneline-msgbox
内でheight: 50px;
→height: 70px;
へ変更します。<span class="text-bold">貸出申請ID:<span th:text="${lendingAppId}">1</span></span><br/>
を追加します。
履歴
2015/10/04
初版発行。