Dropwizard AWS LocalStack Tutorial
Dropwizard AWS LocalStack Tutorial
Introduction
Dropwizard is one of the most popular frameworks
used today for building microservices. I was looking for a
simple Dropwizard AWS LocalStack Tutorial online, but there are a very
few tutorials. Some are very tough to follow. Hence I decided to come up with
a simple post on this topic.
In this tutorial, we will focus on creating a
Dropwizard App and integrating it with AWS SQS(Simple Queue Service) and
SNS(Simple Notification Service). Then we will make a SQS Queue to subscribe
to a SNS Topic, publish a message on the topic and retrieve it on the SQS
Queue.
LocalStack
LocalStack is a fully functional local cloud
stack. With LocalSatck, you can run your AWS Applications or Lambdas on your
local machine without connecting to a remote cloud provider. To know more
about LocalStack please visit their website.
I am assuming that you have a Basic Knowledge of Dropwizard and have a Basic
Dropwizard Application running in your machine. If not, please check my blog
post on Basic Dropwizard Application by going to the link: Dropwizard Tutorial.
Requirements to Run the Application:
- Java
- Maven
- LocalStack
- AWS CLI
- IDE of your choice
Once you have a Basic Dropwizard Application and LocalStack up and running in your machine, here are the additional steps required to add the AWS integration.
Step 1: Maven Dependencies to be added in pom.xml
Add AWS Java SDK to the project by using the following dependency in the
pom.xml:
<dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-java-sdk</artifactId> <version>${amazonaws.version}</version> </dependency>Here is the full pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.aj.dropwizardaws</groupId> <artifactId>DropwizardAWS</artifactId> <version>1.0.0</version> <properties> <dropwizard.version>2.0.25</dropwizard.version> <amazonaws.version>1.12.62</amazonaws.version> <jackson.version>2.12.5</jackson.version> <jdk.version>1.8</jdk.version> <packaging>jar</packaging> </properties> <dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-java-sdk</artifactId> <version>${amazonaws.version}</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.1</version> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>${jackson.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.4</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3</version> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" /> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.aj.dropwizardaws.DropwizardAWSApplication</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
Step 2: AWS Managed Object
Most services involve objects which need to be started and stopped: thread
pools, database connections, etc. Dropwizard provides
the Managed interface for this. Managed interface is a
powerful and very useful concept that you should be aware of in Dropwizard.
You can either have the class in question implement
the start() and stop() methods, or write a
wrapper class which does so. Adding a Managed instance to
your service’s Environment ties that object’s lifecycle to
that of the service’s HTTP server. Before the server starts,
the start() method is called. After the server has stopped
(and after its graceful shutdown period) the stop() method is
called. We will configure AWS as a Managed object. The
code required to do that is present
in AWSManaged.java. Here is the code:
package com.aj.dropwizardaws; import com.amazonaws.AmazonWebServiceClient; import io.dropwizard.lifecycle.Managed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AWSManaged implements Managed { private static final Logger logger = LoggerFactory.getLogger(AWSManaged.class); private AmazonWebServiceClient awsClient; private AWSManaged() { // Prevent instantiation } public AWSManaged(AmazonWebServiceClient awsClient) { if (awsClient == null) { throw new IllegalArgumentException("AWS client cannot be null"); } this.awsClient = awsClient; } @Override public void start() throws Exception { } @Override public void stop() throws Exception { logger.info("Shutting down AWS client, " + awsClient.getClass()); awsClient.shutdown(); } }
Step 3: Object Classes
There are two object classes for SNS Request object and SQS Request Object.Here is AmazonSNSRequest.java:
package com.aj.dropwizardaws.domain; public class AmazonSNSRequest { private String topicArn; private String message; private String subject; private String queueUrl; public String getTopicArn() { return topicArn; } public void setTopicArn(String topicArn) { this.topicArn = topicArn; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getQueueUrl() { return queueUrl; } public void setQueueUrl(String queueUrl) { this.queueUrl = queueUrl; } @Override public String toString() { return "AmazonSNSRequest{" + "topicArn='" + topicArn + '\'' + ", message='" + message + '\'' + ", subject='" + subject + '\'' + ", queueUrl='" + queueUrl + '\'' + '}'; } }
Here is AmazonSQSRequest.java:
package com.aj.dropwizardaws.domain; import com.amazonaws.services.sqs.model.MessageAttributeValue; import java.util.Map; public class AmazonSQSRequest { private String queueUrl; private String messageBody; private int delaySeconds; private Map<String, MessageAttributeValue> messageAttributes; public String getQueueUrl() { return queueUrl; } public void setQueueUrl(String queueUrl) { this.queueUrl = queueUrl; } public String getMessageBody() { return messageBody; } public void setMessageBody(String messageBody) { this.messageBody = messageBody; } public int getDelaySeconds() { return delaySeconds; } public void setDelaySeconds(int delaySeconds) { this.delaySeconds = delaySeconds; } public Map<String, MessageAttributeValue> getMessageAttributes() { return messageAttributes; } public void setMessageAttributes(Map<String, MessageAttributeValue> messageAttributes) { this.messageAttributes = messageAttributes; } @Override public String toString() { return "AmazonSQSRequest{" + "queueUrl='" + queueUrl + '\'' + ", messageBody='" + messageBody + '\'' + ", delaySeconds=" + delaySeconds + ", messageAttributes=" + messageAttributes + '}'; } }
Step 4: API Resources
There are 2 API resources, one for SQS and one for SNS.
Here are the APIs in the SQS API class:
- Create Queue
- List Queues
- Send Message
- Receive Message
Here is AmazonSQSResource.java:
package com.aj.dropwizardaws.resource; import com.aj.dropwizardaws.domain.AmazonSQSRequest; import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.model.*; import com.codahale.metrics.annotation.Timed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.*; @Path("/amazonSQS") @Produces(MediaType.APPLICATION_JSON) public class AmazonSQSResource { private static AmazonSQS amazonSQS; public AmazonSQSResource(AmazonSQS builtSQS) { amazonSQS = builtSQS; } private static final Logger logger = LoggerFactory.getLogger(AmazonSQSResource.class); @Path("/createQueue/{sqsQueueName}") @Timed @GET @Produces(MediaType.APPLICATION_JSON) public Response createQueue(@PathParam("sqsQueueName") String sqsQueueName) { logger.info("SQS Queue Name is: {}", sqsQueueName); CreateQueueRequest request = new CreateQueueRequest().withQueueName(sqsQueueName); CreateQueueResult result = amazonSQS.createQueue(request); Map<String, String> response = new HashMap<>(); response.put("queueUrl", result.getQueueUrl()); return Response.ok(response).build(); } @Path("/listQueues") @Timed @GET @Produces(MediaType.APPLICATION_JSON) public Response listQueues() { List<String> queueUrls = amazonSQS.listQueues().getQueueUrls(); Map<String, List<String>> response = new HashMap<>(); response.put("sqsQueueURLs", queueUrls); return Response.ok(response).build(); } @Path("/sendMessage") @POST @Timed @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response send(AmazonSQSRequest amazonSQSRequest) { logger.info("Request received is: {}", amazonSQSRequest.toString()); SendMessageRequest sendMessageStandardQueue = new SendMessageRequest() .withQueueUrl(amazonSQSRequest.getQueueUrl()) .withMessageBody(amazonSQSRequest.getMessageBody()) .withDelaySeconds(amazonSQSRequest.getDelaySeconds()) .withMessageAttributes(amazonSQSRequest.getMessageAttributes()); amazonSQS.sendMessage(sendMessageStandardQueue); Map<String, String> response = new HashMap<>(); response.put("message", "Message sent"); return Response.ok(response).build(); } @Path("/receiveMessage") @POST @Timed @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response receiveMessage(AmazonSQSRequest amazonSQSRequest) { logger.info("Request received is: {}", amazonSQSRequest.toString()); Map<String, List<Message>> response = new HashMap<>(); List<Message> messages = amazonSQS.receiveMessage(new ReceiveMessageRequest(amazonSQSRequest. getQueueUrl()).withAttributeNames("All").withMessageAttributeNames("All")).getMessages(); logger.info("Number of messages are: {}", messages.size()); response.put("message", messages); return Response.ok(response).build(); } }
Here are the APIs in the SNS API class:
- Create Topic
- Get Topics
- Subscribe Queue
- Get Subscriptions
- Publish Message
Here is AmazonSNSResource.java:
package com.aj.dropwizardaws.resource; import com.aj.dropwizardaws.domain.AmazonSNSRequest; import com.amazonaws.services.sns.AmazonSNS; import com.amazonaws.services.sns.model.*; import com.amazonaws.services.sns.util.Topics; import com.amazonaws.services.sqs.AmazonSQS; import com.codahale.metrics.annotation.Timed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.HashMap; import java.util.List; import java.util.Map; @Path("/amazonSNS") @Produces(MediaType.APPLICATION_JSON) public class AmazonSNSResource { private static AmazonSNS amazonSNS; private static AmazonSQS amazonSQS; public AmazonSNSResource(AmazonSNS builtSNS, AmazonSQS builtSQS) { amazonSNS = builtSNS; amazonSQS = builtSQS; } private static final Logger logger = LoggerFactory.getLogger(AmazonSNSResource.class); @Path("/createTopic/{snsTopicName}") @Timed @GET @Produces(MediaType.APPLICATION_JSON) public Response createTopic(@PathParam("snsTopicName") String snsTopicName) { logger.info("SNS Topic Name is: {}", snsTopicName); CreateTopicRequest request = new CreateTopicRequest() .withName(snsTopicName); CreateTopicResult result = amazonSNS.createTopic(request); Map<String, String> response = new HashMap<>(); response.put("topicArn", result.getTopicArn()); return Response.ok(response).build(); } @Path("/getTopics") @Timed @GET @Produces(MediaType.APPLICATION_JSON) public Response getTopics() { List<Topic> topics = amazonSNS.listTopics().getTopics(); Map<String, List<Topic>> response = new HashMap<>(); response.put("snsTopics", topics); return Response.ok(response).build(); } @Path("/subscribeQueue") @Timed @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response subscribeQueue(AmazonSNSRequest amazonSNSRequest) { logger.info("Request received is: {}", amazonSNSRequest.toString()); Topics.subscribeQueue(amazonSNS, amazonSQS, amazonSNSRequest.getTopicArn(), amazonSNSRequest.getQueueUrl()); Map<String, String> response = new HashMap<>(); response.put("message", "SQS Queue is subscribed to SNS Topic successfully"); return Response.ok(response).build(); } @Path("/getSubscriptions") @Timed @GET @Produces(MediaType.APPLICATION_JSON) public Response getSubscriptions() { List<Subscription> subscriptions = amazonSNS.listSubscriptions().getSubscriptions(); Map<String, List<Subscription>> response = new HashMap<>(); response.put("snsSubscriptions", subscriptions); return Response.ok(response).build(); } @Path("/publish") @POST @Timed @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response publish(AmazonSNSRequest amazonSNSRequest) { logger.info("Request received is: {}", amazonSNSRequest.toString() ); PublishRequest publishRequest = new PublishRequest(amazonSNSRequest.getTopicArn(), amazonSNSRequest.getMessage()) .withSubject(amazonSNSRequest.getSubject()); PublishResult publishResult = amazonSNS.publish(publishRequest); Map<String, String> response = new HashMap<>(); response.put("Message published successfully. Message ID is: ", publishResult.getMessageId()); return Response.ok(response).build(); } }
Step 5: Create the Application Class
Everything comes together in the application class. We register the
resources, health check and AWS Managed Object in the application class. The
SNS and SQS entry points are initialized in the Application class using the
LocalStack AccessKey and SecretKey. We use the Docker Host IP
address and LocalStack runs on the port 4566. Using these details the
service end point is derived. The SQSClient and SNSClient are built with all
these details. The code is present in the Application class. Here is
DropwizardAWSApplication.java:
package com.aj.dropwizardaws; import com.aj.dropwizardaws.resource.AmazonSNSResource; import com.aj.dropwizardaws.resource.AmazonSQSResource; import com.aj.dropwizardaws.resource.DropwizardAWSHealthCheckResource; import com.aj.dropwizardaws.resource.PingResource; import com.amazonaws.AmazonWebServiceClient; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.client.builder.AwsClientBuilder; import com.amazonaws.services.sns.AmazonSNS; import com.amazonaws.services.sns.AmazonSNSClientBuilder; import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.AmazonSQSClientBuilder; import io.dropwizard.Application; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DropwizardAWSApplication extends Application<DropwizardAWSConfiguration> { private static final Logger logger = LoggerFactory.getLogger(DropwizardAWSApplication.class); public static void main(String[] args) throws Exception { new DropwizardAWSApplication().run("server", args[0]); } @Override public void initialize(Bootstrap<DropwizardAWSConfiguration> b) { } @Override public void run(DropwizardAWSConfiguration config, Environment env) { BasicAWSCredentials awsCreds = new BasicAWSCredentials("ajtechdeveloper", "softwaredevelopercentral"); logger.info("Initialize Amazon SNS entry point"); AmazonSNS sns = AmazonSNSClientBuilder.standard(). withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://192.168.99.100:4566", "us-east-1")). withCredentials(new AWSStaticCredentialsProvider(awsCreds)). build(); env.lifecycle().manage(new AWSManaged((AmazonWebServiceClient) sns)); logger.info("Initialize Amazon SQS entry point"); AmazonSQS sqs = AmazonSQSClientBuilder.standard(). withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://192.168.99.100:4566", "us-east-1")). withCredentials(new AWSStaticCredentialsProvider(awsCreds)). build(); env.lifecycle().manage(new AWSManaged((AmazonWebServiceClient) sqs)); logger.info("Registering RESTful API resources"); env.jersey().register(new PingResource()); env.jersey().register(new AmazonSQSResource(sqs)); env.jersey().register(new AmazonSNSResource(sns, sqs)); env.healthChecks().register("DropwizardAWSHealthCheck", new DropwizardAWSHealthCheckResource(config)); } }
Project Setup:
For Java Setup, please refer to: Java Setup
For Maven Setup, please refer to: Maven Setup
For Git and Project Setup, please refer to: Git and Project Setup
For Maven Setup, please refer to: Maven Setup
For Git and Project Setup, please refer to: Git and Project Setup
Run Application:
1. To run application in your IDE use:
Program arguments: src/main/resources/dropwizardaws.yml
Program arguments: src/main/resources/dropwizardaws.yml
2. To run JAR from command prompt:
Build jar by using command:
Build jar by using command:
mvn clean installRun JAR by using command in Project folder location:
java -jar target\DropwizardAWS-1.0.0.jar src\main\resources\dropwizardaws.yml
API calls and results:
1. Create Queue
2. List Queues
Response:
{ "sqsQueueURLs": [ "http://localhost:4566/000000000000/messageQueue" ] }
3. Send Message
Body:
{ "queueUrl": "http://192.168.99.100:4566/000000000000/messageQueue", "messageBody": "Simple message.", "delaySeconds": 5, "messageAttributes":{ "Attribute1": { "stringValue": "An attribute", "dataType": "String" }, "Attribute2": { "stringValue": "Another attribute", "dataType": "String" } } }
4. Create Topic
5. Get Topics
Response:
{ "snsTopics": [ { "topicArn": "arn:aws:sns:us-east-1:000000000000:messageTopic" } ] }
6. Subscribe Queue
Body:
{ "topicArn": "arn:aws:sns:us-east-1:000000000000:messageTopic", "queueUrl": "http://192.168.99.100:4566/000000000000/messageQueue" }
7. Get Subscriptions
Response:
{ "snsSubscriptions": [ { "subscriptionArn": "arn:aws:sns:us-east-1:000000000000:messageTopic:eff8e2f7-5cf7-4914-8a76-3c5eafe8d4ed", "owner": "", "protocol": "sqs", "endpoint": "arn:aws:sqs:us-east-1:000000000000:messageQueue", "topicArn": "arn:aws:sns:us-east-1:000000000000:messageTopic" } ] }
8. Publish Message
POST API: http://localhost:4000/amazonSNS/publish
Body:
{ "topicArn": "arn:aws:sns:us-east-1:000000000000:messageTopic", "message": "Message on SNS: Message Topic", "subject": "Message Subject" }
9. Receive Message
Body:
{ "queueUrl": "http://192.168.99.100:4566/000000000000/messageQueue" }Response 1: Message from SQS
{ "message": [ { "messageId": "ba63d6f1-1160-b01d-a1a9-5950422de93b", "receiptHandle": "ujhsudvtlfyokckqqhtprbzvyvbascjcutfqxdtgtoxpirvsdapvpayhzitfcrzqycuygvhlnhuzjsmbclroeezcxspzypthujkgicfwcmrqcszslrbycmxqxvhooejmwytaesozuzxslqggmfpbwampqkcfoywmumhkuiddafgtnttdhqrgtzdrm", "body": "Simple message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1631011444823", "SenderId": "AIDAIT2UOQQY3AUEKVGXU", "ApproximateFirstReceiveTimestamp": "1631011465975" }, "messageAttributes": { "Attribute2": { "stringValue": "Another attribute", "binaryValue": null, "stringListValues": [], "binaryListValues": [], "dataType": "String" }, "Attribute1": { "stringValue": "An attribute", "binaryValue": null, "stringListValues": [], "binaryListValues": [], "dataType": "String" } }, "md5OfMessageAttributes": "3993451abc49ee6de6c7df6f9163e8de", "md5OfBody": "e4db76f198a5b568108dfbf6b22a01eb" } ] }
Response 2: Message from SNS
{ "message": [ { "messageId": "0746bb54-71c8-fa23-b6f4-7687afec30b1", "receiptHandle": "sbvjggzvehdccfrdzcamicotyntdrfjlbdvliepuffqcfmnshidxpmdlqldtgsvynxiretajviblhgczibspjmivmsgxexanhjaaedecoucuhxxljdozriaoqedmomxllpvxbdbtaxdmaqxafeynmcvgsjwtaweikyvaxwcyiitgiruubctgjzqxj", "body": "{\"Type\": \"Notification\", \"MessageId\": \"a8b20ccb-aca3-475a-aa68-1a26f5c7b79c\", \"TopicArn\": \"arn:aws:sns:us-east-1:000000000000:messageTopic\", \"Message\": \"Message on SNS: Message Topic\", \"Timestamp\": \"2021-09-06T23:23:21.933Z\", \"SignatureVersion\": \"1\", \"Signature\": \"EXAMPLEpH+..\", \"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/SimpleNotificationService-0000000000000000000000.pem\", \"Subject\": \"Message Subject\"}", "attributes": { "ApproximateReceiveCount": "7", "SentTimestamp": "1630970602041", "MessageGroupId": "", "SenderId": "AIDAIT2UOQQY3AUEKVGXU", "ApproximateFirstReceiveTimestamp": "1630971531659" }, "messageAttributes": {}, "md5OfMessageAttributes": null, "md5OfBody": "87df4be88a67a73aa4dcd9802da25a65" } ] }
Other than these APIs, this application has the following APIs:
1. GET API for Application Health Check:
http://localhost:4000/admin/healthcheck
2. GET API to Ping and test if the application is up and running:
http://localhost:4000/ping
3. POST API to Ping and test if the application is up and running:
http://localhost:4000/ping
JSON Request Body:
{ "input": "ping" }4. GET Admin API to see application metrics: Dropwizard provides this Admin API. As I have set adminContextPath as: /admin in dropwizardmongodb.yml the link I need to use in any browser is: http://localhost:4000/admin/
You can click on individual links on this page to see different application metrics.
Conclusion and GitHub link:
This tutorial gives a comprehensive overview of Dropwizard,
LocalStack and integration with AWS SQS and SNS. The code for the application used in this
post is available on GitHub.
Learn the most popular and trending technologies like
Blockchain, Cryptocurrency, Machine Learning, Chatbots, Internet of Things
(IoT), Big Data Processing, Elastic Stack, React, Highcharts, Progressive
Web Application (PWA), Angular, gRPC, GraphQL, Golang, Akka HTTP, Play
Framework, Dropwizard, Docker, Netflix Eureka, Netflix Zuul,
Spring Cloud, Spring Boot, Flask and RESTful Web Service integration with
MongoDB, Kafka, Redis, Aerospike, MySQL DB in simple steps by reading my
most popular blog posts at Software Developer Central.
If you like my post, please feel free to share it using the share button just below this paragraph or next to the heading of the post. You can also tweet with #SoftwareDeveloperCentral on Twitter. To get a notification on my latest posts or to keep the conversation going, you can follow me on Twitter or Instagram. Please leave a note below if you have any questions or comments.
If you like my post, please feel free to share it using the share button just below this paragraph or next to the heading of the post. You can also tweet with #SoftwareDeveloperCentral on Twitter. To get a notification on my latest posts or to keep the conversation going, you can follow me on Twitter or Instagram. Please leave a note below if you have any questions or comments.
Comments
Post a Comment