1. 概述

WhatsApp是全球领先的即时通讯平台,已成为企业与用户连接的重要工具。通过WhatsApp进行沟通,我们可以提升客户参与度、提供高效支持,并与用户建立更紧密的关系。

本教程将探讨如何在Spring Boot应用中通过Twilio发送WhatsApp消息。我们将逐步完成必要配置,实现消息发送功能并处理用户回复。

2. 配置Twilio

**要跟随本教程,首先需要注册Twilio账号和WhatsApp商业账号(WABA)**。

需要通过创建WhatsApp发送者将这两个账号关联起来。Twilio提供了详细的配置教程指导完成此过程。

成功设置WhatsApp发送者后,即可开始向用户发送消息并接收用户回复。

3. 项目设置

使用Twilio发送WhatsApp消息前,需要添加SDK依赖并正确配置应用。

3.1. 依赖项

首先在项目的pom.xml文件中添加Twilio SDK依赖

<dependency>
    <groupId>com.twilio.sdk</groupId>
    <artifactId>twilio</artifactId>
    <version>10.4.1</version>
</dependency>

3.2. 定义Twilio配置属性

要与Twilio服务交互并发送WhatsApp消息,需要配置账号SID和认证令牌用于API请求认证。还需要消息服务SID来指定使用哪个消息服务(通过已启用WhatsApp的Twilio电话号码)发送消息。

我们将这些属性存储在项目的application.yaml文件中,并使用@ConfigurationProperties将值映射到POJO,服务层在与Twilio交互时引用该类:

@Validated
@ConfigurationProperties(prefix = "com.baeldung.twilio")
class TwilioConfigurationProperties {

    @NotBlank
    @Pattern(regexp = "^AC[0-9a-fA-F]{32}$")
    private String accountSid;

    @NotBlank
    private String authToken;

    @NotBlank
    @Pattern(regexp = "^MG[0-9a-fA-F]{32}$")
    private String messagingSid;

    // 标准setter和getter方法

}

还添加了验证注解确保所有必需属性正确配置。如果任何验证失败,将导致Spring ApplicationContext启动失败。这使我们符合快速失败原则

下面是application.yaml文件的片段,定义了将自动映射到TwilioConfigurationProperties类的必需属性:

com:
  baeldung:
    twilio:
      account-sid: ${TWILIO_ACCOUNT_SID}
      auth-token: ${TWILIO_AUTH_TOKEN}
      messaging-sid: ${TWILIO_MESSAGING_SID}

这种设置允许我们将Twilio属性外部化,并在应用中轻松访问它们

3.3. 启动时初始化Twilio

要成功调用SDK暴露的方法,需要在启动时初始化一次。为此,我们创建实现ApplicationRunner接口的TwilioInitializer类:

@Component
@EnableConfigurationProperties(TwilioConfigurationProperties.class)
class TwilioInitializer implements ApplicationRunner {

    private final TwilioConfigurationProperties twilioConfigurationProperties;

    // 标准构造函数

    @Override
    public void run(ApplicationArguments args) {
        String accountSid = twilioConfigurationProperties.getAccountSid();
        String authToken = twilioConfigurationProperties.getAuthToken();
        Twilio.init(accountSid, authToken);
    }

}

使用构造函数注入注入前面创建的TwilioConfigurationProperties实例。然后在*run()*方法中使用配置的账号SID和认证令牌初始化Twilio SDK。

这确保应用启动时Twilio已准备就绪。这种方法比每次需要发送消息时在服务层初始化Twilio客户端更优

4. 发送WhatsApp消息

现在已定义属性,创建WhatsAppMessageDispatcher类并引用它们与Twilio交互。

本演示中,我们以在网站发布新文章时通知用户为例。我们将向用户发送包含文章链接的WhatsApp消息,方便他们查看。

4.1. 配置内容SID

为限制企业发送未经请求或垃圾消息,WhatsApp要求所有企业发起的通知必须使用模板并预先注册。这些模板由唯一的内容SID标识,必须经WhatsApp批准才能在应用中使用

本例中配置以下消息模板:

新文章发布:{{ArticleURL}}

这里*{{ArticleURL}}*是占位符,发送通知时将被新发布文章的实际URL替换。

现在在TwilioConfigurationProperties类中定义嵌套类来保存内容SID:

@Valid
private NewArticleNotification newArticleNotification = new NewArticleNotification();

class NewArticleNotification {

    @NotBlank
    @Pattern(regexp = "^HX[0-9a-fA-F]{32}$")
    private String contentSid;

