Writing REST APIs with Spring MVC
There are two types of communication models. One of them is synchronous, where the client waits for the server to respond to its request. The other is asynchronous, where the client fires a request and forgets. Though Servlet 3.0 and above let you create asynchronous servlets, in our recipes, we will focus on traditional servlet-based HTTP APIs for simplicity. We will also be looking at asynchronous communication in later chapters.
Getting ready
When it comes to building REST APIs, there are several frameworks to choose from. As we already set up the Spring ecosystem in our previous recipe, it would make more sense and be much easier to use Spring MVC to expose REST APIs.
How to do it...
The true advantage of Spring Boot is that you do not have to add any new dependencies to enable web support for your application. Spring Boot's parent pom
file (spring-boot-starter-parent
) takes care of that for you. Now let's take a look at how to write our first API. If you are familiar with Spring MVC, this should be really straight-forward for you:
- Create a
Controller
class calledcom.packt.microservices.geolocation.GeoLocationController.java
, which will be responsible for basic CRUD operations for the geolocation of all users:package com.packt.microservices.geolocation; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/geolocation") public class GeoLocationController { }
There are two things to note here. The
@RestController
annotation indicates that we are going to use this controller to expose our REST APIs. It implicitly adds the@ResponseBody
annotation to all controller methods as that is something you would want to do when exposing your REST APIs using Spring MVC. The@RequestMapping
annotation specifies where your HTTP resource is located. We are setting@RequestMapping
on the controller level to apply it to all controller methods.Note
Using
@RequestMapping
on theController
class level to define a root resource path is considered to be one of the best practices. Instead of having to create API paths such as/getGeolocation
or/createGeolocation
, it is always a better practice to use the same path,/geolocation
, with theGET
method to get geolocation data and thePOST
method to create geolocation data. - Before we jump into creating our APIs, we will need some classes for the domain object and service. Let's start with creating our domain object. Assume that our
GeoLocation
consists latitude and longitude. We will be defining both latitude and longitude asdouble
to provide better precision. Now we will have to say which user's geolocation it is. So we might want to add auserId
. We also need to say at what time the user was at the geolocation. So we might want to add a timestamp in EPOCH time format. The timestamp will be of typelong
. This is how your plain old java object (POJO) class will look:package com.packt.microservices.geolocation; import java.io.Serializable; import java.util.UUID; public class GeoLocation implements Serializable { private static final long serialVersionUID = 1L; private double latitude; private double longitude; private UUID userId; private long timestamp; public double getLatitude() { return latitude; } public void setLatitude(double latitude) { this.latitude = latitude; } public double getLongitude() { return longitude; } public void setLongitude(double longitude) { this.longitude = longitude; } public UUID getUserId() { return userId; } public void setUserId(UUID userId) { this.userId = userId; } public long getTimestamp() { return timestamp; } public void setTimestamp(long timestamp) { this.timestamp = timestamp; } }
As you can see, we have used the
java.util.UUID
class to represent theuserId
, assuming that this UUID uniquely identifies a user. We will not be creating the userPOJO
as it is out of scope for this recipe.In an ideal scenario, one would be using a NoSQL or relational database to store the geolocations. In this case, NoSQL sounds more suitable due to several reasons, including the fact that our data is time series data, in JSON format, unstructured but will change over time and we will have a humongous amount of data.
- For simplicity purposes, we will be storing our geolocations in an in-memory
java.util.List<GeoLocation>
collection. Let's create our repository that holds all our geolocation objects,com.packt.microservices.geolocation.GeoLocationRepository.java
:package com.packt.microservices.geolocation; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.springframework.stereotype.Repository; @Repository public class GeoLocationRepository { private List<GeoLocation> geolocations = new ArrayList<GeoLocation>(); public void addGeoLocation(GeoLocation geolocation) { geolocations.add(geolocation); } public List<GeoLocation> getGeoLocations() { return Collections.unmodifiableList(geolocations); } }
- Now let's take a look at how your
Service
interface will look:package com.packt.microservices.geolocation; import java.util.List; public interface GeoLocationService { public GeoLocation create(GeoLocation geolocation); public List<GeoLocation> findAll(); }
- Both our repository and service have a very simple interface. Ideally in real-time applications, you might want to add more complicated methods that not only perform CRUD operations but also sort, filter, select only specific fields, and so on. Now let's take a look at our
com.packt.microservices.geolocation.GeoLocationServiceImpl.java
class:package com.packt.microservices.geolocation; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class GeoLocationServiceImpl implements GeoLocationService { @Autowired private GeoLocationRepository repository; @Override public GeoLocation create(GeoLocation geolocation) { repository.addGeoLocation(geolocation); return geolocation; } @Override public List<GeoLocation> findAll() { return repository.getGeoLocations(); } }
Note
It is always strongly recommended that you write unit test cases for any new code. But as that is a little out of scope for this book, we will not be writing unit test cases for any of the previous code. To learn more about unit testing Spring Boot applications, please take a look at Spring Boot's documentation at https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html.
- Now that our domain and service classes are all set to go, let's modify our
Controller
class to save and find geolocations. Add the following snippet into yourController
class body:@Autowired private GeoLocationService service; @RequestMapping(method = RequestMethod.POST, produces = "application/json", consumes = "application/json") public GeoLocation create(@RequestBody GeoLocation geolocation) { return service.create(geolocation); } @RequestMapping(method = RequestMethod.GET, produces = "application/json") public List<GeoLocation> findAll() { return service.findAll(); }
In this implementation, there are a few things to notice. The @RequestMapping
annotation does not have a path
defined as it is already derived from the class-level annotation. For both the create
and findAll
methods, we are using the same path but different HTTP methods as per best practice. Since we are dealing only with JSON here, we have set the produces
and consumes
values to application/json
. The return types of the create
and findAll
methods are GeoLocation
and List<GeoLocation>
respectively. Spring MVC internally uses Jackson to convert them to their equivalent JSON strings.
That's it! We are now ready to test our application:
- Let's try to create two geolocations using the
POST
API and later try to retrieve them using theGET
method. Execute the following cURL commands in your terminal one by one:curl -H "Content-Type: application/json" -X POST -d'{"timestamp": 1468203975, "userId": "f1196aac-470e-11e6-beb8-9e71128cae77", "latitude": 41.803488, "longitude": -88.144040}' http://localhost:8080/geolocation
- This should give you an output similar to the following (pretty-printed for readability):
{ "latitude": 41.803488, "longitude": -88.14404, "userId": "f1196aac-470e-11e6-beb8-9e71128cae77", "timestamp": 1468203975 } curl -H "Content-Type: application/json" -X POST -d '{"timestamp": 1468203975, "userId": "f1196aac-470e-11e6-beb8-9e71128cae77", "latitude": 9.568012, "longitude": 77.962444}' http://localhost:8080/geolocation
- This should give you an output similar to the following (pretty-printed for readability):
{ "latitude": 9.568012, "longitude": 77.962444, "userId": "f1196aac-470e-11e6-beb8-9e71128cae77", "timestamp": 1468203975 }
- To verify whether your entities were stored correctly, execute the following cURL command:
curl http://localhost:8080/geolocation
- This should give you an output similar to the following (pretty-printed for readability):
[ { "latitude": 41.803488, "longitude": -88.14404, "userId": "f1196aac-470e-11e6-beb8-9e71128cae77", "timestamp": 1468203975 }, { "latitude": 9.568012, "longitude": 77.962444, "userId": "f1196aac-470e-11e6-beb8-9e71128cae77", "timestamp": 1468203975 } ]
You now have a fully working version of your microservice. The remaining recipes in this chapter try to achieve the same logic with different frameworks, such as WildFly Swarm and Dropwizard. Later in this chapter, we will also look at another framework that helps you build REST APIs quickly called SparkJava (different from Apache Spark). If you will be using Spring Boot for your microservices, you can jump to the next chapter. If you are interested in any of the frameworks that were mentioned, jump to the appropriate recipe in this chapter.