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:
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:
Expand the "Access Keys (access key ID and secret access key)" tab and click on "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:
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:
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:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
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:
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:
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("[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:
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:
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:
- 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.
- 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.