Spring Cloud: AWS SNS

Introduction

Sending notifications to users is a fairly common task - be it through email, SMS messages, or even through HTTP/HTTPS POST requests.

The Simple Notification Service (SNS) is a publisher/subscriber messaging system provided by Amazon Web Services (AWS). It's a popular choice for many developers and very reliable.

In this article, we'll be making a Spring Cloud application with messaging support (SMS and Email) with the help of AWS SNS.

Why Choose AWS SNS?

AWS' Simple Notification Service enables a publisher (typically a microservice) to send (publish) notifications on certain topics to receivers (subscribers) through various mediums - SMS, email, HTTP, AWS Lambda, and AWS SQS.

These receivers deliberately subscribe to a topic they wish to receive notifications from:

Spring Cloud AWS SNS - Messaging Support

Here are some of the reasons why AWS SNS is extremely popular:

  • Allows publishing to HTTP endpoints and other AWS Services
  • Supports over 205 countries for SMS and email delivery. This level of availability is especially important if your users are going to be of global origin.
  • Guarantees delivery of messages as long as the SMS/email address is valid.
  • AWS provides a feature-rich and well-written SDK for Java for SNS with excellent documentation.
  • Through Spring's easily integrated modules, the hassle of integrating AWS's SDK for Java is made extremely simple.
  • If you are already using other AWS services for storage or deployment, then it is a no-brainer to stay in the same ecosystem and use SNS.

Spring Boot Use-Cases for AWS SNS

There are many areas where you could use SMS, email, or HTTP/S notifications in a Spring Web application:

  • Notify all microservices of an application-wide event.
  • Notify admins/developers that of critical errors or downed services.
  • Phone number verification via OTP (One-Time Password) during user registration or password reset.
  • Notify users of an event that is directly associated with the user (ex: an application is accepted).
  • Increase user engagement as email and SMS notifications can bring the user back to your application.

AWS Account

Like with any AWS service, we need to get the Access Key ID and Secret Key from our AWS account. Login to your AWS console and visit the "My Security Credentials" page listed under your account drop-down menu:

My Security Credentials

Expand the "Access Keys (access key ID and secret access key)" tab and click on "Create New Access Key":

Create New Access Key

Download your credentials file and keep it somewhere safe! Nobody should have access to this file as then they'll also have full authorization to use your AWS account:

Download credentials

You need to decide on an AWS region to use as the processing location of your SNS service requests. Note your SMS pricing might differ according to the chosen region and that not all regions support SMS messages.

Make sure to choose an SMS supported location from here.

For the sake of brevity, we've used the root account to generate the AWS Key Id and Secret Key - but this practice is highly discouraged, and AWS recommends using IAM User roles instead.

Spring Boot Project

As always, for a quick bootstrapped Spring Boot project, we'll be using Spring Initializr:

Spring Initializr

Alternatively, we can use the Spring Boot CLI:

$ spring init --dependencies=web sns-demo

Dependencies

Using your build tool of choice, add the required dependencies:

Gradle

dependencies {
    implementation platform('software.amazon.awssdk:bom:2.5.29') // BOM for AWS SDK For Java
    implementation 'software.amazon.awssdk:sns' // We only need to get SNS SDK in our case
    implementation 'software.amazon.awssdk:ses' // Needed for sending emails with attachment
    implementation 'com.sun.mail:javax.mail' // Needed for sending emails with attachment
    compile group: 'org.springframework.cloud', name: 'spring-cloud-aws-messaging', version: '2.2.1.RELEASE'
    compile group: 'org.springframework.cloud', name: 'spring-cloud-aws-autoconfigure', version: '2.2.1.RELEASE'
}

Maven

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-aws-messaging</artifactId>
    <version>{version}</version>
</dependency>

Sending Emails Using SNS

Create an SNS Topic

An SNS Topic is an access point that groups together different endpoints between a publisher (our Spring Boot project) and subscribers. A publisher publishes a message to a topic and that message will then be delivered to all the subscribers of that topic.

First, let's define a helper method that'll allow us to get an SNS client:

private SnsClient getSnsClient() throws URISyntaxException {
    return SnsClient.builder()
            .credentialsProvider(getAwsCredentials(
                    "Access Key ID",
                    "Secret Key"))
            .region(Region.US_EAST_1) // Set your selected region
            .build();
}

