Spring BootアプリケーションでSMSを送信する方法

March 22, 2022
レビュー担当者

Spring BootアプリケーションでSMSを送信する方法

この記事はPooja Srinathこちらで公開した記事(英語)を日本語化したものです。

本稿では、Spring BootでWebSocket APIを使用し、シンプルなSMS送信アプリケーションを構築する方法について説明します。

WebSocketは、サーバーとクライアント間の双方向通信チャネルを確立する通信プロトコルです。WebSocketは、現在一般的に使用されているブラウザのほとんどにおいてサポートされています。

プロジェクトの設定

まず、Twilioアカウントの登録と電話番号を取得します。

Twilioのアカウントを登録します。Twilioホームページをブラウザで開き、[今すぐ無料サインアップ]ボタンをクリックするか、Twilioアカウントの作成リンクからサインアップします。このリンクを使用するとアカウントのアップグレード時に$10(米国ドル)相当分のクレジットが追加で付与されます。

次に、SMSの送信に使用する電話番号を購入します。

TwilioコンソールのBuy a Numberにアクセスします。

Countryで、電話番号が属する国を選択します。現在、TwilioではSMS送信機能付きの日本の電話番号は販売していませんので、米国等を選択してください。

電話番号購入画面

各国の電話番号で使用できる機能について詳しくは、TwilioヘルプセンターのTwilioの国際電話番号の利用と機能(英語)を参照してください。

CapabilitiesSMSがチェックされていることを確認してください。

購入した電話番号をコピーし、記録しておきます。

Spring Initializrを使いプロジェクトを生成します。手順は、次のとおりです。

  1. http://start.spring.io/にアクセスします。
  2. [Artifact]の値に「websocket-callback」と入力します。
  3. Dependencies]セクションに「Websocket」を追加します。
  4. Generate]をクリックし、プロジェクトをダウンロードします。
  5. ダウンロードしたZIPファイルを展開します。

Java 8以降をインストールし、JDKをこちらからダウンロードしてください。

WebSocketの設定

まずは、WebSocketエンドポイントとメッセージブローカーを設定します。WebSocketConfigクラスをcom.twilio.websocketcallbackパッケージ内に作成し、以下の内容を入力します。

package com.example.websocketcallback;
  
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/gs-guide-websocket").withSockJS();
    }

}

@EnableWebSocketMessageBrokerは、WebSocketサーバーの有効化に使用します。WebSocketMessageBrokerConfigurerインタフェースを組み込み、WebSocket接続を設定するメソッドを指定します。

最初のメソッドのconfigureMessageBrokerでは、クライアント間のメッセージルーティングに使用するメッセージブローカーを設定しています。

15行目では、宛先が/topicで始まるメッセージをメッセージブローカーにルーティングするよう定義しています。メッセージブローカーは、特定のトピックを購読しているすべての接続クライアントにメッセージを配信します。

16行目では、宛先が/appで始まるメッセージを定義しています。メッセージの処理後、コントローラーはメッセージをブローカーチャネルに送信します。

2番目のメソッドregisterStompEndpointsでは、クライアントがWebSocketサーバーへの接続に使用するWebSocketエンドポイントを登録しています。

エンドポイント設定にwithSockJS()を使用しています。SockJSは、WebSocketをサポートしていないブラウザのフォールバックオプションを有効化するのに使用します。

メソッド名の「Stompは、SpringフレームワークのSTOMP実装が由来です。STOMPは、Simple Text Oriented Messaging Protocolの頭字語です。これは、データ交換の形式とルールを定義するメッセージングプロトコルです。

以下の図は、全二重通信チャネルを活用したWebSocketを表したものです。

アーキテクチャ説明
 

Gradleを使い構築する

build.gradleファイルを作成し、以下のようにTwilioとWebSocketの依存ライブラリを含めます。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation group: "com.twilio.sdk", name: "twilio", version : "7.47.2"
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

リソース表現クラスを作成する

メッセージ送信SMSクラスをモデル化するには、tomessageのプロパティと、関連するメソッドを持つPlain Old Java Object(POJO)を作成します。

package com.twilio.websocketcallback.domain;
  
public class SMS {
    private String to;
    private String message;

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }


    @Override
    public String toString() {
        return "SMS{" +
                "to='" + to + '\'' +
                ", message='" + message + '\'' +
                '}';
    }
}

メッセージ処理コントローラーを作成する

STOMPメッセージングを使用するSpringのアプローチでは、STOMPメッセージを@Controllerクラスにルーティングできます。以下のようにSMSControllerをマッピングして/smsへのメッセージを処理します。

