Creating a REST Web Service With Java and Spring (Part 2)
Welcome back! We pick up where we left off, and show you how to implement the data source and domain layers of your REST web service.
Join the DZone community and get the full member experience.
Join For FreeIn the prevous article, we delved into the design and thought process behind the creation of a Java RESTful web service. In this article, we will start by implementing the bottom-most layers in our system: The data source and domain layers.
Implementing the Web Service
Finding where to start can be difficult, but we will take a systematic approach to creating our web service. Start with the layer that depends on the fewest other layers and is depended on by the greatest number of layers. Thus, we will first implement the domain layer, which does not depend on the other two layers but is depended on by both. Then we will implement the data source layer, which depends on the domain layer and is likewise depended on by the presentation layer. Lastly, we will implement the presentation layer, which is not depended on by any other layer but depends on both the data source and domain layers.
Implementing the Domain Layer
The first step in creating our RESTful web service is creating the domain layer. While our domain layer is very simple, we will soon see that it requires some very specific details to be accounted for in order to be properly used by the rest of our system. The first of these details is identity.
All domain objects that will be persisted must have some unique means of identity; the simplest among these is a unique ID. While there are many data structures that can be used as IDs, such as Universally Unique IDs (UUIDs), we will keep it as simple from the start and use a numeric value. Each class in our domain layer must, therefore, have a means of obtaining the ID associated with the object. In addition, to allow our persistence layer to set the ID of a newly created domain object, we must have a means of setting the ID for an object as well. Since we do not want the persistence layer to depend on any concrete class in our domain layer for this functionality, we will create an interface to accomplish this task:
public interface Identifiable extends org.springframework.hateoas.Identifiable<Long> {
public void setId(Long id);
}
While we could have simply created an interface with a getter and a setter for ID, we instead extend the Identifiable
interface provided by the Spring HATEOAS framework. This interface provides a getId()
method and extending this interface allows us to use our domain class within the Spring HATEOAS framework, which will come in handy when we implement the presentation layer. With our identity interface complete, we can now create our Order
class:
public class Order implements Identifiable {
private Long id;
private String description;
private long costInCents;
private boolean isComplete;
@Override
public Long getId() {
return id;
}
@Override
public void setId(Long id) {
this.id = id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public void setCostInCents(long cost) {
costInCents = cost;
}
public long getCostInCents() {
return costInCents;
}
public void setComplete(boolean isComplete) {
this.isComplete = isComplete;
}
public void markComplete() {
setComplete(true);
}
public void markIncomplete() {
setComplete(false);
}
public boolean isComplete() {
return isComplete;
}
}
Our Order class is strikingly simple, but that is the beauty of separating domain classes from the remainder of the system: we can create Plain Old Java Objects (POJOs) that are independent of the rest of the intricacies and idiosyncrasies of our web service (such as how the POJO is persisted and how it is presented through a RESTful interface). Due to the simplicity of this class (it is only composed of getters and setters), we have forgone unit tests.
Although automated unit tests are important, they should be used with discretion. If our Order
class contained complicated business logic, such as "an order can only be created between 8am and 5pm EST," we would be wise to implement this logic by first creating a test that exercises this functionality and implement our Order
class to pass this test. As more functionality is added, we can create more tests and implement this functionality to pass these tests as well (the core of TDD). This leaves us with a general rule about unit testing, especially for domain objects:
It is also important to note that we have created three methods for setting the completion status of our order: (1) setComplete
, (2) markComplete
, and (3) markIncomplete
. Although it is usually not good practice to set the value of a boolean flag using a setter that takes a boolean argument, we have added this method to our Order
class because it allows for simplified updated logic. For example, if we did not have this method, we would have to update the completion status of an order in a manner akin to:
public void updateOrder(Order original, Order updated) {
// Update the other fields of the order
if (updated.isComplete()) {
original.markComplete();
}
else {
original.makeIncomplete();
}
}
By providing a setter with a boolean argument, we can reduce this update logic to:
public void updateOrder(Order original, Order updated) {
// Update the other fields of the order
original.setComplete(updated.isComplete());
}
We have also provided the remaining two setter methods for the completion state to allow for clients to set the completion status in the recommended manner (without passing a boolean flag to a setter method).
In general, we want to externalize the update logic of our domain objects. Instead of creating anupdate(Order updated)
method within our Order
class, we leave it up to external clients to perform the update. The reasoning behind this is that our Order
class does not have enough knowledge of the update process needed by external clients to internalize this logic. For example, when we update an Order
in the persistence layer, do we update the ID of our order or leave the original ID? That is a decision that should be made in the persistence layer, and therefore, we leave it up to the persistence layer to implement the update logic for our Order
class.
Implementing the Data Source Layer
The first step to developing our data source layer is to define the requirements that this layer must fulfill. At its most basic level, our data source must be able to perform the following fundamental operations:
- Retrieve a list of all orders.
- Retrieve a specific order based on its ID.
- Create a new order.
- Delete an existing order.
- Update an existing order.
These five operations are common among a vast majority of data sources, and unsurprisingly, follows the basic set of CRUD operations. In this particular case, we create a specialization of the read operation (the R in CRUD) by allowing a caller to supply an ID and retrieve only the order that corresponds to that ID (if such an order exists).
This specialization is particularly useful when dealing with databases, where reads and writes are relatively expensive operations. If we were only capable of retrieving all order and then filtering by ID to find the order of interest, we would wastefully perform a read on each and every order in the database. In a large system with possibly thousands or even millions of entries, this strategy is completely untenable. Instead, we provide methods to target our searches for orders, which in turn allows our data source to optimize the logic used to retrieve entries from its database.
Although we will use an in-memory database in our system, we will include this specialized retrieval method in order to maintain a consistent interface with clients. Although we internally know that the data source is an in-memory database, any clients should not have to change depending on our internal data source implementation.
Since we will be using an in-memory persistence scheme, we cannot rely on a database to provide a new ID for each domain class that we persist. Instead, we will need to create an ID generator that will provide us with unique IDs for each of the Order
s we will persist. Our implementation of this generator is as follows:
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class IdGenerator {
private AtomicLong nextId = new AtomicLong(1);
public long getNextId() {
return nextId.getAndIncrement();
}
}
Although our ID generator is very simple, there are some important aspects that require special attention. First, we have marked our generator as a Spring component using the @Component
annotation. This annotation allows our Spring boot application to recognize (through component scanning) our generator class as an injectable component. This will allow us to use the@Autowired
annotation in other classes and have our component injected without having to create the generator using the new
operator. For more information on Spring DI and autowiring, see the Spring Reference Document for Inversion of Control (IoC) Container.
We have also denoted that the scope of the generator (using the @Scope
annotation) is PROTOTYPE
, which ensures that a new object is created each time is it autowired. By default, when Spring injects an autowired dependency, it treats each component as a singleton, injecting the same object into each autowired location (within the same context). Using this default logic would result in each caller receiving the same generator, which would result in inconsistent ID generation.
For example, suppose we inject an ID generator into two data sources. If the same generator is used, if source one gets an ID, the resulting ID will be 1. If source two then requests an ID, the resulting ID will then be 2. This is contrary to our desired scheme, where each data source starts with an ID of 1 and increments only when a new object is created within that data source (the IDs can be differentiated based on the type of the object, i.e. "this is order 1" and "this is user 1"). Thus, the first object created in data source one should have an ID of 1, while the first object created in data source two should have an ID of 1 as well. This can only be accomplished if we have different ID generator objects, hence the prototype scoping of our ID generator class.
A second point is the use of AtomicLong
to store our next ID. It may be tempting to use a simple long to store the next ID, but this could lead to a very nuanced situation: What if two different calls are made to generate an ID? Since we are working within the realm of a concurrent web application, if we forgo any type of synchronization logic, we would run into a classic race condition, where two identical IDs may be produced. In order to eliminate these difficult-to-debug issues, we use the built-in concurrency mechanisms provided by Java. This ensures that even if two callers request IDs at the same time, each will be unique and the consistency of the next ID will be maintained.
With our ID generator in place, we are now ready to implement our data source. Since much of the in-memory logic is common for all types of objects, we will create an Abstract Base Class (ABC) that contains the core logic for managing our persisted objects (note that Spring uses the nomenclature Repository to mean a data source, and hence we follow the same convention):
public abstract class InMemoryRepository<T extends Identifiable> {
@Autowired
private IdGenerator idGenerator;
private List<T> elements = Collections.synchronizedList(new ArrayList<>());
public T create(T element) {
elements.add(element);
element.setId(idGenerator.getNextId());
return element;
}
public boolean delete(Long id) {
return elements.removeIf(element -> element.getId().equals(id));
}
public List<T> findAll() {
return elements;
}
public Optional<T> findById(Long id) {
return elements.stream().filter(e -> e.getId().equals(id)).findFirst();
}
public int getCount() {
return elements.size();
}
public void clear() {
elements.clear();
}
public boolean update(Long id, T updated) {
if (updated == null) {
return false;
}
else {
Optional<T> element = findById(id);
element.ifPresent(original -> updateIfExists(original, updated));
return element.isPresent();
}
}
protected abstract void updateIfExists(T original, T desired);
}
The basic premise of this InMemoryRepository
is simple: Provide a mechanism for all five of the previously enumerated persistence methods for any object that implements the Identifiable
interface. This is where the importance of our Identifiable
interface is made apparent: Our in-memory repository knows nothing about the objects it is storing, except that it can get and set the ID of those objects. Although we have created a dependency between the data source and domain layers, we have made that dependency very weak, where the data source layer only depends on an interface (rather than an abstract or concrete class that contains executable code) with only two methods. The fact that this interface is very unlikely to change also ensures that any changes to the domain layer will be unlikely to affect the data source layer.
As previously stated, our ID generator is capable of being injected into other classes, and we have done so using the @Autowired
annotation. In addition, we have used a synchronized list to store our managed objects. For the same reason as the AtomicLong
in our ID generator, we must ensure that we do not allow for a race condition if multiple callers try to perform concurrent operations on our data source.
While each of the operations is simple in their implementation (due to the simple in-memory design), there is one piece of information that requires knowledge of the stored object to perform: an update if the object exists. As previously expressed, our domain objects do not have enough knowledge on how to perform an update on themselves, therefore, it is the responsibility of our data source to perform this update.
Since this operation requires information we do not have, we mark it as abstract and require that concrete subclasses provide an implementation of this template method. In our case, we have only one subclass:
@Repository
public class OrderRepository extends InMemoryRepository<Order> {
protected void updateIfExists(Order original, Order updated) {
original.setDescription(updated.getDescription());
original.setCostInCents(updated.getCostInCents());
original.setComplete(updated.isComplete());
}
}
This concrete class simply provides an implementation for updating all non-ID fields of an Order
. We have also decorated our class with the @Repository
annotation, which is the same as the@Component
annotation (allows our class to be injected into other classes by the Spring DI framework) but also includes additional database-related capabilities. For more information, see the Javadocs for the @Repository annotation.
Although we were required to account for synchronization and DI capabilities, our entire data source layer consists of three classes and less than 70 lines of code (not counting package and import declarations). This is where the power of using a framework such as Spring comes to the forefront: many of the common tasks and conventions in web application development have been codified and made as simple as decorating classes and fields with the correct annotations. With the completion of our data source layer, we are ready to implement the final layer of our RESTful web service: the presentation layer.
Opinions expressed by DZone contributors are their own.
Comments