This method uses another helper method, getAWSCredentials():

private AwsCredentialsProvider getAwsCredentials(String accessKeyID, String secretAccessKey {
    AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKeyID, secretAccessKey);
    AwsCredentialsProvider awsCredentialsProvider = () -> awsBasicCredentials;
    return awsCredentialsProvider;
}

Really, you can set up the client when you use it, but helper methods are a bit more elegant. With that out of the way, let's make an endpoint for topic creation:

@RequestMapping("/createTopic")
private String createTopic(@RequestParam("topic_name") String topicName) throws URISyntaxException {

    // Topic name cannot contain spaces
    final CreateTopicRequest topicCreateRequest = CreateTopicRequest.builder().name(topicName).build();

    // Helper method makes the code more readable
    SnsClient snsClient = getSnsClient();

    final CreateTopicResponse topicCreateResponse = snsClient.createTopic(topicCreateRequest);

    if (topicCreateResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Topic creation successful");
        System.out.println("Topic ARN: " + topicCreateResponse.topicArn());
        System.out.println("Topics: " + snsClient.listTopics());
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, topicCreateResponse.sdkHttpResponse().statusText().get()
        );
    }

    snsClient.close();

    return "Topic ARN: " + topicCreateResponse.topicArn();
}

Note: If your system is behind a proxy then you need to configure your SnsClient with a custom HTTP client set to work with your proxy:

SnsClient snsClient = SnsClient.builder()
        .credentialsProvider(getAwsCredentials(
                "Access Key ID",
                "Secret Key"))
        .httpClient(getProxyHTTPClient("http://host:port"))
        .region(Region.US_EAST_1) // Set your selected region
        .build();

private SdkHttpClient getProxyHTTPClient(String proxy) throws URISyntaxException {
    URI proxyURI = new URI(proxy);
    // This HTTP Client supports custom proxy
    final SdkHttpClient sdkHttpClient = ApacheHttpClient.builder()
            .proxyConfiguration(ProxyConfiguration.builder()
                    .endpoint(proxyURI)
                    .build())
            .build();

    return sdkHttpClient;
}

Or, you could use the system proxy:

private SdkHttpClient getProxyHTTPClient() throws URISyntaxException {
    // This HTTP Client supports system proxy
    final SdkHttpClient sdkHttpClient = ApacheHttpClient.builder()
            .proxyConfiguration(ProxyConfiguration.builder()
                    .useSystemPropertyValues(true)
                    .build())
            .build();

    return sdkHttpClient;
}

Finally, let's make a curl request to test out if our topic creation works:

$ curl http://localhost:8080/createTopic?topic_name=Stack-Abuse-Demo
Topic ARN: arn:aws:sns:us-east-1:123456789:Stack-Abuse-Demo

You can also confirm if the topic was created or not from your AWS console:

AWS SNS Topics

Please store the topic ARN (Amazon Resource Name) somewhere (for example in a database along with user records) as we'll need it later.

Subscribing to a Topic

With the topic setup out of the way, let's make an endpoint for subscription. Since we're doing email, we'll set the protocol to for "email". Please note than in AWS terms, a "subscriber" is referred to as an "endpoint", so we'll use our email address for the endpoint property:

@RequestMapping("/addSubscribers")
private String addSubscriberToTopic(@RequestParam("arn") String arn) throws URISyntaxException {

    SnsClient snsClient = getSnsClient();

    final SubscribeRequest subscribeRequest = SubscribeRequest.builder()
            .topicArn(arn)
            .protocol("email")
            .endpoint("[email protected]")
            .build();

    SubscribeResponse subscribeResponse = snsClient.subscribe(subscribeRequest);

    if (subscribeResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Subscriber creation successful");
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, subscribeResponse.sdkHttpResponse().statusText().get()
        );
    }
    snsClient.close();

    return "Subscription ARN request is pending. To confirm the subscription, check your email.";
}

Let's send another curl request:

$ curl http://localhost:8080/addSubscribers?arn=arn:aws:sns:us-east-1:123456789:Stack-Abuse-Demo
Subscription ARN request is pending. To confirm the subscription, check your email.

Note: The subscriber needs to confirm the subscription by visiting their email address and clicking on the confirmation email sent by AWS:

AWS SNS Topic Subscription

Sending Emails