package com.example.websocketcallback;
  
import com.twilio.exception.ApiException;
import com.twilio.websocketcallback.domain.SMS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@RestController
public class SMSController {

    @Autowired
    SMSService service;

    @Autowired
    private SimpMessagingTemplate webSocket;

    private final String  TOPIC_DESTINATION = "/topic/sms";

    @RequestMapping(value = "/sms", method = RequestMethod.POST,
            consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public void smsSubmit(@RequestBody SMS sms) {
        try{
            service.send(sms);
        }
        catch(ApiException e){

            webSocket.convertAndSend(TOPIC_DESTINATION, getTimeStamp() + ": Error sending the SMS: "+e.getMessage());
            throw e;
        }
        webSocket.convertAndSend(TOPIC_DESTINATION, getTimeStamp() + ": SMS has been sent!: "+sms.getTo());

    }

    @RequestMapping(value = "/smscallback", method = RequestMethod.POST,
            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public void smsCallback(@RequestBody MultiValueMap<String, String> map) {
        service.receive(map);
        webSocket.convertAndSend(TOPIC_DESTINATION, getTimeStamp() + ": Twilio has made a callback request! Here are the contents: "+map.toString());
    }

    private String getTimeStamp() {
        return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now());
    }
}

このコントローラーは簡潔ですが、内部で実行されるプロセスが多数あります。順に詳しく見てみましょう。

@RequestMappingアノテーションは、メッセージがPOSTメソッドを介して/smsに送信されるようにします。コンシューマーが受け入れるcontent_typeは、consumes = MediaType.APPLICATION_JSON_VALUEに設定されます。

produces = MediaType.APPLICATION_JSON_VALUEは、マッピングされたハンドラーが生成する値です。producesは、1つ以上のメディアタイプで構成されます。そのうちの1つは、リクエストの「受け入れ可能な」メディアタイプに対するコンテンツ交渉を介して選択する必要があります。通常、メディアタイプはAcceptヘッダーから抽出されますが、クエリパラメーターや別の場所から抽出される場合もあります。

メッセージのペイロードは、@RequestBody SMSオブジェクトにバインドされ、SMSSubmit()に渡されます。

/topic/smsの購読者全員への配信は、webSocket.convertAndSend()メソッドにて指定しています。このメソッドは、アプリケーションからメッセージを、接続されたクライアントに送信するために使用されます。どのアプリケーションコンポーネントもメッセージを「brokerChannel」に送信できます。メッセージの送信は、SimpMessagingTemplate注入により行われます。

messageフィールドとtoフィールドをUIから受け取ると、フィールド値が解析され、sendメソッドで定義するSMSサービスに送信されます。Twilio電話番号からUIに入力した電話番号に、SMSメッセージが送信されます。

smsCallbackメソッドは、Twilioエンドポイントからのコールバックを受信するコールバックハンドラーメソッドです。このメソッドは、MediaType.APPLICATION_FORM_URLENCODED_VALUEタイプのリクエストcontent-typeヘッダーを利用し、MediaType.APPLICATION_JSON_VALUEタイプのネゴシエーション可能なmediatypeを生成します。このmediatypeを、フロントエンドのJavaScriptエンジンにより処理します。Twilioは、URL_ENCODEDフォーム値でコールバックを送信するため、アプリケーションで簡単に利用できるタイプの形式にしておく必要があります。

メッセージングサービスを作成する

コンポーネントは、単独で交換やアップグレードができるソフトウェアの単位です。

マイクロサービスアーキテクチャでは、ライブラリを使用しますが、ソフトウェアをコンポーネント化する主な方法は、サービスに分解することです。ライブラリはプログラムに関連付けられ、メモリ内の関数呼び出しで呼び出されるコンポーネントとして定義されます。また、サービスはWebサービスリクエストやリモートプロシージャコールなどの方法で通信するプロセス外のコンポーネントとして定義されます。サービスを(ライブラリではなく)コンポーネントとして使用する主な理由は、サービスが単独で展開できるからです。本稿で作成するアプリケーションでは、Twilioエンドポイントと直接通信するサービスを使用し、コールバック情報を送受信しています。

Twilioは、認証情報を開発環境やプロジェクト内に保存しないことを推奨しています。代わりに、開発マシンや、オペレーティングシステムに応じた別の場所にある./bash_profile(環境)ファイルに認証情報を保存し、そこから取り出すようにしてください。Springでは、外部化設定を介してデータを抽出できます。

環境変数と認証情報を関連付ける方法については、以下のドキュメントを参照してください。

Store Your Twilio Credentials Securely (英語)

以下のコードで、メッセージングサービスを作成します。

package com.example. websocketcallback;
import com.twilio. websocketcallback.domain.SMS;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import com.twilio.Twilio;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;

import java.net.URI;

@Component
public class SMSService {

