Enviar SMS mediante su app de Spring Boot

January 13, 2020
Redactado por

Enviar SMS mediante su app de Spring Boot

En este artículo, aprenderá a usar la API de WebSocket con Spring Boot y, al final, podrá crear una aplicación simple de entrega de estados.

WebSocket es un protocolo de comunicación que permite establecer un canal de comunicación bidireccional entre un servidor y su cliente. La mayoría de los navegadores que se utilizan comúnmente en la actualidad son compatibles con WebSockets. 

Crear una aplicación

En primer lugar, debe configurar su cuenta de Twilio y un número de teléfono adecuado.

A continuación, se indican los pasos para generar un proyecto con Spring Initializr: 

  1. Vaya a http://start.spring.io/.
  2. Ingrese el valor del artefacto como websocket-callback.
  3. Agregue WebSocket en la sección Dependencies (Dependencias).
  4. Haga clic en Generate Project (Generar proyecto) para descargar el proyecto.
  5. Extraiga el archivo zip descargado.
  6. Nota: Necesitará tener instalado Java 8 o posterior y descargar el JDK desde aquí.

Configurar de WebSocket

El primer paso es configurar el punto final de WebSocket y un agente de mensajería. Cree una clase nueva denominada WebSoocketConfig dentro del paquete com.twilio.websocketcallback con el siguiente contenido:

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 se utiliza para habilitar nuestro servidor de WebSocket. Incluimos la interfaz de WebSocketMessageBrokerConfigurer y ofrecemos una implementación para algunos de sus métodos para configurar la conexión de WebSocket.

En el primer método configureMessageBroker, estamos configurando un agente de mensajería que se utilizará para enrutar los mensajes de un cliente a otro.

La línea n.º 15 define que los mensajes cuyo destino comienza con /topic se deben dirigir al agente de mensajería. El agente de mensajería transmite mensajes a todos los clientes conectados que están suscritos a un tema en particular. 

La línea n.º 16 define los mensajes cuyo destino comienza con /app. Después de procesar el mensaje, el controlador lo enviará al canal del agente.

En el segundo método registerStompEndpoints, registramos un punto final de WebSocket que los clientes usan para conectarse a nuestro servidor de WebSocket.

Observe el uso de withSockJS() con la configuración del punto final. SockJS se utiliza para activar segundas opciones para navegadores que no son compatibles con WebSocket.

La palabra STOMP en el nombre del método muestra que es una derivación de la implementación STOMP del marco de Spring. STOMP significa Simple Text Oriented Messaging Protocol (Protocolo simple de mensajería orientado al texto). Es un protocolo de mensajería que define el formato y las reglas para el intercambio de datos.

A continuación, se muestra la representación gráfica de los WebSockets, que facilita el canal de comunicación bidireccional completo:

Diagrama de cómo fluirán los datos en la app WebSocket

Desarrollar con Gradle

Cree un archivo build.gradle con las bibliotecas de dependencias de Twilio y WebSockets asociadas. 

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'
    }
}

Crear una clase de representación de recursos

Para modelar la clase de SMS que transporta los mensajes, puede crear un objeto Java simple (POJO) con las prioridades to y message, y los métodos correspondientes.

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 + '\'' +
                '}';
    }
}

Crear un controlador de gestión de mensajes

En el enfoque de Spring con respecto a cómo trabajar con la mensajería de STOMP, los mensajes de STOMP se pueden dirigir a clases @Controller. Por ejemplo, SMSController está asignado para gestionar los mensajes al destino /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());
    }
}

Este controlador es conciso y preciso, pero hay muchos procesos que se ejecutan de forma interna. Vamos a revisarlos paso a paso.

La anotación @RequestMapping garantiza que, si se envía un mensaje al destino /sms mediante el método POST, el content_type aceptado por el consumidor se establece en la línea consumes = MediaType.APPLICATION_JSON_VALUE.

produces = MediaType.APPLICATION_JSON_VALUE es el valor que puede producir el gestor asignado. produces consta de uno o más tipos de medios, uno de los cuales se debe elegir mediante la negociación de contenido con los tipos de medios “aceptables” de la solicitud. Normalmente, estos se extraen del encabezado "Accept", pero pueden derivarse de los parámetros de consulta o en otro lugar. 

La carga útil del mensaje está vinculada a un objeto @RequestBody SMS que se pasa a SMSSubmit().

La transmisión a todos los suscriptores a /topic/sms, como se especifica en el método webSocket.convertAndSend(). Este método se utiliza para enviar mensajes a clientes conectados desde cualquier parte de la aplicación. Cualquier componente de la aplicación puede enviar mensajes a "brokerChannel" y es realizado por la inyección de SimpMessagingTemplate para enviar mensajes.

