Spring Boot でメール送信する Web アプリケーションを作る ( その6 )( メール送信画面の作成 )
概要
Spring Boot でメール送信する Web アプリケーションを作る ( その5 )( Controller クラス、Thymeleaf テンプレートファイルの作成 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- メール送信画面の作成
- 今回はメール送信機能を実装します。
ソフトウェア一覧
参考にしたサイト
spring-boot-sample-velocity
https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples/spring-boot-sample-velocityVelocityAutoConfiguration.java
https://github.com/spring-projects/spring-boot/blob/1.2.x/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/velocity/VelocityAutoConfiguration.javaSpring Boot Reference Guide - Appendix A. Common application properties
http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.htmlどこよりも早い Spring Boot 1.2 解説 #渋谷
http://www.slideshare.net/makingx/spring-boot12- Spring Boot 1.2 のメール送信機能が書かれている 36ページからの「Email送信サポート」を参照しました。
手順
1.0.x-sendmail ブランチの作成
build.gradle の変更
spring-boot-sample-velocity の pom.xml では
<groupId>org.apache.velocity</groupId><artifactId>velocity</artifactId>
が記述されていて spring-boot-starter-velocity は記述されていないのですが、spring-boot-starter-velocity を build.gradle に記述すればいろいろ自動設定されるのではないかと思いましたので、使うべきか否かを判断するために調査しました。- おそらく自動設定される内容が実装されていると思われる VelocityAutoConfiguration.java を見た感じでは、
- spring.velocity.enabled = false を設定すれば、HTML のテンプレートファイル用の設定は反映されない ( velocityViewResolver Bean が生成されない )。
- Velocity を使用するために必要な velocityEngine Bean が自動で設定される。
- Velocity 用の設定は Spring Boot で用意されている Appendix A. Common application properties の "spring.velocity" のものが使用できる。
- 上記の内容であれば spring-boot-starter-velocity を記述して使用した方が良さそうです。
- 念のため
compile("org.apache.velocity:velocity")
だけでも使用できるのか確認するために簡単なサンプルを作成してみたところ、org.springframework.ui.velocity.VelocityEngineUtils
が使用できませんでした。build.gradle にcompile("org.apache.velocity:velocity")
を書くだけでは Velocity は使用できないようです。 - よって spring-boot-starter-velocity を記述することにします。
- おそらく自動設定される内容が実装されていると思われる VelocityAutoConfiguration.java を見た感じでは、
build.gradle を リンク先の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」アイコンをクリックして、変更した build.gradle の内容を反映します。
Velocity、Spring Boot のメール送信機能を使用するための設定
Velocity は以下の設定をします。
- application.properties に以下の項目を設定します。
- application-develop.properties に以下の項目を設定します。
- spring.velocity.cache = false
- develop 環境では cache は無効にします。
- spring.velocity.cache = false
Spring Boot のメール送信機能は以下の設定をします。
- application-develop.properties, application-product.properties, application-unittest.properties それぞれに以下の項目を設定します ( 今回設定するのは同じ値ですが、通常テストと本番環境で設定が変わる項目なので環境毎の設定ファイルの方に記述します ) 。
- spring.mail.host = localhost
- spring.mail.port = 25
- application-develop.properties, application-product.properties, application-unittest.properties それぞれに以下の項目を設定します ( 今回設定するのは同じ値ですが、通常テストと本番環境で設定が変わる項目なので環境毎の設定ファイルの方に記述します ) 。
application.properties, application-develop.properties, application-product.properties, application-unittest.properties は リンク先の内容 になります。
Form クラスの作成
src/main/java/ksbysample/webapp/email/web/mailsend の下に MailsendForm.java を作成します。作成後、リンク先の内容 に変更します。
html ファイルに記述ミスがあったので修正します。src/main/resources/templates/mailsend の下の mailsend.html を リンク先のその1の内容 に変更します。
Thymeleaf テンプレートファイルの修正
メール送信画面の「性別」「項目」「商品」に表示する選択肢を定義するファイルを作成します。src/main/resources の下に constant.yml を作成します。作成後、リンク先の内容 に変更します。
constant.yml のデータを取得するためのクラスを作成します。src/main/java/ksbysample/webapp/email/config の下に Constant.java を作成します。作成後、リンク先の内容 に変更します。
src/main/resources/templates/mailsend の下の mailsend.html を リンク先のその2の内容 に変更します。
Controller クラスの修正
- src/main/java/ksbysample/webapp/email/web の下の MailsendController.java を リンク先のその1の内容 に変更します。
一旦、動作確認
まずはここまでの変更で画面が正常に表示されることを確認します。Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/mailsend へアクセスします。画面が正常に表示されることを確認します。
Velocity テンプレートファイルの作成
Velocity テンプレートファイルの配置場所は Appendix A. Common application properties に記載されている spring.velocity.resource-loader-path のデフォルト設定では classpath:/templates/ になっていますので、templates ディレクトリの下に作成します。
src/main/resources/templates の下に mail/MAIL001 ディレクトリを作成します。
src/main/resources/templates/mail/MAIL001 の下に MAIL001-body.vm を作成します。作成後、リンク先の内容 に変更します。
Velocity 用ユーティリティクラスの作成
org.springframework.ui.velocity.VelocityEngineUtils は少し使いにくいので、Web アプリケーション専用のユーティリティクラスを作成します。
src/main/java/ksbysample/webapp/email の下に util パッケージを作成します。
src/main/java/ksbysample/webapp/email/util の下に VelocityUtils.java を作成します。作成後、リンク先の内容 に変更します。
Stream API を使用できるよう設定する
次のクラスを作成しようとしたら、プロジェクトのデフォルトの設定では Stream API が使用できませんでした。。。 設定を変更します。
メイン画面のメニューから「File」->「Project Structure...」を選択して「Project Structure」ダイアログを表示します。
画面左側のツリーから「Project Settings」-「Project」を選択し、画面右側で「Project language level」の設定を「6 - @Override in interfaces」→「8 - lambdas, type annotations etc.」へ変更して「OK」ボタンをクリックします。
※Spring Boot でメール送信する Web アプリケーションを作る ( その3 )( Project の作成 ) の方にも後で記述を追加しておきます。
メール生成クラスの作成
- src/main/java/ksbysample/webapp/email/web/mailsend の下に MAIL001MailBuilder.java を作成します。作成後、リンク先の内容 に変更します。
メール送信用 Service クラスの作成
src/main/java/ksbysample/webapp/email の下に service パッケージを作成します。
src/main/java/ksbysample/webapp/email/service の下に EmailService.java を作成します。作成後、リンク先の内容 に変更します。
メール送信処理の実装
- src/main/java/ksbysample/webapp/email/web/mailsend の下の MailsendController.java を リンク先のその2の内容 に変更します。
動作確認
動作確認します。Run View で Ctrl+F5 を押して Tomcat を再起動します。
ブラウザを起動し http://localhost:8080/mailsend へアクセスします。以下の画像の値を入力後、「送信」ボタンをクリックします。
メールが送信され、smtp4dev の画面に表示されます。
smtp4dev の画面のリストに表示されているデータをダブルクリックすると Windows Live メールの画面が開き、送信されたメールの内容が表示されます。
Run View で Ctrl+F2 を押して Tomcat を停止します。
commit、Push、Pull Request、マージ
commit します。commit 時に Code Analysis のダイアログが表示されますので、「Review」ボタンをクリックして以下の対応をします。
- Unused property の Warning は無視します。
- Attribute th:... is not allowed here の Warning が出ていましたので、出なくなるようにします。対象は th:object, th:field, th:each, th:value, th:text です。
commit、GitHub へ Push、1.0.x-sendmail -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-sendmail ブランチを削除、をします。
課題&次回は。。。
- 次回は Validation、その次にメール保存処理、最後にテストを実装します。
- Thymeleaf テンプレートに設定ファイルの値を表示する方法が全然スマートじゃないですね。何か良い例はないものでしょうか。
- クラスの設計 ( オブジェクト指向開発? ) がよく分かっていないなあと今回思いました。メール送信処理は Web や本等を見ながら作っては見たのですが、たぶんオブジェクト指向開発ならこういう風に作った方がよいみたいなものがあると思うんですよね。。。
ソースコード
build.gradle
dependencies { // 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.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.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.yaml:snakeyaml") // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの compile("org.postgresql:postgresql:9.4-1201-jdbc41") compile("org.seasar.doma:doma:2.2.0") 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.2") testCompile("org.dbunit:dbunit:2.5.0") }
compile("org.apache.velocity:velocity")
を削除します。compile("org.springframework.boot:spring-boot-starter-velocity")
を追加します。compile("org.springframework.boot:spring-boot-starter-mail")
を追加します。
application.properties, application-develop.properties, application-product.properties, application-unittest.properties
■application.properties
hibernate.dialect = org.hibernate.dialect.PostgreSQL9Dialect spring.jpa.hibernate.ddl-auto = none spring.jpa.hibernate.naming_strategy = org.hibernate.cfg.ImprovedNamingStrategy spring.velocity.enabled = false spring.velocity.charset = UTF-8
- spring.velocity.enabled, spring.velocity.charset を追加します。
■application-develop.properties
spring.datasource.url = jdbc:log4jdbc:postgresql://localhost/postgres spring.datasource.username = postgres spring.datasource.password = xxxxxxxx spring.datasource.driverClassName = net.sf.log4jdbc.sql.jdbcapi.DriverSpy spring.mail.host = localhost spring.mail.port = 25 spring.messages.cache-seconds = 0 spring.thymeleaf.cache = false spring.velocity.cache = false
- spring.mail.host, spring.mail.port, spring.velocity.cache を追加します。
■application-product.properties
server.tomcat.basedir = C:/webapps/ksbysample-webapp-email spring.datasource.url = jdbc:postgresql://localhost/postgres spring.datasource.username = postgres spring.datasource.password = xxxxxxxx spring.datasource.driverClassName = org.postgresql.Driver spring.mail.host = localhost spring.mail.port = 25 spring.thymeleaf.cache = true
- spring.mail.host, spring.mail.port を追加します。
■application-unittest.properties
spring.datasource.url = jdbc:postgresql://localhost/postgres spring.datasource.username = postgres spring.datasource.password = xxxxxxxx spring.datasource.driverClassName = org.postgresql.Driver spring.mail.host = localhost spring.mail.port = 25 spring.thymeleaf.cache = true
- spring.mail.host, spring.mail.port を追加します。
MailsendForm.java
package ksbysample.webapp.email.web.mailsend; import lombok.Data; import java.util.List; @Data public class MailsendForm { private String from; private String to; private String subject; private String name; private String sex; private String type; private List<String> item; private String naiyo; }
mailsend.html
■その1
<div class="form-group"> <label for="subject" class="control-label col-sm-2">Subject</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-12"><input type="text" name="subject" id="subject" class="form-control input-sm" value="" placeholder="件名を入力して下さい"/></div></div> <!--<div class="row"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>--> </div> </div>
<label for="to" ...
→<label for="subject" ...
に変更します。
■その2
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>ksbysample-webapp-email</title> <meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' name='viewport'/> <meta th:replace="common/head-cssjs"/> <style> <!-- .checkbox label, .radio label { padding-right: 10px; } --> </style> </head> <body class="skin-blue"> <div class="wrapper"> <!-- Main Header --> <div th:replace="common/mainparts :: main-header"></div> <!-- Left side column. contains the logo and sidebar --> <div th:replace="common/mainparts :: main-sidebar (active='mailsend')"></div> <!-- Content Wrapper. Contains page content --> <div class="content-wrapper"> <!-- Content Header (Page header) --> <section class="content-header"> <h1> メール送信画面 </h1> </section> <!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12"> <form id="mailsendForm" method="post" action="/mailsend/send" th:action="@{/mailsend/send}" th:object="${mailsendForm}" class="form-horizontal"> <!--<div class="callout callout-danger">--> <!--<p>共通エラーメッセージ表示エリア</p>--> <!--</div>--> <div class="box box-primary"> <div class="box-body"> <div class="form-group"> <label for="from" class="control-label col-sm-2">From</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-8"><div class="input-group"><span class="input-group-addon"><i class="fa fa-envelope"></i></span><input type="text" name="from" id="from" class="form-control input-sm" value="" placeholder="Fromアドレスを入力して下さい" th:field="*{from}"/></div></div></div> <!--<div class="row"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>--> </div> </div> <div class="form-group"> <label for="to" class="control-label col-sm-2">To</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-8"><div class="input-group"><span class="input-group-addon"><i class="fa fa-envelope"></i></span><input type="text" name="to" id="to" class="form-control input-sm" value="" placeholder="Toアドレスを入力して下さい" th:field="*{to}"/></div></div></div> <!--<div class="row"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>--> </div> </div> <div class="form-group"> <label for="subject" class="control-label col-sm-2">Subject</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-12"><input type="text" name="subject" id="subject" class="form-control input-sm" value="" placeholder="件名を入力して下さい" th:field="*{subject}"/></div></div> <!--<div class="row"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>--> </div> </div> <div class="form-group"> <label for="name" class="control-label col-sm-2">氏名</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-8"><input type="text" name="name" id="name" class="form-control input-sm" value="" placeholder="(例) 田中 太郎" th:field="*{name}"/></div></div> <!--<div class="row"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>--> </div> </div> <div class="form-group"> <label class="control-label col-sm-2">性別</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-12"> <div class="radio"> <label th:each="sex : ${T(ksbysample.webapp.email.config.Constant).getInstance().SEX_MAP.entrySet()}"> <input type="radio" name="sex" th:value="${sex.getKey()}" th:text="${sex.getValue()}" th:field="*{sex}"/> </label> </div> </div></div> <!--<div class="row"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>--> </div> </div> <div class="form-group"> <label class="control-label col-sm-2">項目</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-8"> <select name="type" id="type" class="form-control input-sm" th:field="*{type}"> <option th:each="type : ${T(ksbysample.webapp.email.config.Constant).getInstance().TYPE_MAP.entrySet()}" th:value="${type.getKey()}" th:text="${type.getValue()}">sex</option> </select> </div></div> <!--<div class="row"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>--> </div> </div> <div class="form-group"> <label class="control-label col-sm-2">商品</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-12"> <div class="checkbox"> <label th:each="item : ${T(ksbysample.webapp.email.config.Constant).getInstance().ITEM_MAP.entrySet()}"> <input type="checkbox" name="item" th:value="${item.getKey()}" th:text="${item.getValue()}" th:field="*{item}"/> </label> </div> </div></div> <!--<div class="row"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>--> </div> </div> <div class="form-group"> <label for="naiyo" class="control-label col-sm-2">内容</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-12"><textarea rows="5" name="naiyo" id="naiyo" class="form-control input-sm" placeholder="お問い合わせ内容を入力して下さい" th:field="*{naiyo}"></textarea></div></div> <!--<div class="row"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>--> </div> </div> </div> <div class="box-footer"> <div class="text-center"> <button type="button" id="send" value="send" class="btn btn-primary">送信</button> </div> </div> </div> </form> </div> </div> </section> <!-- /.content --> </div> <!-- /.content-wrapper --> </div> <!-- ./wrapper --> <!-- REQUIRED JS SCRIPTS --> <div th:replace="common/bottom-js"></div> <script type="text/javascript"> <!-- $(document).ready(function() { $('#from').focus(); $('#send').bind('click', function(){ $('#mailsendForm').submit(); }); }); --> </script> </body> </html>
- 以下の変更をします。
- チェックボックス、ラジオボタンの右側のスペースを調整したいので、
.checkbox label, .radio label { padding-right: 10px; }
の定義を追加します。 - form タグの id を
mailsendForm
に変更し、th:object="${mailsendForm}"
を追加します。 - 性別のラジオボタン、項目のドロップダウンリスト、商品のチェックボックスの値、名称を Constant クラスから取得して出力するよう変更します。
- 各入力項目に
th:field="*{...}"
を追加します。 - 最後に記述している Javascript で、
$('#mailSendForm').submit();
→$('#mailsendForm').submit();
へ変更します。
- チェックボックス、ラジオボタンの右側のスペースを調整したいので、
constant.yml
!!ksbysample.webapp.email.config.Constant SEX_MAP: 1: 男性 2: 女性 TYPE_MAP: 1: 資料請求 2: 商品に関する苦情 3: その他 ITEM_MAP: 101: 商品1 102: 商品2 103: 商品3
Constant.java
package ksbysample.webapp.email.config; import org.springframework.stereotype.Component; import org.yaml.snakeyaml.Yaml; import java.util.Map; @Component public class Constant { private static final Constant instance = (Constant) new Yaml().load(Constant.class.getResourceAsStream("/constant.yml")); public static Constant getInstance() { return instance; } // 性別 public Map<String, String> SEX_MAP; // 問い合わせ項目 public Map<String, String> TYPE_MAP; // 商品 public Map<String, String> ITEM_MAP; }
- @Component アノテーションを付加しているのは、Tomcat 起動時に Spring Framework の DI コンテナにインスタンスを生成させるためです。
- インスタンス生成時に constant.yml のデータが SEX_MAP, TYPE_MAP, ITEM_MAP にセットされます。
MailsendController.java
■その1
@RequestMapping public String index(MailsendForm mailsendForm , Model model) { return "mailsend/mailsend"; }
- index メソッドの引数に
MailsendForm mailsendForm
とModel model
を追加します。
■その2
package ksbysample.webapp.email.web.mailsend; import ksbysample.webapp.email.service.EmailService; import ksbysample.webapp.email.util.VelocityUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/mailsend") public class MailsendController { @Autowired private VelocityUtils velocityUtils; @Autowired private EmailService emailService; @RequestMapping public String index(MailsendForm mailsendForm , Model model) { return "mailsend/mailsend"; } @RequestMapping("/send") public String send(MailsendForm mailsendForm , Model model) { // メールを送信する SimpleMailMessage mailMessage = MAIL001MailBuilder.build() .setForm(mailsendForm) .setVelocityUtils(velocityUtils) .setTemplateLocation("mail/MAIL001/MAIL001-body.vm") .create(); emailService.sendSimpleMail(mailMessage); return "redirect:/mailsend"; } }
private VelocityUtils velocityUtils;
を追加します。private EmailService emailService;
を追加します。- send メソッドを上記の内容に変更します。
MAIL001-body.vm
氏名 : $name 性別 : $sex 項目 : $type 商品 : $item 内容 : $naiyo
VelocityUtils.java
package ksbysample.webapp.email.util; import org.apache.velocity.app.VelocityEngine; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.ui.velocity.VelocityEngineUtils; import java.util.Map; public class VelocityUtils { @Autowired private VelocityEngine velocityEngine; @Value("${spring.velocity.charset}") private String charset; public String merge(String templateLocation, Map<String, Object> model) { return VelocityEngineUtils.mergeTemplateIntoString(this.velocityEngine, templateLocation, charset, model); } }
MAIL001MailBuilder.java
package ksbysample.webapp.email.web.mailsend; import ksbysample.webapp.email.config.Constant; import ksbysample.webapp.email.util.VelocityUtils; import org.springframework.mail.SimpleMailMessage; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; public class MAIL001MailBuilder { private MailsendForm mailsendForm; private VelocityUtils velocityUtils; private String templateLocation; public static MAIL001MailBuilder build() { return new MAIL001MailBuilder(); } public MAIL001MailBuilder setForm(MailsendForm mailsendForm) { this.mailsendForm = mailsendForm; return this; } public MAIL001MailBuilder setVelocityUtils(VelocityUtils velocityUtils) { this.velocityUtils = velocityUtils; return this; } public MAIL001MailBuilder setTemplateLocation(String templateLocation) { this.templateLocation = templateLocation; return this; } public SimpleMailMessage create() { SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setFrom(mailsendForm.getFrom()); mailMessage.setTo(mailsendForm.getTo()); mailMessage.setSubject(mailsendForm.getSubject()); Constant constant = Constant.getInstance(); Map<String, Object> model = new HashMap<>(); model.put("name", mailsendForm.getName()); model.put("sex", constant.SEX_MAP.get(mailsendForm.getSex())); model.put("type", constant.TYPE_MAP.get(mailsendForm.getType())); String itemList = mailsendForm.getItem().stream() .map(constant.ITEM_MAP::get) .collect(Collectors.joining(", ")); model.put("item", itemList); model.put("naiyo", mailsendForm.getNaiyo()); mailMessage.setText(velocityUtils.merge(this.templateLocation, model)); return mailMessage; } }
EmailService.java
package ksbysample.webapp.email.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.MailSender; import org.springframework.mail.SimpleMailMessage; import org.springframework.stereotype.Service; @Service public class EmailService { @Autowired private MailSender mailSender; public void sendSimpleMail(SimpleMailMessage mailMessage) { mailSender.send(mailMessage); } }
履歴
2015/04/22
初版発行。