    @Value("#{systemEnvironment['TWILIO_ACCOUNT_SID']}")
    private String ACCOUNT_SID;

    @Value("#{systemEnvironment['TWILIO_AUTH_TOKEN']}")
    private String AUTH_TOKEN;

    @Value("#{systemEnvironment['TWILIO_PHONE_NUMBER']}")
    private String FROM_NUMBER;

    public void send(SMS sms) {
        Twilio.init(ACCOUNT_SID, AUTH_TOKEN);

        Message message = Message.creator(new PhoneNumber(sms.getTo()), new PhoneNumber(FROM_NUMBER), sms.getMessage())
                .create();
        System.out.println("here is my id:"+message.getSid());// Unique resource ID created to manage this transaction

    }

    public void receive(MultiValueMap<String, String> smscallback) {
    }
}

ブラウザクライアントを作成する

すべてのサーバーコンポーネントの構築と接続が完了すると、JavaScriptクライアントとHTMLページを使用してUIを実装できます。src/main/resources/staticに、次のようなindex.htmlファイルを作成します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello WebSocket</title>

    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
    <link href="/main.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
    <script src="/app.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
        <div class="col-md-6">

            <form class="form-inline" id ="smsForm"  method="post">
                <div class="form-group">
                <p>To Number: <input type="text"  id="to" /></p>
                    <br/><p>Message: <textarea  id="message" > </textarea></p>
                </div>
                <p> <button id="send" class="btn btn-primary" type="submit">Send</button></p>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>Message Status dashboard</th>
                </tr>
                </thead>
                <tbody id="messages">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

このHTMLファイルでは、SockJSSTOMPのJavaScriptライブラリをインポートします。これらのライブラリは、WebSocket経由でSTOMPを使用したサーバーとの通信に使用されます。ここでは、クライアントアプリケーションのロジックが含まれたapp.jsファイルもインポートしています。

そのファイルを作成しましょう。

var stompClient = null;
  
function setConnected(connected) {

    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#messages").html("");
}

function connect() {
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/sms', function (sms) {
            showData(sms.body);
        });
    });
}

function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

function showData(message) {
    $("#messages").append("<tr><td>" + message + "</td></tr>");
}

function processForm(e) {
    if (e.preventDefault) e.preventDefault();
    var form = document.getElementById('smsForm');
    var data = {};
    for (var i = 0, ii = form.length; i < ii -1; ++i) {
        var input = form[i];
        if (input.id) {
            data[input.id] = input.value;
        }
    }
    // construct an HTTP request
    var xhr = new XMLHttpRequest();
    xhr.open("post", "/sms", true);
    xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');

    // send the collected data as JSON
    xhr.send(JSON.stringify(data));

    return false;
}

$(function () {

    let form = document.getElementById('smsForm');
    if (form.attachEvent) {
        form.attachEvent("submit", processForm);

    } else {
        form.addEventListener("submit", processForm);
    }

        // collect the form data while iterating over the inputs

    });

    connect();

Gradleを使用している場合は、./gradlew bootRunを使用し、アプリケーションを実行できます。

サービスをテストする

実行中のサービスをテストします。ブラウザでhttp://localhost:8080を開き、[Send]ボタンをクリックします。

接続が開始されると、宛先の電話番号とメッセージを入力するよう要求されます。情報を入力し、[Send]をクリックします。データは、STOMPを介してJSONとしてサーバーに送信されます。1秒間の遅延をシミュレーションした後、サーバーはTwilioから受信したコールバックステータス情報を含むメッセージを送り返します。

アーキテクチャ説明

完成画面

Spring BootとWebSocketでTwilioアプリケーションを構築する

すべてのテストが正常に完了すると、Spring BootとWebSocketで構築したSMSアプリケーションが完成します。本稿でご紹介したコードの全体を確認したい場合は、GitHubリポジトリを確認してください。

ぜひ、ご感想をお寄せください。コミュニケーションの未来を構築しましょう。

その他の参考資料(すべて英語):

Pooja Srinathは、Twilioのシニアソリューションエンジニアです。新しいことを学び、新進気鋭の開発者の助けとなる便利なアプリを構築することに力を入れています。ご不明な点は、psrinath@twilio.comまでお問い合わせください。