Send messages to an Amazon SNS topic using Spring Boot

In this article we create an Amazon Simple Notification Service (Amazon SNS) topic and subscribe to it. Then we build a Spring Boot app that receives and publishes messages to the topic.

Amazon SNS provides message delivery from publishers to subscribers using the pub/sub pattern. Subscribers can be other applications, for example, an Amazon SQS queue, AWS lambda function, or HTTPS endpoint. A subscriber can also be a person, receiving messages through text messages, push notifications to their mobile device, or email.

Create a topic

  1. From the Amazon SNS Dashboard, select Topics, then Create Topic.
  2. Under Type, select Standard.
  3. Enter a name for the topic.
  4. Click Create Topic.

Add a subscription to the topic

Subscribe to the topic, so you’re notified when a message is published.

  1. From the Amazon SNS Dashboard, select Subscriptions, then Create Subscription.
  2. Select the topic just created.
  3. Select Email-JSON for protocol.
  4. Enter an email address.
  5. Click Create Subscription.
  6. Confirm the subscription by clicking on the Subscribe URL in the confirmation email sent by AWS.

Set up project dependencies

Generate a sample project using Spring Initializr (select Spring Web and Validation as dependencies).

In the pom.xml, add the BOM for AWS SDK for Java 2.x and a dependency for the SNS service.

<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>software.amazon.awssdk</groupId>
         <artifactId>bom</artifactId>
         <version>2.15.56</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
   </dependencies>
</dependencyManagement>

<dependencies>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>

   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
   </dependency>

   <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>sns</artifactId>
   </dependency>
</dependencies>
Code language: HTML, XML (xml)

Connect the application to AWS

We need to set up an AWS access key and secret key in the Java system properties, so the app is able to connect to AWS. One way to accomplish this is to set up an application listener.

package com.makolyte.springsnsstarter.config;

import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;

public class AppListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
        // Add AWS access key and secret key to Java system properties as soon as the environment is available
        System.setProperty("aws.accessKeyId", "your_access_key");
        System.setProperty("aws.secretAccessKey", "your_secret_key");
    }
}
Code language: Java (java)

The application listener must be added to the application.

package com.makolyte.springsnsstarter;

import com.makolyte.springsnsstarter.config.AppListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringSnsStarterApplication {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(SpringSnsStarterApplication.class);
        app.addListeners(new AppListener());
        app.run(args);
    }
}

Code language: Java (java)

Set up application properties

Include the following properties in application.properties. Update the values with those applicable to your setup.

### AWS ###
aws.sns.region=your_sns_region
aws.sns.topicArn=your_topic_arn
Code language: Properties (properties)

Configure the SNS Client

We need to configure the SNS Client that will publish messages to the topic.

First, create a class that reads the AWS SNS properties from the properties file.

package com.makolyte.springsnsstarter.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import javax.validation.constraints.NotNull;

@Configuration
@ConfigurationProperties(prefix = "aws.sns")
public class AwsProperties {

    @NotNull
    private String region;

    @NotNull
    private String topicArn;

    public String getRegion() {
        return region;
    }

    public void setRegion(String region) {
        this.region = region;
    }

    public String getTopicArn() {
        return topicArn;
    }

    public void setTopicArn(String topicArn) {
        this.topicArn = topicArn;
    }
}

Code language: Java (java)

Then, configure an SNS Client with the appropriate region.

package com.makolyte.springsnsstarter.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sns.SnsClient;

@Configuration
public class SnsConfig {

    @Autowired
    private AwsProperties awsProperties;

    @Bean
    public SnsClient snsClient() {
        return SnsClient.builder()
                .region(Region.of(awsProperties.getRegion()))
                .build();
    }
}

Code language: Java (java)

Build a message with message attributes

Let’s say topic subscribers are interested in being notified when prices for certain goods go up or down. Create this Message class to define the properties the subscribers are interested in.

package com.makolyte.springsnsstarter.model;

import java.math.BigDecimal;

public class Message {
    private String category;
    private String productName;
    private EventType eventType;
    private String seller;
    private BigDecimal newPrice;

    public Message() {}