Una vez que sereciben los campos message y to de la interfaz de usuario, estos valores se analizan y envían al servicio de SMS que se llama método send. Esto enviará mensajes de SMS salientes desde su número de teléfono de Twilio al número de teléfono compatible con texto que ingresó en la interfaz de usuario. Twilio envía un SMS en lugar del número de teléfono real con cable. Con el SMS programable, podemos desarrollar una lógica para enviar SMS con un cuerpo del mensaje y el número de teléfono de destino, y permitir que Twilio haga su magia.

Del mismo modo, el segundo método smsCallback es un método de gestor de devoluciones de llamadas que recibe la devolución de la llamada desde el punto final de Twilio.  Consume un encabezado de content-type de solicitud de tipo MediaType.APPLICATION_FORM_URLENCODED_VALUE y produce un mediatype negociable de tipo MediaType.APPLICATION_JSON_VALUE. Luego, nuestro motor js de front-end procesa este tipo de medios. Twilio envía una devolución de llamada en valores de formulario URL_ENCODED, por lo tanto, deben formatearse en un tipo que la aplicación pueda consumir con facilidad. 

Crear un servicio de mensajería

Un componente es una unidad de software que se puede reemplazar y actualizar de manera independiente. 

Las arquitecturas de microservicio usarán bibliotecas, pero su manera principal de hacer los componentes de su software es desglosando los servicios. Definimos las bibliotecas como componentes que se vinculan a un programa y a las que se llama mediante llamadas de funciones en la memoria, y los servicios como componentes fuera del proceso que se comunican con un mecanismo, como una solicitud de servicio web o una llamada de procedimiento remoto. Una de las principales razones para utilizar los servicios como componentes (en lugar de bibliotecas) es que los servicios se pueden implementar de manera independiente. En la aplicación de demostración, tenemos un servicio para comunicarnos directamente con los puntos finales de Twilio para enviar o recibir información de devolución de llamada.

Twilio le sugiere que nunca guarde sus credenciales en su entorno de desarrollo o proyecto. En su lugar, puede guardar estos valores y recuperarlos a través del archivo ./bash_profile (entorno) en su máquina de desarrollo u otro lugar adecuado, según su sistema operativo. Spring puede extraer datos mediante una configuración externa

Contamos con un par de publicaciones de blog sobre cómo vincular las variables del entorno y cómo guardar las credenciales.

A continuación, se muestra el código de esta implementación:

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) {
    }
}

Crear un cliente del navegador

Una vez que se establecen y conectan todos los componentes del servidor, la interfaz de usuario se puede implementar con el cliente JS y las páginas HTML. Se recomienda colocar estos archivos en src/main/resources/static.

Cree un archivo index.html que tenga este aspecto:

<!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>

Este archivo HTML importa las bibliotecas javascript de SockJS y STOMP que se utilizarán para comunicarse con nuestro servidor mediante STOMP a través de WebSocket. Aquí también estamos importando aquí un archivo app.js que contiene la lógica de nuestra aplicación de cliente.

Vamos a crear ese archivo:

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();

Crear un JAR ejecutable

Si utiliza Gradle, puede ejecutar la aplicación mediante ./gradlew bootRun

Probar el servicio

Ahora que el servicio se está ejecutando, dirija su navegador a http://localhost:8080 y haga clic en el botón “Send” (Enviar).

Cuando abra una conexión, se le pedirá el número de teléfono de destino y el mensaje. Ingrese la información y haga clic en “Send” (Enviar). Los datos se envían al servidor como un mensaje JSON mediante STOMP. Después de un retraso simulado de un segundo, el servidor envía un mensaje de respuesta con la información del estado de devolución de llamada recibida desde Twilio. 

Diagrama de secuencia de flujo entre cliente, controlador, servicio y Twilio

Diagrama de secuencia web de la implementación completa

Pantalla en el navegador con ejemplo en ejecución de inspector de código

Resultado final de la implementación

Twilio con Spring Boot y WebSockets

Si todas las pruebas funcionaron, entonces ya tiene una aplicación SMS diseñada con Spring Boot y WebSockets. Si desea trabajar con mi código, puede encontrar mi almacén de datos aquí.

Avíseme si lo hace bien; ¡estamos ansiosos por ver lo que es capaz de crear!

Referencias adicionales:

Este artículo fue traducido del original "Send SMS in Your Spring Boot App". Mientras estamos en nuestros procesos de traducción, nos encantaría recibir sus comentarios en help@twilio.com - las contribuciones valiosas pueden generar regalos de Twilio.