LoginSignup
4

More than 1 year has passed since last update.

posted at

updated at

RESTfulなメール送信サービス+クライアントを作ってみよう

 xmax.png 
 メリークリスマス! 「Javaアドベントカレンダー2020」の最終日です。
 クリスマスらしく、楽しい(?)内容になるようにタイトルを変えました。ライブラリの紹介だけと思っていましたが、クリスマスだし、ライブラリを使って役に立つウェブサービスを作ってみよう、というわけです。
 RESTfulウェブサービスについて基本から説明しているので、この分野を知りたい人にもおススメですよ。

 なお、ライブラリの詳細は、投稿の最後にまとめましたので、気になる方は、先に、そちらを見てください。

1.メールサービスの作成 --- Senderクラス

宛名を変えて、同じメールを一斉に送ってくれるサービス

 つまりですね、

  ○○ 様
  こんにちは、・・・。

 のような、宛名と敬称付きで、後の内容は同じ、というメールを、名簿にある人達に一斉に送ってくれるサービスです(宛名と敬称がつくところがポイントです)。メール送信は、ライブラリを使うので数行ですし、送信サービスもJakarta RESTの仕組みを使うと簡単に書けるはずです。ちなみに、Jakarta RESTの開発・実行環境はJETを使うと5分で準備できますので、初めての方も是非トライしてみてください。

  (ここからJETをダウンロードできます。使い方の解説やビデオもあります。)

 一方、サービスを利用するクライアントプログラムは、ウェブ画面を作ってデータをPOSTで送るだけなので、PHPやJavascriptなどあらゆる言語で作成できます。ウェブサービスの強みのひとつです。ただ、JavaにもクライアントAPIがあるので、Javaで作っても何の不都合もありません。

  (これから解説するメール送信サービスとクライアントプログラムのプロジェクトはここからダウンロードしてください。また、プロジェクトに同梱している重要.txtファイルには、SMTPサーバーの設定について、注意が書いてあります。必ず、読んだ上で利用してください。)
 

こんな形になりました

 先に目に見える結果を見ておきましょう。サービスを利用するクライアントの画面です。

client004-S.png

 上段は、メールの内容を書く領域で、下段は名簿画面です。名簿は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")を付けます。これだけです。

ServerConfig.java
@ApplicationPath("sendmail")
    public class ServiceConfig extends Application {    
}   

 次に個別のサービスを表すパスは、別にクラス(名前は任意。Senderクラス)を作って、@Path("cc")を付ければOKです。それから、このSenderクラス内に具体的なサービス機能を作成します。

Sender.java
@Path("cc")
public class Sender {

}  

 最後に、サブパスtextですが、これは、Senderクラス内で実際に処理を担当するメソッド(sendメソッド)に、@Path("text")を付けます。また、POSTでアクセスするメソッドなので、@POSTアノテーションも付けておきます。

Sender.java
@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クラスのフィールド変数は次のようです。

DataSet.java
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クラスは次のようにしました。

Recipient.java
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メソッドが引数に受け取るデータの形式を指定します。(ざっくりした言い方ですが)このように複数の形式を指定しておくと、クライアント側でどれかに対応できます。

Sender.java
@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については何もする必要はありません)。

DataSet.java
@XmlRootElement
public class DataSet {

(2)戻り値型

 メソッドはResponse型のオブジェクトを返すのが普通です。Responseクラスは、オールインワンの便利なオブジェクトで、その中には、いろいろな応答ヘッダとその値、実行結果を表すステータス、呼び出し側に返すオブジェクトなどを含めることができます。HTTPの規約に従って送信されるので、クライアントプログラムは、それぞれの言語固有の標準的な方法でそれらを受け取ることができます。Java言語でのやり方は、後ほど説明します。

では、メールを送信しよう!

 sendメソッドの処理は次のように簡単です。

Sender.java
    @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から導入されたクラスです。クリスマスですからね、ここはカッコよく書いてみましょう。

Sender.java
    @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クラスのクラスメソッドを使って作成します。

Sender.java
        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())があるので、これらを使って、次のように書きます。

