Envie SMS do seu aplicativo Spring Boot

January 13, 2020
Escrito por

Envie SMS do seu aplicativo Spring Boot

Neste artigo, você aprenderá a usar a API WebSocket com o Spring Boot e, no final, a criar um aplicativo de entrega de status simples.

WebSocket é um protocolo de comunicação que permite estabelecer um canal de comunicação bidirecional entre um servidor e o cliente. Os WebSockets são compatíveis com a maioria dos navegadores usados atualmente. 

Criar um aplicativo

Primeiro, você precisa configurar sua conta da Twilio e um número de telefone apropriado.

Estas são as etapas para gerar um projeto usando o Spring Initializr: 

  1. Acesse http://start.spring.io/.
  2. Digite o valor do Artefato como websocket-callback.
  3. Adicione WebSocket na seção de dependências.
  4. Clique em Generate Project (Gerar projeto) para baixar o projeto.
  5. Extraia o arquivo zip baixado.
  6. Nota: você precisará do Java 8 ou posterior instalado e precisará baixar o JDK aqui.

Configuração do WebSocket

A primeira etapa é configurar o endpoint do WebSocket e um agente de mensagens. Crie uma nova classe chamada WebSocketConfig dentro do pacote com.twilio.websocketcallback com o seguinte conteúdo:

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 é usado para habilitar nosso servidor WebSocket. Incluímos a interface WebSocketMessageBrokerConfigurer e fornecemos uma implementação para alguns de seus métodos para configurar a conexão WebSocket.

No primeiro método configureMessageBroker, estamos configurando um agente de mensagens que será usado para rotear mensagens de um cliente para outro.

A linha 15 define que as mensagens cujo destino começa com /topic devem ser roteadas para o agente de mensagens. O agente de mensagens transmite mensagens para todos os clientes conectados que estão inscritos em um tópico específico. 

A linha 16 define as mensagens cujo destino começa com /app. Depois de processar a mensagem, o controlador a enviará para o canal do agente.

No segundo método registerStompEndpoints, registramos um endpoint WebSocket que os clientes usam para se conectar ao nosso servidor WebSocket.

Observe o uso de withSockJS() com a configuração de endpoint. O SockJS é usado para ativar opções de fallback para navegadores que não são compatíveis com o WebSocket.

A palavra STOMP no nome do método mostra que é uma derivação da implementação STOMP da estrutura do Spring. STOP significa Simple Text Oriented Messaging Protocol (Protocolo simples de mensagens orientadas por texto). É um protocolo de mensagens que define o formato e as regras para a troca de dados.

Aqui está a representação gráfica dos WebSockets que facilita o canal de comunicação full duplex:

Diagrama de como os dados fluirão no app WebSocket

Compilar com o sistema Gradle

Crie um arquivo build.gradle com as bibliotecas de dependência Twilio e WebSockets associadas. 


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

Criar uma classe de representação de recurso

Para modelar a classe Message carrying SMS, você pode criar um POJO (Plain Old Java Object, objeto Java antigo simples) com as propriedades to e message e os métodos correspondentes.

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

Criar um controlador de gerenciamento de mensagens

Na abordagem do Spring para trabalhar com mensagens STOMP, as mensagens STOMP podem ser roteadas para classes @Controller. Por exemplo, SMSController é mapeado para lidar com mensagens para o 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());
    }
}

Esse controlador é conciso e preciso, mas há muitos processos sendo executados internamente. Vamos revisá-los passo a passo.

A anotação @RequestMapping garante isso, se uma mensagem for enviada ao destino /sms por meio do método POST. O content_type aceito pelo consumidor é definido na linha consumes = MediaType.APPLICATION_JSON_VALUE.

produces = MediaType.APPLICATION_JSON_VALU é o valor que pode ser produzido pelo manipulador mapeado. produces consiste em um ou mais tipos de mídia, um dos quais deve ser escolhido por meio de negociação de conteúdo em relação aos tipos de mídia "aceitáveis" da solicitação. Normalmente, eles são extraídos do cabeçalho "Accept" (Aceitar), mas podem ser derivados de parâmetros de consulta ou de outro lugar. 

O payload da mensagem é vinculado a um objeto @RequestBody SMS, que é passado para SMSSubmit().

A transmissão para todos os assinantes para /topic/sms conforme especificado no método webSocket.convertAndSend(). Esse método é usado para enviar mensagens a clientes conectados de qualquer parte do aplicativo. Qualquer componente do aplicativo pode enviar mensagens para o "brokerChannel" e é feito pela injeção de SimpMessagingTemplate para enviar mensagens.