    // 标准setter和getter方法

}

再次添加验证注解确保正确配置内容SID并匹配预期格式。

类似地,在application.yaml文件中添加对应的内容SID属性:

com:
  baeldung:
    twilio:
      new-article-notification:
        content-sid: ${NEW_ARTICLE_NOTIFICATION_CONTENT_SID}

4.2. 实现消息分发器

配置好内容SID后,实现服务方法向用户发送通知:

public void dispatchNewArticleNotification(String phoneNumber, String articleUrl) {
    String messagingSid = twilioConfigurationProperties.getMessagingSid();
    String contentSid = twilioConfigurationProperties.getNewArticleNotification().getContentSid();
    PhoneNumber toPhoneNumber = new PhoneNumber(String.format("whatsapp:%s", phoneNumber));

    JSONObject contentVariables = new JSONObject();
    contentVariables.put("ArticleURL", articleUrl);

    Message.creator(toPhoneNumber, messagingSid)
      .setContentSid(contentSid)
      .setContentVariables(contentVariables.toString())
      .create();
}

在*dispatchNewArticleNotification()*方法中,使用配置的消息SID和内容SID向指定电话号码发送通知。还传递文章URL作为内容变量,用于替换消息模板中的占位符。

*注意也可以配置不带占位符的静态消息模板。这种情况下只需省略对setContentVariables()*方法的调用**。

5. 处理WhatsApp回复

发送通知后,用户可能回复想法或问题。当用户回复WhatsApp商业账号时,会开启24小时会话窗口,期间可以使用自由格式消息与用户沟通,无需预先批准的模板

**要自动处理用户回复,需要在Twilio消息服务中配置webhook接口**。Twilio服务在用户发送消息时调用此接口。我们在配置的API接口中接收多个参数,可用于自定义响应。

下面介绍如何在Spring Boot应用中创建这样的API接口。

5.1. 实现回复消息分发器

首先在WhatsAppMessageDispatcher类中创建服务方法发送自由格式回复消息:

public void dispatchReplyMessage(String phoneNumber, String username) {
    String messagingSid = twilioConfigurationProperties.getMessagingSid();
    PhoneNumber toPhoneNumber = new PhoneNumber(String.format("whatsapp:%s", phoneNumber));

    String message = String.format("你好 %s,我们的团队会尽快回复您。", username);
    Message.creator(toPhoneNumber, messagingSid, message).create();
}

在*dispatchReplyMessage()*方法中,向用户发送个性化消息,称呼其用户名并告知团队将尽快回复。

值得注意的是,在24小时会话期间甚至可以向用户发送多媒体消息

5.2. 暴露Webhook接口

接下来在应用中暴露POST API接口。此接口路径应与Twilio消息服务中配置的webhook URL匹配

@PostMapping(value = "/api/v1/whatsapp-message-reply")
public ResponseEntity<Void> reply(@RequestParam("ProfileName") String username,
        @RequestParam("WaId") String phoneNumber) {
    whatsappMessageDispatcher.dispatchReplyMessage(phoneNumber, username);
    return ResponseEntity.ok().build();
}

在控制器方法中,接收来自Twilio的ProfileNameWaId参数。这些参数分别包含发送消息用户的用户名和电话号码。然后将这些值传递给*dispatchReplyMessage()*方法向用户发送回复。

本例使用了ProfileNameWaId参数。但如前所述,Twilio向配置的API接口发送多个参数。例如可以访问Body参数检索用户消息的文本内容。潜在地可将此消息存储在队列中并路由到相应的支持团队进行进一步处理。

6. 测试Twilio集成

现在已实现使用Twilio发送WhatsApp消息的功能,下面介绍如何测试此集成。

测试外部服务可能具有挑战性,因为测试期间不想实际调用Twilio API。这里使用MockServer模拟出站Twilio调用

6.1. 配置Twilio REST客户端

要将Twilio API请求路由到MockServer,需要为Twilio SDK配置自定义HTTP客户端

在测试套件中创建类生成带有自定义HttpClientTwilioRestClient实例:

class TwilioProxyClient {

    private final String accountSid;
    private final String authToken;
    private final String host;
    private final int port;

    // 标准构造函数