Now you can publish emails to your topic, and all the recipients who have confirmed their subscription should receive the message:

@RequestMapping("/sendEmail")
private String sendEmail(@RequestParam("arn") String arn) throws URISyntaxException {

    SnsClient snsClient = getSnsClient();

    final SubscribeRequest subscribeRequest = SubscribeRequest.builder()
                                              .topicArn(arn)
                                              .protocol("email")
                                              .endpoint("sapnes[email protected]")
                                              .build();

    final String msg = "This Stack Abuse Demo email works!";

    final PublishRequest publishRequest = PublishRequest.builder()
                                          .topicArn(arn)
                                          .subject("Stack Abuse Demo email")
                                          .message(msg)
                                          .build();

    PublishResponse publishResponse = snsClient.publish(publishRequest);

    if (publishResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Message publishing successful");
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, publishResponse.sdkHttpResponse().statusText().get());
    }

    snsClient.close();
    return "Email sent to subscribers. Message-ID: " + publishResponse.messageId();
}

Let's send another curl request:

$ curl http://localhost:8080/sendEmail?arn=arn:aws:sns:us-east-1:650924441247:Stack-Abuse-Demo
Email sent to subscribers. Message-ID: abdcted-8bf8-asd54-841b-5e0be960984c

And checking our email, we're greeted with:

AWS SNS Notification Email

Handling Email Attachments

AWS SNS supports message sizes of only up to 256Kb, and it does not support attachments. SNS' primary feature is sending notification messages, not attachments.

If you need to send attachments with your email, then you'll need to use AWS' Simple Email Service (SES), along with its SendRawEmail to achieve this functionality. We'll be constructing the emails with the javax.mail library.

If you're unfamiliar with it, feel free to check out How to Send Emails in Java.

First, let's set up the SesClient, just like we set up the SnsClient and add an email address:

SesClient sesClient = SesClient.builder()
        .credentialsProvider(getAwsCredentials(
                "Access Key ID",
                "Secret Key"))
        .region(Region.US_EAST_1) //Set your selected region
        .build();

VerifyEmailAddressRequest verifyEmailAddressRequest = VerifyEmailAddressRequest.builder()
        .emailAddress("[email protected]").build();
sesClient.verifyEmailAddress(verifyEmailAddressRequest);

The email addresses you add here will be sent a confirmation message and the owners of the email address need to confirm the subscription.

And then, let's build an email object and use AWS' SendRawEmail to send them:

@RequestMapping("/sendEmailWithAttachment")
private String sendEmailWithAttachment(@RequestParam("arn") String arn) throws URISyntaxException, MessagingException, IOException {

    String subject = "Stack Abuse AWS SES Demo";

    String attachment = "{PATH_TO_ATTACHMENT}";

    String body = "<html>"
                    + "<body>"
                        + "<h1>Hello!</h1>"
                        + "<p>Please check your email for an attachment."
                    + "</body>"
                + "</html>";

    Session session = Session.getDefaultInstance(new Properties(), null);
    MimeMessage message = new MimeMessage(session);

    // Setting subject, sender and recipient
    message.setSubject(subject, "UTF-8");
    message.setFrom(new InternetAddress("[email protected]")); // AWS Account Email
    message.setRecipients(Message.RecipientType.TO,
            InternetAddress.parse("[email protected]")); // Recipient email

    MimeMultipart msg_body = new MimeMultipart("alternative");
    MimeBodyPart wrap = new MimeBodyPart();

    MimeBodyPart htmlPart = new MimeBodyPart();
    htmlPart.setContent(body, "text/html; charset=UTF-8");
    msg_body.addBodyPart(htmlPart);
    wrap.setContent(msg_body);

    MimeMultipart msg = new MimeMultipart("mixed");

    message.setContent(msg);
    msg.addBodyPart(wrap);

    MimeBodyPart att = new MimeBodyPart();
    DataSource fds = new FileDataSource(attachment);
    att.setDataHandler(new DataHandler(fds));
    att.setFileName(fds.getName());
    msg.addBodyPart(att);

    // Build SesClient
    SesClient sesClient = SesClient.builder()
            .credentialsProvider(getAwsCredentials(
                    "Access Key ID",
                    "Secret Key"))
            .region(Region.US_EAST_1) // Set your preferred region
            .build();

    // Send the email
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    message.writeTo(outputStream);

    RawMessage rawMessage = RawMessage.builder().data(SdkBytes.fromByteArray(outputStream.toByteArray())).build();

    SendRawEmailRequest rawEmailRequest = SendRawEmailRequest.builder().rawMessage(rawMessage).build();

    // The .sendRawEmail method is the one that actually sends the email
    SendRawEmailResponse sendRawEmailResponse = sesClient.sendRawEmail(rawEmailRequest);

    if (sendRawEmailResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Message publishing successful");
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, sendRawEmailResponse.sdkHttpResponse().statusText().get()
        );
    }

    return "Email sent to subscribers. Message-ID: " + sendRawEmailResponse.messageId();
}