Depois que os campos message e to são recebidos da UI, esses valores são analisados e enviados ao serviço SMS chamado de método send. Isso enviará mensagens SMS recebidas do número de telefone da Twilio no número de telefone habilitado para texto inserido na UI. A Twilio envia SMS em nome do número de telefone fixo real. Com o Programmable SMS, podemos criar uma lógica de envio de SMS com uma mensagem e o número de telefone de destino e deixar a Twilio agir.

Da mesma forma, o segundo método smsCallback é o do manipulador de retorno de chamada que recebe o retorno de chamada do endpoint da Twilio.  Ele consome um cabeçalho de solicitação content-type do tipo MediaType.APPLICATION_FORM_URLENCODED_VALUE e produz um mediatype do tipo MediaType.APPLICATION_JSON_VALUE negociável. Depois, esse mediatype é processado pelo nosso mecanismo front-end js. A Twilio envia um retorno de chamada em valores de formulário URL_ENCODED, assim eles precisam ser formatados em um tipo que seja facilmente consumível pelo seu aplicativo. 

Criar um serviço de mensagens

Um componente é uma unidade de software que pode ser substituída e atualizada de modo independente. 

As arquiteturas de microsserviço usarão bibliotecas, mas sua principal maneira de componentizar seu software é dividindo os serviços. Definimos bibliotecas como componentes que são vinculados a um programa e chamados usando chamadas de função na memória e serviços como componentes fora do processo que se comunicam com um mecanismo, como uma solicitação de serviço da Web, ou chamada de procedimento remoto. Um dos principais motivos para usar serviços como componentes (em vez de bibliotecas) é que os serviços são implantados de modo independente. No aplicativo de demonstração, temos um serviço para nos comunicarmos diretamente com os endpoints da Twilio e enviar ou receber informações de retorno de chamada.

A Twilio sugere que você nunca salve suas credenciais no ambiente de desenvolvimento ou projeto. Em vez disso, você pode salvar esses valores e recuperá-los por meio do arquivo ./bash_profile na sua máquina de desenvolvimento ou em outro local adequado, dependendo do sistema operacional. O Spring pode extrair dados por meio da configuração externalizada

Temos alguns posts de blog sobre como vincular as variáveis de ambiente e como armazenar credenciais.

Abaixo está o código desta implementação:


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

Criar um cliente de navegador

Depois que todos os componentes do servidor forem estabelecidos e conectados, a IU poderá ser implementada com o cliente JS e páginas HTML. Recomendamos colocar esses arquivos em src/main/resources/static.

Crie um arquivo index.html semelhante a este:

<!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 arquivo HTML importa as bibliotecas JavaScript SockJS e STOMP que serão usadas para fazer a comunicação com nosso servidor usando STOMP via WebSocket. Também estamos importando aqui um arquivo app.js, que contém a lógica de nosso aplicativo cliente.

Vamos criar esse arquivo:

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

Compile um JAR executável

Se você usar o Gradle, poderá executar o aplicativo usando ./gradleew bootRun

Testar o serviço

Agora que o serviço está em execução, acesse http://localhost:8080 pelo seu navegador e clique no botão "Send" (Enviar).

Ao abrir uma conexão, é solicitado que você forneça o número de destino e a mensagem. Insira as informações e clique em "Send" (Enviar). Os dados são enviados ao servidor como uma mensagem JSON via STOMP. Após um atraso simulado de um segundo, o servidor envia uma mensagem de volta com as informações de status de retorno de chamada recebidas da Twilio. 

Diagrama de sequência da Web da implementação completa

Diagrama de sequência da Web da implementação completa

Imagem com resultado final da implementação

Resultado final da implementação

Twilio com Spring Boot e WebSockets

Se todos os testes foram bem-sucedidos, agora você tem um aplicativo SMS criado com Spring Boot e WebSockets. Se você quiser trabalhar com meu código, meu repositório está disponível aqui.

Avise-me se você conseguir fazê-lo funcionar. Estamos ansiosos para ver o que você vai criar!

Referências adicionais:

Este artigo foi traduzido do original "Send SMS in Your Spring Boot App". Enquanto melhoramos nossos processos de tradução, adoraríamos receber seus comentários em help@twilio.com - contribuições valiosas podem render brindes da Twilio.