メリークリスマス! 「Javaアドベントカレンダー2020」の最終日です。
クリスマスらしく、楽しい(?)内容になるようにタイトルを変えました。ライブラリの紹介だけと思っていましたが、クリスマスだし、ライブラリを使って役に立つウェブサービスを作ってみよう、というわけです。
RESTfulウェブサービスについて基本から説明しているので、この分野を知りたい人にもおススメですよ。
なお、ライブラリの詳細は、投稿の最後にまとめましたので、気になる方は、先に、そちらを見てください。
#1.メールサービスの作成 --- Senderクラス
##宛名を変えて、同じメールを一斉に送ってくれるサービス
つまりですね、
○○ 様
こんにちは、・・・。
のような、宛名と敬称付きで、後の内容は同じ、というメールを、名簿にある人達に一斉に送ってくれるサービスです(宛名と敬称がつくところがポイントです)。メール送信は、ライブラリを使うので数行ですし、送信サービスもJakarta RESTの仕組みを使うと簡単に書けるはずです。ちなみに、Jakarta RESTの開発・実行環境はJETを使うと5分で準備できますので、初めての方も是非トライしてみてください。
(ここからJETをダウンロードできます。使い方の解説やビデオもあります。)
一方、サービスを利用するクライアントプログラムは、ウェブ画面を作ってデータをPOSTで送るだけなので、PHPやJavascriptなどあらゆる言語で作成できます。ウェブサービスの強みのひとつです。ただ、JavaにもクライアントAPIがあるので、Javaで作っても何の不都合もありません。
(これから解説するメール送信サービスとクライアントプログラムのプロジェクトはここからダウンロードしてください。また、プロジェクトに同梱している重要.txtファイルには、SMTPサーバーの設定について、注意が書いてあります。必ず、読んだ上で利用してください。)
##こんな形になりました
先に目に見える結果を見ておきましょう。サービスを利用するクライアントの画面です。
上段は、メールの内容を書く領域で、下段は名簿画面です。名簿はExcelファイルをアップロードします。自動アップロードなので、ファイルを選択するだけでアップロードされ、内容が表示されます。左端のチェックボックスは、送信先として使うかどうかの選択欄です。初期は全員が対象になっていますが、不要な宛先はチェックを外したりできます。
##まず、RESTfulウェブサービスとは
GETやPOSTなどで特定のURI(URLとほぼ同じ)にアクセスすると、決められた仕事をやってくれるのがRESTfulウェブサービスです。API的なメソッド呼び出しは一切なく、単なるHTTPアクセスで対応する処理を実行します。GET、POST、PUT、DELETEなどの"モード"と、アクセスするURIの組み合わせで、どんな仕事をするのか切り分けます。アクセスと同時に、URLパラメータやPOSTデータを使って、必要なデータも渡せます。
URIは、
http://サーバー/プロジェクト/サービス/パス/
のように構成します。
例えば、
サーバー:localhost:8080
プロジェクト名:mailservice
全体のサービス名:sendmail
ひとつのサービス名(ここでは同報メール送信):cc
という名前にする場合、URIは次のようになります。
http://localhost:8080/mailservice/sendmail/cc/
なお、メールにはテキストメールとHTMLメールがあるので、さらにサブパスを切って、次のようにできます。
http://localhost:8080/mailservice/sendmail/cc/text
これはテキストメールの同報送信を行うURIです。HTMLメールなら
http://localhost:8080/mailservice/sendmail/cc/html
とでもしておけばいいでしょう。
##URIを構成するための書き方 --- Jakarta REST
Jakarta REST(=JAX-RS)を使うので、これはもう簡単です。
まず、プロジェクト名は作ったプロジェクトの名前ですから問題ありませんね。
それ以外の部分は、何と、プロジェクト内のクラスやメソッドに、決められたアノテーションを付けるだけでいいのです。
最初に、サービス(sendmail)の部分ですが、次のように中身のないクラス(名前は任意)を作って、@ApplicationPath("sendmail")を付けます。これだけです。
@ApplicationPath("sendmail")
public class ServiceConfig extends Application {
}
次に個別のサービスを表すパスは、別にクラス(名前は任意。Senderクラス)を作って、@Path("cc")を付ければOKです。それから、このSenderクラス内に具体的なサービス機能を作成します。
@Path("cc")
public class Sender {
}
最後に、サブパスtextですが、これは、Senderクラス内で実際に処理を担当するメソッド(sendメソッド)に、@Path("text")を付けます。また、POSTでアクセスするメソッドなので、@POSTアノテーションも付けておきます。
@Path("cc")
public class Sender {
@Path("text")
@POST
public Response send(DataSet data){
}
}
これだけで、POSTアクセスに対応するURI、
http://localhost:8080/mailservice/sendmail/cc/text
が構成できました。クライアント側で、このURIにPOSTアクセスすると、sendメソッドが起動します。
後はsendメソッドの中身を作成するだけです。どうです、簡単でしょう!
##データの受け渡し
そうそう、sendメソッドの引数と戻り値型についての説明を忘れていました。
###(1)引数
まず、引数にはどんな型のオブジェクトでも渡せます。ここではDataSet型を渡すことにしています。DataSet型は、今回、作成したクラスで、内容は、メールタイトル、本文、宛先リストなど、送信に必要なすべてのデータを含みます。クライアント側が、POSTアクセスでこのオブジェクトを送信すると、サーバー側はそれをsendメソッドの引数、DataSet data
に受け取ることができます。DataSetクラスのフィールド変数は次のようです。
public class DataSet {
private String sender; // 送信者メールアドレス
private String title; // メールタイトル
private String message; // 本文 or HTML文
private String type; // "CC" or "BCC" or "TO"
private List<Recipient> dlist; // 宛先のリスト
フィールド変数のうち、List<Recipient> dlist
は宛先のListです。Recipientクラスは次のようにしました。
public class Recipient {
private boolean flag; // 状態の記録用(送信対象とするかなど、用途は任意)
private String id; // 番号やIDなど
private String name; // 名前
private String email; // メールアドレス
private String mr; // 敬称(様、殿、先生など)
これらのオブジェクトは、実際には、JSON形式やXML形式のデータに自動変換されて受け渡されます。したがって、Java言語以外のクライアントでも、JSONやXMLを使って、同じものを受け渡しできます。つまり、プログラミング言語の制限はない、ということです。
受け渡すデータ形式を指定しなければ、適当に(といっても大抵はJSON)決められますが、やり取りする形式をアノテーションで指定しておくことができます。次は、JSONかXMLを使うというという指定です。
@Produces
で、sendメソッドが返すデータの形式を指定し、@Consumes
で、sendメソッドが引数に受け取るデータの形式を指定します。(ざっくりした言い方ですが)このように複数の形式を指定しておくと、クライアント側でどれかに対応できます。
@Path("cc")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class Sender {
@Path("text")
@POST
public Response send(DataSet data){
}
}
そうそう、うっかりするところでした。XML形式でやり取りする可能性があるオブジェクトには、クラスに@XmlRootElementというアノテーションを付けます。これだけで、XMLへの変換が自動的に行われるようになります(JSONについては何もする必要はありません)。
@XmlRootElement
public class DataSet {
###(2)戻り値型
メソッドはResponse型
のオブジェクトを返すのが普通です。Responseクラスは、オールインワンの便利なオブジェクトで、その中には、いろいろな応答ヘッダとその値、実行結果を表すステータス、呼び出し側に返すオブジェクトなどを含めることができます。HTTPの規約に従って送信されるので、クライアントプログラムは、それぞれの言語固有の標準的な方法でそれらを受け取ることができます。Java言語でのやり方は、後ほど説明します。
##では、メールを送信しよう!
sendメソッドの処理は次のように簡単です。
@Path("/text")
@POST
public Response send(DataSet data) {
EmailStatus result = semdMails(data); // メール送信処理
reply(data,result); // 送信結果をクライアントにメールで通知する
return Response
.ok()
.build();
}
sendMails(data)が、実際にメール送信を行う部分で、reply(data, result)が、送信結果をクライアントに通知する部分です(内容は後で説明)。
ただ、このままでは、ちょっと問題があります。
というのも、送信するメールの件数が何十もあると、送信に数十秒かかることが予想されます。その間、ブロッキングが発生して、クライアントに応答を返すことができなくなります。クライアントはボーッと待たされるので、かなり不安な気持ちになりますね。
ここはマルチスレッドの出番です。ひとまず別のスレッドで非同期に送信処理を実行しつつ、クライアントには、return Response.・・・
で「いまから送ります」的なメッセージを返しておきます。
sendMailsメソッドは実行結果を戻り値として返すので、これを使って、replyメソッドで結果をクライアントに知らせます。なので、sendMailsとreplyメソッドは一連の不可分な処理です。そこで、2つをまとめて1つのスレッドで実行するしかありません・・・。
###(1)スタイリッシュな非同期処理の書き方
ところで、ComputableFutuerクラスって知っていますか?
上記のような問題をカッコよく処理するためにJava8から導入されたクラスです。クリスマスですからね、ここはカッコよく書いてみましょう。
@Path("/text")
@POST
public Response send(DataSet data) {
CompletableFuture.supplyAsync(()->sendMails(data))
.thenAccept(result->reply(data,result));
return Response
.ok("送信を開始します。この後、送信結果のメールが届きます。")
.build();
}
どうでしょう、かなりスマートに書けました。
ComputableFutureは、最初に、supplyAsyncメソッドでsendMailsメソッドを非同期に実行し、それが終了して戻り値(プログラムではresult)を返したら、それを使って、thenAcceputメソッドでreplyメソッドを非同期に実行します。thenAcceputメソッドは、前段の非同期処理の結果を受け取り、それを使って、次の処理を非同期に実行することができるメソッドです。
全体は、メソッドチェーンでスタイリッシュな書き方をします。これだけのことを1文でかけるのですから、ComputableFutureって便利ですね。なお、supplyAsyncやthenAcceptは実行するメソッドを引数に取るので、引数はラムダ式で書くことになっています。
「でも、ラムダ式は・・・」という方も大丈夫です。ここは、やっていることの意味がわかればいいだけですから。ラムダ式については、「わかりやすいJava オブジェクト指向徹底解説
」で平易に解説していますのでご覧ください (^_^;。また、同書では、ComputableFutuerについても詳しく解説しています。
###(2)クライアントへの応答の返し方
次に、sendメソッドでのreturn文の書き方について説明します。sendメソッドでのreturnは、そのままクライアントへ応答として送信される内容になります。returnで返すのは、Resposeクラスのオブジェクトです。次のように、Responseクラスのクラスメソッドを使って作成します。
return Response
.ok("送信を開始します。この後、送信結果のメールが届きます。")
.build();
このようにメソッドチェーンで書けるのは、Responseクラスに次のようなクラスメソッドがあるからです。
戻り値型 | メソッド名 | 機 能 |
---|---|---|
ResponseBuilder | ok() | ステータス"OK"を応答にセットする |
ResponseBuilder | ok(obj) | ステータス"OK"とオブジェクトobjを応答にセットする |
ResponseBuilder | status(statusValue) | 任意のステータスをセットする |
これらメソッドの戻り値型は、ResponseBuilder型ですが、このクラスには次のようなメソッドがあるので、オブジェクトやステータスを含めつつ、最終的にbuild()メソッドで、Response型にして返す、という仕組みです。
戻り値型 | メソッド名 | 機 能 |
---|---|---|
ResponseBuilder | entity(object) | 応答に任意のオブジェクトを含める |
ResponseBuilder | header(name, value) | 応答に任意のヘッダとその値をセットする |
Response | build() | Responseインスタンスを作成する |
いくつか、書き方のパターンを示すと、次のようなやり方があります。
書き方 | 意 味 |
---|---|
Response.ok().build() | ステータス"OK"を返す |
Response.ok("完了").build() | ステータス"OK"と「完了」という文字列を返す |
Response.status(Status.BAD_REQUEST).build() | ステータス"BAD_REQUEST"を返す |
Response.status(Status.BAD_REQUEST).entity("パラメータの誤り").build() | "BAD_REQUEST"とメッセージを返す |
Response.ok(obj).build() | ステータス"OK"とオブジェクトobjを返す |
メソッドを組み合わせるといろいろ出来るので、柔軟な応答を作成できます。
なお、結果ステータスですが、主なものだけ示すと、次のようなものがあります。ステータスの値としては、Enum型か整数を使うことができます。
ステータス | 意 味 |
---|---|
Status.OK | 200 OK |
Status.CREATED | 201 作成 |
Status.ACCEPTED | 202 承認済 |
Status.NO_CONTENT | 204 コンテンツなし |
Status.BAD_REQUEST | 400 リクエストが正しくない |
Status.FORBIDDEN | 403 禁止されている |
Status.FOUND | 302 見つかった |
Status.NOT_FOUND | 403 見つからない |
Status.REQUEST_TIMEOUT | 408 リクエストがタイムアウトした |
Status.CONFLICT | 409 競合している |
##sendMailsメソッドの作成
では、いよいよ、実際にメールを送信するsendMailsメソッドを作成しましょう。
SendMailsメソッドは、メール送信ライブラリを使うと簡単に作成できます。ライブラリには、BCCやCCで一括送信するメソッドもありますが、各メールごとに宛名と敬称を付けるので、ここでは、個別に1通ずつ送信する必要があります。
そのようなケースに対応するために、ライブラリのEmailSenderクラスには、メールサーバーとの接続、送信、切断をそれぞれ個別に行うメソッド(connect()、xsend()、disconnect())があるので、これらを使って、次のように書きます。
@Inject EmailSender es; // EmailSenderのインスタンスを得る
....
// メールサーバーに接続
es.connect();
// すべての送信先にメール送信
List<Recipient> rs = data.getDlist(); // 宛先リストを取り出す
for(Recipient r : rs){
// メール本文に宛名と敬称を付加する処理
.....
// 送信処理
es.xsend(宛先アドレス, 表題, 本文);
}
// メールサーバーから切断
es.disconnect();
変数宣言、EmailSender es;
に付けたアノテーション、@Injectは、オブジェクトのインスタンスをnewで作らず、システムから受け取る時に使います。変数宣言に、@Injectアノテーションを付けると、システムがEmailSenderクラスのインスタンスを作成し、変数esに入れてくれるのです。
これを「(コンテキストと)依存性注入」といいます。Jakarta EEに限らず、エンタープライズJavaの世界では、「依存性注入」によりインスタンスを取得するのが普通です。実際には、引数のないコンストラクタを使って、インスタンスを生成してくれるのですが、new を書かなくてよいので、なかなか便利です。
後は、EmailSenderのメソッドを使ってメールサーバーに接続し、必要なだけメールを送信し、最後に切断する、というストーリーです。簡単ですね。ただ、これは「あらすじ」ですから、あと少し、内容を検討する必要があります。
###(1)本文に宛名と敬称を追加したい
どうということもありません。次のようにして送信前に、本文を書き換えてしまいます。
// 送信
// すべての送信先にメール送信
List<Recipient> rs = data.getDlist(); // 宛先リストを取り出す
for(Recipient r : rs){
// メール本文に宛名と敬称を付加する処理
StringBuilder sb = new StringBuilder();
sb.append(r.getName()).append(" ") // 宛名
.append(r.getMr()).append("\n") // 敬称
.append(data.getMessage()); // メール本文
// 送信処理
es.xsend(r.getEmail(), data.getTitle(), sb.toString());
}
for文を使って、宛先リストの数だけメールを送信しますが、その際、本文の前に、宛名と敬称(どちらもRecipientクラスのフィールド変数に含まれています)を追加してしまうわけです。
###(2)接続、送信、切断の実行結果を返すようにしたい
connect()、send()、disconnect()の各メソッドは、実行結果のステータスをEmailStatus型の値(列挙型)で返します。
ステータス | 意 味 |
---|---|
EmailStatus.DONE | 正常終了 |
EmailStatus.INIT_ERROR | 接続パラメータの誤り |
EmailStatus.CONNECT_ERROR | 接続できない |
EmailStatus.SEND_ERROR | メール送信に失敗した |
EmailStatus.DISCONNECT_ERROR | 切断時のエラー |
そこで、接続フェーズでエラーが起こった時は、処理を中止してステータスを返します。
また、メール送信中にエラーが起こった時(メールアドレスの形式不正が原因)は、その宛先(Recipientオブジェクト)にfalseを書き込んでおいて、処理を継続します。つまり、記録を残すわけです。
こうしておくと、後で、reply()メソッドが実行結果をメールで返す時、未送信の宛先を報告できます。
なお、切断フェースは、最後の処理なので単にステータスを返すだけで構いません。
以上から、完成したsendMailsメソッドは次のようになります。
// 平文・複数メールの送信
private EmailStatus sendMails(DataSet data){
// メールサーバーに接続
EmailStatus sts = es.connect();
if(sts!=EmailStatus.DONE) return sts; // エラーなら終了
// すべての送信先にメール送信
List<Recipient> rs = data.getDlist(); // 宛先リストを取り出す
for(Recipient r : rs){
StringBuilder sb = new StringBuilder(); // 本文を書き換える
sb.append(r.getName()).append(" ") // 宛名
.append(r.getMr()).append("\n") // 敬称
.append(data.getMessage()); // メール本文
sts = es.xsend(r.getEmail(),data.getSender(), data.getTitle(), sb.toString());
if(sts!=EmailStatus.DONE) r.setFlag(false); // 送信エラーの宛先にはfalseを付ける
}
// メールサーバーから切断
sts = es.disconnect(); // ステータスを返す
return sts;
}
ところで、メールアドレスの形式が不正だと、送信エラーとして捕捉できますが、形式が正しければ、間違いのメールアドレスでも、そのまま送信されてしまいます。後でリターンメールが返ってくるかもしれませんが、それをどうするかは、別の問題です。
一般に、メールアドレスを登録してもらう時は、本人に確認メールを送り、リターンがあれば登録という手順を踏みます。この手順に従えば、メールアドレスの誤りは発生しません。ただ、登録者がメールアドレスを廃棄・変更した場合にはどうにもなりませんので、時々、確認メールを送って調べる必要があります。
もちろん、このシステムにそのようなサービスも追加できますが、やってみませんか?
##応答を返すreplyメソッド
最後は、送信終了後に、結果をメールで通知するreply()メソッドを作成します。
reply()メソッドの引数は、送信データ(DataSet)と結果のステータス(EmailStatus)です。
// 送信結果通知メールの送信
private void reply(DataSet data, EmailStatus status){
....
es.send(data.getSender(), "送信結果のお知らせ", 本文);
}
メール送信はJ-mailライブラリのsend()メソッドを使うだけです。
xが付かないsend()メソッドは、
send(宛先, タイトル, 本文)
の形式で使う、コンビニエンスメソッドで、メールサーバーへの接続、切断も一緒にやってくれます。
###(1)通知に必要なデータを集める
送信は簡単ですが、手間なのは本文を作らなければいけないことです。
本文には、送信結果の通知、メールの件数や、送信できなかったメールアドレス、そして、送信した本文のコピーを付けておきます。そこで、最初に、次のようにして、それらのデータを取得します。
// 送信結果通知メールの送信
private void reply(DataSet data, EmailStatus status){
String result = returnMessage(status); // ①メッセージに変換
int all = data.getDlist().size(); // ②全件数
int done = doneMails(data); // ③送信件数
int errs = all - done; // ④エラー件数
String errors = errMails(data); // ⑤エラーアドレス(CSV)
....
es.send(data.getSender(), "送信結果のお知らせ", 本文);
}
では、実行内容を、順に確認しましょう。
####①String result = returnMessage(status); // メッセージに変換
returnMessageメソッドで、実行結果のステータス(status)を、メッセージ文字列に変換して返します。returnMessageメソッドは、次のように、簡単なswitch文で変換しています。
// 終了コードをメッセージに変換して返す
private String returnMessage(EmailStatus status){
switch(status){
case INIT_ERROR: return "ユーザー名・パスワードが設定されていません";
case CONNECT_ERROR: return "SMTPサーバーに接続できないため送信できませんでした";
case SEND_ERROR: return "送信中に予期しないエラーが発生したため送信できませんでした";
case DISCONNECT_ERROR: return "切断時のエラーのため送信できませんでした";
default: return "送信完了しました";
}
}
####②int all = data.getDlist().size(); // 全件数
data.getDlist()は、メンバのdlist、つまり宛先オブジェクト(Recipient)のリストです。
size()でその要素数を求めると送信要求されたメール件数が得られます。
####③int done = doneMails(data); // 送信件数
doneMailsメソッドで、実際に送信したメールの件数を求めます。
先のsendMailsメソッドで、送信できなかった宛先オブジェクト(Recipient)について、そのflagメンバをfalseにセットしておいたので、doneMailsメソッドは、それを利用します。つまり、dataから宛先オブジェクト(Recipient)のリスト(dlist)を取り出し、ストリーム処理で、flagがtrueになっているRecipientオブジェクトだけを抽出して、最後にその件数を数えます。
// 送信済件数を返す
public int doneMails(DataSet data){
int n = (int)data.getDlist().stream()
.filter(Recipient::isFlag) // flagがtrueのものだけ
.count(); // 件数を得る
return n;
}
filterメソッドは、条件に合うものだけを抽出するメソッドです。条件は、Recipient::isFlag
ですが、これは、メソッド参照という書き方です。r->r.isFlag()
というラムダ式と同じで、意味は、r.isFlag()==true
です。つまり、Flagフィールドの値がtrueなら、ということです。
ストリーム処理やラムダ式を使わずに、普通の構文でも同じことをかけますが、ストリーム処理は、簡潔に書けるのが利点です。
####④int errs = all - done; // エラー件数
全件数から送信済件数を引いて、エラー件数を求めます。
####⑤String errors = errMails(data); // エラーアドレス(CSV)
errMailsメソッドで、送信エラーになったメールアドレスをコンマ区切りの文字列として取得します。
③と同じように、ストリーム処理を使います。
// 送信エラーのアドレスをCSV文字列にして返す
public String errMails(DataSet data){
String errors = data.getDlist().stream()
.filter(r->!r.isFlag()) // flagがfalseのものだけ
.map(Recipient::getEmail) // メールアドレスだけにする
.collect(Collectors.joining(",")); // コンマで連結する
return errors;
}
filterメソッドで、今度はflagメンバがfalseであるものだけを抽出します。続くmapメソッドは変換メソッドです。ここでは、Recipientオブジェクトのストリームを、emailのストリームに変換します。Recipient::getEmail
は、r->r.getEmail()
というラムダ式と同じです。つまり、Recipientオブジェクトrから、emailを取り出してストリームに流すので、Recipientのストリームが、文字列であるemailのストリームに変わります。
最後のcollectメソッドは、高次の集約、変換を行うメソッドで、ここでは、文字列ストリームの要素を、すべてコンマで連結して、ひとつの文字列にする、という処理をおこないます。
以上のことを普通の構文で書くと倍以上のコードを書くことになるでしょう。ストリーム処理は、本当に強力ですね!
(「わかりやすいJava オブジェクト指向徹底解説
」には、ストリーム処理の詳しい解説があります)
###(2)通知メールの本文を作る
データがそろったら、あとは簡単です。StringBuilderを使って、次のように文字列を作成します。
StringBuilder sb = new StringBuilder();
sb.append(result).append("\n\n")
.append("\t受付件数 ").append(all).append("件\n")
.append("\t送信件数 ").append(done).append("件\n");
if(errs!=0){
sb.append("\n送信不能 ").append(errs).append("件\n")
.append("\t次の宛先は無効です\n\t").append(errors);
}
es.send(data.getSender(), "送信結果のお知らせ", sb.toString());
特に説明がいらないくらい単純ですね。これで、次のようなメール本文が作成できます。
###(3)完成したreplyメソッド
以上から、完成したreplyメソッドは、次のようです。
// 送信結果通知メールの送信
private void reply(DataSet data, EmailStatus status){
String result = returnMessage(status); // メッセージに変換
int all = data.getDlist().size(); // 全件数
int done = doneMails(data); // 送信件数
int errs = all - done; // エラー件数
String errors = errMails(data); // エラーアドレス(CSV)
StringBuilder sb = new StringBuilder();
sb.append(result).append("\n\n")
.append("\t受付件数 ").append(all).append("件\n")
.append("\t送信件数 ").append(done).append("件\n");
if(errs!=0){
sb.append("\n送信不能 ").append(errs).append("件\n")
.append("\t次の宛先は無効です\n\t").append(errors);
}
es.send(data.getSender(), "送信結果のお知らせ", sb.toString());
}
#2.サービスクライアントの作成 --- SenderClientクラス
今度は、クライアント側の作成です。
Java言語で作る時は、Jakarta REST Client APIを利用します。このクライアントAPIには、URIを指定してGET、POSTなどでアクセスするためのメソッドが用意されています。また、サービスからの戻り値として、Responseクラスのオブジェクトを取得できます。
###(1)クライアントクラスの自動生成
IDEとして、NetBeansを使うと、サービスクライアントは、自動生成できます。
新しくmailserviceClietnというプロジェクトを作り、何か適当な名前のクラス(ここではMultimailクラス)を作成して、次の手順を実行します。
①クラス定義の外側にカーソルを置く
②[ソース]⇒[コードを挿入]と選択する
③ダイアログが開くので[RESTクライアントを生成]を選択する
④[元のサービス]が選択されていることを確認し、[参照]を押す
⑤現在開いているプロジェクトが表示される
④RESTサービスプロジェクト(ここではmailservice)を展開し[Sender]を選択して[OK]を押す
⑤元のダイアログに戻るので、クラス名にSenderClientと入力して[OK]を押す
以上で、現在のクラスの中に、内部クラスとしてSenderClientクラスが生成されます。
###(2)クライアントクラスの修正
生成されるクラスは次のようです。一部、修正する必要がありますので、説明は後にして、修正箇所を直してしまいましょう。
public class Multimail {
static class SenderClient {
private WebTarget webTarget;
private Client client;
private static final String BASE_URI = "http://localhost:8080/mailservice/sendmail";
public SenderClient() {
client = javax.ws.rs.client.ClientBuilder.newClient();
webTarget = client.target(BASE_URI).path("cc");
}
public Response send() throws ClientErrorException {
return webTarget.path("text").request()
.post(null, Response.class);
}
public void close() {
client.close();
}
}
}
修正箇所は、次の通りです。
①send()メソッドに引数として、DataSet dataを指定する。
②postメソッドの引数を、post(Entity.entity(ds, MediaType.APPLICATION_JSON))
と書き換える
③closeメソッドに、@preDestroy というアノテーションを付ける
修正すると、次のようになります。
public class Multimail {
static class SenderClient {
private WebTarget webTarget;
private Client client;
private static final String BASE_URI = "http://localhost:8080/mailservice/sendmail";
public SenderClient() {
client = javax.ws.rs.client.ClientBuilder.newClient();
webTarget = client.target(BASE_URI).path("cc");
}
public Response send(DateSet data) throws ClientErrorException {
return webTarget.path("text").request()
.post(Entity.entity(ds, MediaType.APPLICATION_JSON));
}
@PreDestroy
public void close() {
client.close();
}
}
}
では、順に説明していきましょう。
###(3)クライアントクラスの説明
####どうして内部クラス?
まず、クラスが、内部クラス(静的内部クラス)として生成されたことです。これは、必ず内部クラスにまとめる必要があるわけではありません。実際には外側のMultimailクラス(外部クラスといいます)の中に、これらの変数やメソッドをちりばめて書いても問題ありません。ただ、内部クラスにまとめておくと、外部クラスと守備範囲をはっきり分けることができる、という利点があります。
外部クラスであるMultimailクラスの中身はこれから作成しますが、ユーザーインタフェースを担当するだけのクラスです。つまり、冒頭に見たようなウェブ画面を生成し、利用者に入力してもらいます。そして、入力されたデータは、SenderClientクラスのメソッドを使って、サービスに送信する、という役割分担です。
####クライアントクラスの機能
#####1.フィールド変数
private WebTarget webTarget; // URIを指定してサービスにアクセスする機能を持つオブジェクト
private Client client; // WebTargetを生成するためのクラス
// サービス全体を表すURI(@ApplicationPath("sendmail")で指定したURI)
private static final String BASE_URI = "http://localhost:8080/mailservice/sendmail";
WebTargetクラスを使って、サービスにアクセスしますが、WebTargerクラスのインスタンスは、Clientクラスを使って作成します。そこで、フィールド変数として、それぞれの変数を宣言しておきます。また、BASE_URIは、対象とするメール送信サービスのURIで、全体のサービス名までのURIです。
#####2.コンストラクタ
public SenderClient() {
// Clientのインスタンスを生成する
client = javax.ws.rs.client.ClientBuilder.newClient();
// (@Path("cc")で指定した)Senderクラスのサービスを表すURIで、
// WebTargetのインスタンスを生成しておく
webTarget = client.target(BASE_URI).path("cc");
}
コンストラクタでは、ClientクラスとWebTargetクラスのインスタンスを作成し、アクセスの準備をしておきます。Clientクラスのインスタンス生成は、いつもこのように書く決まりきったやり方です。コストのかかる処理なので、コンストラクタでやっておくのが普通です。
WebTargetのインスタンは、SenderクラスのURIを指すようにして生成します。"cc"に続く後の部分のパスは、実際にアクセスする時、pathメソッドで指定します。
######3.send()メソッド
public Response send(DataSet data) throws ClientErrorException {
return webTarget
.path("text") // パスを追加
.request() // アクセスする
.post(Entity.entity(data, MediaType.APPLICATION_JSON)); // POSTアクセスでdsを送信する
}
これは、自動生成なので、ウェブサービス側と同じ名前になります。おかげで対応が分かりやすくなる効果があります。
結局これは、http://localhost:8080/mailservice/sendmail/cc/text
に、POSTでアクセスするためのメソッドです。そのため、引数をDataSet型のdataと修正したのでした。dataは、POSTで送信するオブジェクトです。
POSTアクセスの手順は、①path("text")によって、末尾に"text"を追加する、②request()でアクセスし、③postでdataを送信する、という順序です。この一連のメソッド呼び出しは、自動生成されました。ただ、postメソッドの引数は、DateSet型のオブジェクトdataを送信するように、手直ししました。
サービスにオブジェクトを送る時は、Entityクラスのentityメソッドを使って、変数とMediaTypeを指定します。MediaTypsとしはこのように、APPLICATION_JSONを指定するといいでしょう。
post(Entity.entity(data, MediaType.APPLICATION_JSON))
これは、オブジェクトを送信する時の一般的な書き方です。
#####4.closeメソッド
@PreDestroy
public void close() {
client.close();
}
処理が終わったら、リソースを開放するために、Clientクラスをcloseしなければいけません。これはそのためのメソッドですが、実行し忘れても大丈夫なように、@PreDestroyアノテーションを付けておきます。このアノテーションを付けておくと、クラスのインスタンスが消去される直前に、close()メソッドが自動実行されるので安心です。
###(4)ユーザーインタフェース --- JSF
ここでは、ユーザーインタフェースはJSF(Jakarta Faces)で作成しています。つまり、MultimailクラスはJSFのクラスです。JSFのコントローラはバッキングビーンというので、Multimailクラスはバッキングビーンです。必要なアノテーションなどを付けた形は次のようです。また、作成する主な機能もコトバで書き込んでいます。
@ViewScoped
@Named
public class Multimail implements Serializable {
// ユーザーインタフェースにかかるフィールド変数の宣言
// SenderClientのインスタンスを生成
// アップロードファイルの受け取り処理(宛先リストのExcelファイルを受け取る)
// Excelファイルを読んでRecipientオブジェクトのListを作成する処理
// 入力完了後、SenderClientのpostメソッドを使って、データを送信する処理
static class SenderClient {
...
}
}
ソースコードは、プロジェクトファイルの中にあります(100行くらいです)が、長くなりすぎるので、これ以降の解説は、今回は割愛します。それでも、ここまでで、RESTfulウェブサービスとクライアントの説明は、ひとまずできたように思いますが、どうでしょう。
RESTfulウェブサービスの、さらに詳細な情報は、書籍「 わかりやすいJakarta EE
」にありますので、よろしければご覧ください。
なお、このユーザーインタフェースにかかる部分については、ファイルアップロード処理や、Excelファイルの読み取りなど、面白い内容なので、Qiitaに、別途、投稿する予定です。年明けには投稿できると思いますので、興味のある方は続けて、ご覧ください。
##3.メール送信ライブラリ --- J-mail
メール送信に使ったのは、J-mailというライブラリです。すでに10年以上に渡って改訂を繰り返していて、今年は、Jakarta EEで使えるように改訂しました。ライブラリのソースコードは、ここからダウンロードできます。また、今回作成したmailserviceプロジェクトにも含まれています。jmailというパッケージがそれです。
###◆ソースコードファイルの内容
ライブラリのファイルは、EmailSender.javaと、JmSender.javaの2つが主なものです。EmailSenderはJmSenderを利用するクラスで、公開するAPIを定義しています。JmSenderはJakarta Mail(=Java Mail)を直接操作するクラスです。また、SenderProperties.javaは、インタフェースで、SMTPサーバーアクセスに必要なデータをプロパティファイルで取得するgetProperties()メソッドだけを定義しています。
このインタフェースは、プロパティのデータを、xmlファイルからロードするか、microprofile-config.propatiesファイルを使うかなど、柔軟に選択できるようにするためです。ライブラリでは、2つの実装(ConfigToProperties.java、XmlToProperties.java)を提供しています。CDIビーンなので、beans.xmlファイルで、どちらを利用するか記述します。デフォルトではXmlToPropertiesです。
###◆J-mailライブラリのAPI
EmailSenderの公開APIです。平文メール、HTMLメールを送信できます。また、どちらのメールも、ファイル添付やCC/BCC送信ができます。さらに、今回の投稿のように、こまかな操作をしたい場合は、接続、メール送信(平文orHTML)、切断の操作を分けて実行するメソッドもあります。
####(1)戻り値型
すべてのメソッドは、実行結果を示すステータス(EmailStatus型)を返します。ただし、簡略化のためAPIの表では戻り値の記載を省略しています。
戻り値は次のような値です。
ステータス | 意 味 |
---|---|
EmailStatus.DONE | 正常終了 |
EmailStatus.INIT_ERROR | 接続パラメータの誤り |
EmailStatus.CONNECT_ERROR | 接続できない |
EmailStatus.SEND_ERROR | メール送信に失敗した |
EmailStatus.DISCONNECT_ERROR | 切断時のエラー |
####(2)メソッドの引数
引数は次のような意味です。
引数名 | 意 味 |
---|---|
String to | 宛先メールアドレス |
String subject | メールタイトル |
String body | メール本文 |
String html | メール本文になるHTMLデータ |
String type | 送信区分:"TO" "CC" "BCC" "TO"は「宛先は1人」の意味 |
String fileDir | ファイルのあるディレクトリ名 |
List<String> flist | 添付ファイル名のリスト |
・htmlは、HTML文書全体を文字列にしたものです | |
・fileDirは、送信ライブラリのあるコンピュータ上の場所でなければいけません |
####(3)接続、切断をマニュアルで行うAPI
・戻り値はすべてEmailStatus型です
・引数はすべてString型です
API | 機 能 |
---|---|
connect() | SMTPサーバーに接続する |
disconnect() | SMTPサーバーから切断する |
xsend(to, subject, body) | 1通のテキストメールを送信する |
xsendHtml(to, subject, body) | 1通のHTMLメールを送信する |
・多数のメールを送信するのに適したAPIです | |
・繰り返し送信ができるので、CC/BCC指定はしません | |
・ファイル添付機能はありません。通常はファイルのあるURLをメール本文に含めます。 |
####(4)コンビニエンスAPI(接続・切断処理が不要)
・1~数通のメールを手軽に送るためのAPIです
・戻り値はすべてEmailStatus型です
・引数はList型以外はすべてString型です
API | 機 能 |
---|---|
send(to, subject, body) | メールを送信する |
send(to, subject, body, type) | CC/BCCを指定してメールを送信する |
send(to, subject, body, fileDir, List<String> flist) | ファイル添付メールを送信する |
send(to, subject, body, fileDir, List<String> flist, String type) | CC/BCC指定を指定してファイル添付メールを送信する |
sendHtml(to, subject, html) | HTMLメールを送信する |
sendHtml(to, subject, html, type) | CC/BCCを指定してHTMLメールを送信する |
sendHtml(to, subject, html, fileDir, List<String> flist) | ファイル添付HTMLメールを送信する |
sendHtml(to, subject, html, fileDir, List<String> flist, type) | CC/BCC指定、ファイル添付HTMLメールを送信する |