Sender.java
    @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)本文に宛名と敬称を追加したい

 どうということもありません。次のようにして送信前に、本文を書き換えてしまいます。

Sender.java
    // 送信
    // すべての送信先にメール送信
    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メソッドは次のようになります。

Sender.java
// 平文・複数メールの送信
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)です。

Sender.java
    // 送信結果通知メールの送信
    private void reply(DataSet data, EmailStatus status){
        ....
        es.send(data.getSender(), "送信結果のお知らせ", 本文);
    }

 メール送信はJ-mailライブラリのsend()メソッドを使うだけです。
 xが付かないsend()メソッドは、
  send(宛先, タイトル, 本文)
の形式で使う、コンビニエンスメソッドで、メールサーバーへの接続、切断も一緒にやってくれます。

(1)通知に必要なデータを集める

 送信は簡単ですが、手間なのは本文を作らなければいけないことです。
 本文には、送信結果の通知、メールの件数や、送信できなかったメールアドレス、そして、送信した本文のコピーを付けておきます。そこで、最初に、次のようにして、それらのデータを取得します。

Sender.java
    // 送信結果通知メールの送信
    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文で変換しています。

Sender.java
    // 終了コードをメッセージに変換して返す
    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オブジェクトだけを抽出して、最後にその件数を数えます。

Sender.java
    // 送信済件数を返す
    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メソッドで、送信エラーになったメールアドレスをコンマ区切りの文字列として取得します。
 ③と同じように、ストリーム処理を使います。

Sender.java
    // 送信エラーのアドレスを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を使って、次のように文字列を作成します。 

Sender.java
    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());

 
特に説明がいらないくらい単純ですね。これで、次のようなメール本文が作成できます。
client006.png

(3)完成したreplyメソッド

 以上から、完成したreplyメソッドは、次のようです。

Sender.java
    // 送信結果通知メールの送信
    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クラス)を作成して、次の手順を実行します。

①クラス定義の外側にカーソルを置く
  client001.png
②[ソース]⇒[コードを挿入]と選択する
③ダイアログが開くので[RESTクライアントを生成]を選択する
④[元のサービス]が選択されていることを確認し、[参照]を押す
⑤現在開いているプロジェクトが表示される
④RESTサービスプロジェクト(ここではmailservice)を展開し[Sender]を選択して[OK]を押す
 client002.png
⑤元のダイアログに戻るので、クラス名にSenderClientと入力して[OK]を押す
 client003.png

 以上で、現在のクラスの中に、内部クラスとしてSenderClientクラスが生成されます。

(2)クライアントクラスの修正

 生成されるクラスは次のようです。一部、修正する必要がありますので、説明は後にして、修正箇所を直してしまいましょう。

Multimail.java
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 というアノテーションを付ける

 修正すると、次のようになります。

Multimail.java
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.フィールド変数
SenderClient.java
    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.コンストラクタ
SenderClient.java
    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()メソッド
SenderClient.java
    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メソッド
SenderClient.java
    @PreDestroy
    public void close() {
        client.close();
    }

 処理が終わったら、リソースを開放するために、Clientクラスをcloseしなければいけません。これはそのためのメソッドですが、実行し忘れても大丈夫なように、@PreDestroyアノテーションを付けておきます。このアノテーションを付けておくと、クラスのインスタンスが消去される直前に、close()メソッドが自動実行されるので安心です。
 

(4)ユーザーインタフェース --- JSF

 ここでは、ユーザーインタフェースはJSF(Jakarta Faces)で作成しています。つまり、MultimailクラスはJSFのクラスです。JSFのコントローラはバッキングビーンというので、Multimailクラスはバッキングビーンです。必要なアノテーションなどを付けた形は次のようです。また、作成する主な機能もコトバで書き込んでいます。

SenderClient.java
@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メールを送信する

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4