    public TwilioRestClient createHttpClient() {
        SSLContext sslContext = SSLContextBuilder.create()
          .loadTrustMaterial((chain, authType) -> true)
          .build();
        
        HttpClientBuilder clientBuilder = HttpClientBuilder.create()
          .setSSLContext(sslContext)
          .setProxy(new HttpHost(host, port));

        HttpClient httpClient = new NetworkHttpClient(clientBuilder);
        return new Builder(accountSid, authToken)
          .httpClient(httpClient)
          .build();
    }

}

TwilioProxyClient类中,创建自定义HttpClient,将所有请求通过hostport参数指定的代理服务器路由。还配置SSL上下文信任所有证书,因为MockServer默认使用自签名证书

6.2. 配置测试环境

编写测试前,在src/test/resources目录创建application-integration-test.yaml文件,内容如下:

com:
  baeldung:
    twilio:
      account-sid: AC123abc123abc123abc123abc123abc12
      auth-token: test-auth-token
      messaging-sid: MG123abc123abc123abc123abc123abc12
      new-article-notification:
        content-sid: HX123abc123abc123abc123abc123abc12

这些虚拟值绕过了之前在TwilioConfigurationProperties类中配置的验证

现在使用@BeforeEach注解设置测试环境:

@Autowired
private TwilioConfigurationProperties twilioConfigurationProperties;

private MockServerClient mockServerClient;

private String twilioApiPath;

@BeforeEach
void setUp() {
    String accountSid = twilioConfigurationProperties.getAccountSid();
    String authToken = twilioConfigurationProperties.getAuthToken();

    InetSocketAddress remoteAddress = mockServerClient.remoteAddress();
    String host = remoteAddress.getHostName();
    int port = remoteAddress.getPort();

    TwilioProxyClient twilioProxyClient = new TwilioProxyClient(accountSid, authToken, host, port);
    Twilio.setRestClient(twilioProxyClient.createHttpClient());
    
    twilioApiPath = String.format("/2010-04-01/Accounts/%s/Messages.json", accountSid);
}

setUp()方法中,创建TwilioProxyClient实例,传入运行中MockServer实例的hostport。然后使用此客户端为Twilio SDK设置自定义RestClient。还在twilioApiPath变量中存储发送消息的API路径。

6.3. 验证Twilio请求

最后编写测试用例验证*dispatchNewArticleNotification()*方法向Twilio发送了预期请求:

// 设置测试数据
String contentSid = twilioConfigurationProperties.getNewArticleNotification().getContentSid();
String messagingSid = twilioConfigurationProperties.getMessagingSid();
String contactNumber = "+911001001000";
String articleUrl = RandomString.make();

// 配置MockServer期望
mockServerClient
  .when(request()
    .withMethod("POST")
    .withPath(twilioApiPath)
    .withBody(new ParameterBody(
        param("To", String.format("whatsapp:%s", contactNumber)),
        param("ContentSid", contentSid),
        param("ContentVariables", String.format("{\"ArticleURL\":\"%s\"}", articleUrl)),
        param("MessagingServiceSid", messagingSid)
    ))
  )
  .respond(response()
    .withStatusCode(200)
    .withBody(EMPTY_JSON));

// 调用被测方法
whatsAppMessageDispatcher.dispatchNewArticleNotification(contactNumber, articleUrl);

// 验证预期请求已发送
mockServerClient.verify(request()
  .withMethod("POST")
  .withPath(twilioApiPath)
  .withBody(new ParameterBody(
      param("To", String.format("whatsapp:%s", contactNumber)),
      param("ContentSid", contentSid),
      param("ContentVariables", String.format("{\"ArticleURL\":\"%s\"}", articleUrl)),
      param("MessagingServiceSid", messagingSid)
  )),
    VerificationTimes.once()
);

在测试方法中,首先设置测试数据并配置MockServer期望对Twilio API路径发送POST请求,请求体包含特定参数。还指示MockServer在收到此请求时响应200状态码和空JSON体。

然后使用测试数据调用*dispatchNewArticleNotification()*方法,并验证向MockServer发送了预期请求恰好一次。

通过使用MockServer模拟Twilio API,我们确保集成按预期工作,且无需实际发送任何消息或产生任何费用

7. 总结

本文探讨了如何在Spring Boot应用中使用Twilio发送WhatsApp消息。

我们完成了必要配置并实现了向用户发送带动态占位符的模板化通知功能。

最后通过暴露webhook接口接收Twilio的回复数据,实现了对通知的用户回复处理,并创建了服务方法发送通用的非模板化回复消息。

本文使用的所有代码示例均可在GitHub上获取。


原始标题:Sending WhatsApp Messages in Spring Boot Using Twilio | Baeldung