    public Message(String category, String productName, EventType eventType, String seller, BigDecimal newPrice) {
        this.category = category;
        this.productName = productName;
        this.eventType = eventType;
        this.seller = seller;
        this.newPrice = newPrice;
    }

    public String getCategory() {
        return category;
    }

    public String getProductName() {
        return productName;
    }

    public EventType getEventType() {
        return eventType;
    }

    public String getSeller() {
        return seller;
    }

    public BigDecimal getNewPrice() {
        return newPrice;
    }
}

Code language: Java (java)
package com.makolyte.springsnsstarter.model;

public enum EventType {
    DROP, INCREASE
}

Code language: Java (java)

Next, create a builder that takes a message and builds an Amazon PublishRequest.

package com.makolyte.springsnsstarter.model;

import software.amazon.awssdk.services.sns.model.MessageAttributeValue;
import software.amazon.awssdk.services.sns.model.PublishRequest;

import java.util.HashMap;
import java.util.Map;

public class RequestBuilder {
    public static final String CATEGORY = "Category";
    public static final String PRODUCT_NAME = "ProductName";
    public static final String EVENT_TYPE = "EventType";
    public static final String SELLER = "Seller";
    public static final String NEW_PRICE = "NewPrice";
    public static final String DEFAULT_MESSAGE_BODY = "Please see attributes.";


    public static PublishRequest build(String topicArn, Message message) {
        Map<String, MessageAttributeValue> attributes = new HashMap<>();
        attributes.put(CATEGORY, buildAttribute(message.getCategory(), "String"));
        attributes.put(PRODUCT_NAME, buildAttribute(message.getProductName(), "String"));
        attributes.put(EVENT_TYPE, buildAttribute(message.getEventType().toString(), "String"));
        attributes.put(SELLER, buildAttribute(message.getSeller(), "String"));
        attributes.put(NEW_PRICE, buildAttribute(message.getNewPrice().toString(), "Number"));

        PublishRequest request = PublishRequest.builder()
                .topicArn(topicArn)
                .message(DEFAULT_MESSAGE_BODY)
                .messageAttributes(attributes)
                .build();

        return request;
    }

    private static MessageAttributeValue buildAttribute(String value, String dataType) {
        return MessageAttributeValue.builder()
                .dataType(dataType)
                .stringValue(value)
                .build();
    }
}

Code language: Java (java)

The message uses Amazon SNS message attributes. A benefit of sending the information using message attributes is that subscribers can filter their subscription by any of the attributes so that they only receive events they’re interested in.

Publish a message

Create an endpoint to publish a message to the topic. The endpoint returns the status code, message, and message ID returned by Amazon SNS. The message ID is a unique identifier assigned to a published message. If an exception occurs, the endpoint returns any status code and error message returned by Amazon SNS.

package com.makolyte.springsnsstarter.controller;

import com.makolyte.springsnsstarter.model.Message;
import com.makolyte.springsnsstarter.model.SnsResponse;
import com.makolyte.springsnsstarter.service.MessagePublisher;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
public class MessageController {

    private final MessagePublisher messagePublisher;

    public MessageController(MessagePublisher messagePublisher) {
        this.messagePublisher = messagePublisher;
    }

    @PostMapping(value = "/publish")
    @ResponseStatus(HttpStatus.CREATED)
    public SnsResponse publishMessage(@RequestBody Message message) {
        return messagePublisher.publish(message);
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(RuntimeException.class)
    private String handleException(RuntimeException e) {
        return e.getMessage();
    }
}

Code language: Java (java)
package com.makolyte.springsnsstarter.model;

public class SnsResponse {
    private Integer statusCode;
    private String message;
    private String publishedMessageId;

    public SnsResponse(Integer statusCode, String message, String publishedMessageId) {
        this.statusCode = statusCode;
        this.message = message;
        this.publishedMessageId = publishedMessageId;
    }

    public Integer getStatusCode() {
        return statusCode;
    }

    public String getMessage() {
        return message;
    }

    public String getPublishedMessageId() {
        return publishedMessageId;
    }