And finally, let's send a request to test if this is working:

$ curl http://localhost:8080/sendEmailWithAttachment?arn=arn:aws:sns:Stack-Abuse-Demo
Email sent to subscribers. Message-ID: 0100016fa375071f-4824-2b69e1050efa-000000

Note: If you cannot find the email, make sure to check your spam folder:

AWS SNS Notification Email with Attachment

Sending SMS Messages

Some prefer to send SMS messages instead of emails, mainly as SMS messages are more likely to be seen. There are two types of SMS messages:

  1. Promotional: As the name says, these message types are used for promotional purposes only. These messages are delivered between 9AM and 9PM and should only contain promotional material.
  2. Transactional: These messages are used for high value and critical notifications. For example, for OTP and phone number verification. These type of messages cannot be used for promotional purposes as it violates the regulations set for transactional messages.

Send SMS to a Single Phone Number

@RequestMapping("/sendSMS")
private String sendSMS(@RequestParam("phone") String phone) throws URISyntaxException {
    SnsClient snsClient = getSnsClient();

    final PublishRequest publishRequest = PublishRequest.builder()
            .phoneNumber(phone)
            .message("This is Stack Abuse SMS Demo")
            .build();

    PublishResponse publishResponse = snsClient.publish(publishRequest);

    if (publishResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Message publishing to phone successful");
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, publishResponse.sdkHttpResponse().statusText().get()
        );
    }
    snsClient.close();
    return "SMS sent to " + phone + ". Message-ID: " + publishResponse.messageId();
}

Let's test it out with a curl request:

$ curl http://localhost:8080/sendSMS?phone=%2B9112345789
SMS sent to +919538816148. Message-ID: 82cd26aa-947c-a978-703d0841fa7b

Send SMS in Bulk

Sending SMS in bulk isn't done by simply looping the previous approach. This time, we'll be creating an SNS topic and instead of email, we'll use the sms protocol. When we wish to send a message in bulk, all subscribed phone numbers will receive the notification:

@RequestMapping("/sendBulkSMS")
private String sendBulkSMS(@RequestParam("arn") String arn) throws URISyntaxException {

    SnsClient snsClient = getSnsClient();

    String[] phoneNumbers = new String[]{"+917760041698", "917760041698", "7760041698" };

    for (String phoneNumber: phoneNumbers) {
        final SubscribeRequest subscribeRequest = SubscribeRequest.builder()
                                                  .topicArn(arn)
                                                  .protocol("sms")
                                                  .endpoint(phoneNumber)
                                                  .build();

        SubscribeResponse subscribeResponse = snsClient.subscribe(subscribeRequest);
        if (subscribeResponse.sdkHttpResponse().isSuccessful()) {
            System.out.println(phoneNumber + " subscribed to topic "+arn);
        }
    }

    final PublishRequest publishRequest = PublishRequest.builder()
                                          .topicArn(arn)
                                          .message("This is Stack Abuse SMS Demo")
                                          .build();

    PublishResponse publishResponse = snsClient.publish(publishRequest);

    if (publishResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Bulk Message sending successful");
        System.out.println(publishResponse.messageId());
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, publishResponse.sdkHttpResponse().statusText().get()
        );
    }
    snsClient.close();
    return "Done";
}

Conclusion

Spring Cloud AWS makes it extremely easy to incorporate AWS services into a Spring Boot project.

AWS SNS is a reliable and simple publisher/subscriber service, used by many developers over the globe to send simple notifications to other HTTP endpoints, emails, phones, and other AWS services.

We've built a simple Spring Boot application that generates an SNS Topic, can add subscribers to it and send them messages via email and SMS.

The source code is available on GitHub.