    @Override
    public String toString() {
        return "SnsResponse{" +
                "statusCode=" + statusCode +
                ", message='" + message + '\'' +
                ", publishedMessageId='" + publishedMessageId + '\'' +
                '}';
    }
}

Code language: Java (java)

Next, convert the message to an Amazon PublishRequest and publish it. For more information refer to the Publish API documentation.

package com.makolyte.springsnsstarter.service;

import com.makolyte.springsnsstarter.model.Message;
import com.makolyte.springsnsstarter.model.SnsResponse;

public interface MessagePublisher {
    SnsResponse publish(Message message);
}

Code language: Java (java)
package com.makolyte.springsnsstarter.service;

import com.makolyte.springsnsstarter.config.AwsProperties;
import com.makolyte.springsnsstarter.model.Message;
import com.makolyte.springsnsstarter.model.RequestBuilder;
import com.makolyte.springsnsstarter.model.SnsResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.core.exception.SdkServiceException;
import software.amazon.awssdk.http.SdkHttpResponse;
import software.amazon.awssdk.services.sns.SnsClient;
import software.amazon.awssdk.services.sns.model.PublishRequest;
import software.amazon.awssdk.services.sns.model.PublishResponse;
import software.amazon.awssdk.services.sns.model.SnsException;

@Service
public class MessagePublisherImpl implements MessagePublisher {
    private final static Logger LOG = LoggerFactory.getLogger(MessagePublisherImpl.class);

    private final SnsClient snsClient;
    private final AwsProperties awsProperties;

    public MessagePublisherImpl(SnsClient snsClient, AwsProperties awsProperties) {
        this.snsClient = snsClient;
        this.awsProperties = awsProperties;
    }

    @Override
    public SnsResponse publish(Message message) {
        SnsResponse response = null;

        try {
            PublishRequest request = RequestBuilder.build(awsProperties.getTopicArn(), message);
            LOG.info("Request: {}", request);

            PublishResponse publishResponse = snsClient.publish(request);
            LOG.info("Publish response: {}", publishResponse);

            SdkHttpResponse httpResponse = publishResponse.sdkHttpResponse();
            response = new SnsResponse(
                    httpResponse.statusCode(),
                    httpResponse.statusText().orElse(null),
                    publishResponse.messageId());
            LOG.info("Response details: {}", response);
        } catch (SnsException e) {
            rethrow(e.statusCode(), e.getClass().getSimpleName() + ": " + e.awsErrorDetails());
        } catch (SdkServiceException e) {
            rethrow(e.statusCode(), e.getClass().getSimpleName() + ": " + e.getMessage());
        } catch (SdkClientException e) {
            rethrow(null, e.getClass().getSimpleName() + ": " + e.getMessage());
        } catch (SdkException e) {
            rethrow(null, e.getClass().getSimpleName() + ": " + e.getMessage());
        }
        return response;
    }

    private void rethrow(Integer statusCode, String detailedMessage) {
        SnsResponse response = new SnsResponse(statusCode, detailedMessage, null);
        throw new RuntimeException(response.toString());
    }
}

Code language: Java (java)

The code handles different types of exceptions that could be thrown by Amazon SNS; SdkException is a catch all for the other exceptions. To learn more, refer to the Exception handling section of the AWS Developer Guide and the SnsClient javadoc.

Send a message using Postman

Run the app and send a message to the publish endpoint. The following example uses Postman.

In Postman, enter the following information, then click Send.

  • Method type: POST
  • URL: http://localhost:8080/publish
  • Message Body:
{
    "category": "Snow Removal",
    "productName": "Safe Paw35 lb. Coated Non-Salt Ice Melt",
    "eventType": "INCREASE",
    "seller": "Home Depot",
    "newPrice": "64.99"
}
Code language: JSON / JSON with Comments (json)
Sending a message to SNS with a Postman request

The response body shows that Amazon SNS returned a 200 status code with the message “OK” and a message ID, which confirms the message was published successfully.

Check the email that was previously subscribed to the topic and there should be a notification from AWS.

aws-notification-email

Sample code

The entire app can be found on GitHub.

Comments are closed.