5.0 Practical Lab I: Constructing a Foundational SOA Application
To fully appreciate the principles of decomposition inherent in microservices, it is essential first to understand and build the type of service-oriented, yet monolithic, application from which MSA evolves. This lab will guide you through the construction of a complete CRUD (Create, Read, Update, Delete) application using a traditional Service-Oriented Architecture (SOA) approach. This will provide a baseline for understanding the architectural shift in the subsequent lab.
5.1 System Configuration and Project Setup
This lab requires the NetBeans IDE and the Maven build automation tool. We will create a new Maven project using a specific archetype for building RESTful web services with the Jersey framework. This setup process automatically downloads the necessary dependencies, streamlining the creation of our service.
The setup process involves the following steps:
- Open the NetBeans IDE and navigate to File -> New Project.
- In the dialog box, select Maven as the category and Project from Archetype as the project type, then click Next.
- In the “Select Archetype” screen, search for jersey-archType-Webapp(2.16) in the search box. You may need to check the “Show Older” checkbox to find it.
- Select the appropriate JAR from the list and click Next.
- Provide a project name (e.g., UserProfile), a Group ID (e.g., com.tutorialspoint), and a package name. Click Finish.
- NetBeans will create the project structure. You can inspect the Dependencies folder to see that Maven has automatically included all the necessary JAR files for building the application.
5.2 Building the Application Components
Our application will consist of four main Java classes: a data model (POJO), an in-memory database simulator, a service layer for business logic, and a resource layer to expose the service via HTTP.
- The POJO Model ()
This class is a Plain Old Java Object (POJO) that serves as the data model for a user profile. It defines the data structure for our application’s core entity.
package com.tutorialspoint.userprofile.Model;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class UserProfile {
private long ProId;
private String FName;
private String LName;
private String Add;
public UserProfile(){}
public UserProfile(long Proid, String Fname, String Lname,String Add) {
this.ProId = Proid;
this.FName = Fname;
this.LName = Lname;
this.Add = Add;
}
public long getProId() { return ProId; }
public void setProId(long ProId) { this.ProId = ProId; }
public String getFName() { return FName; }
public void setFName(String FName) { this.FName = FName; }
public String getLName() { return LName; }
public void setLName(String LName) { this.LName = LName; }
public String getAdd() { return Add; }
public void setAdd(String Add) { this.Add = Add; }
}
Code Analysis:
- @XmlRootElement: This JAXB (Java Architecture for XML Binding) annotation is critical. It declares that this Java object can be converted to and from an XML representation. When our REST service sends or receives user profile data, this annotation enables the Jersey framework to automatically handle the serialization and deserialization process.
- Fields and Methods: The class defines private fields to encapsulate the user’s data (ProId, FName, etc.) and provides public getter and setter methods. This follows standard JavaBean conventions, making the object easy to use with various Java frameworks.
- The In-Memory Database ()
To maintain simplicity and focus on the service architecture, this application will not use a real database. Instead, this class simulates an in-memory data store using a static HashMap.
package com.tutorialspoint.userprofile.DAO;
import com.tutorialspoint.userprofile.Model.UserProfile;
import java.util.HashMap;
import java.util.Map;
public class DatabaseClass {
private static Map<Long,UserProfile> messages = new HashMap<>();
public static Map<Long,UserProfile> getUsers() {
return messages;
}
}
Code Analysis:
- private static Map<Long,UserProfile> messages: A HashMap is declared to store UserProfile objects, using the profile ID (Long) as the key. The static keyword is essential here; it ensures that only one instance of this map exists for the entire application, regardless of how many times DatabaseClass is accessed. This effectively creates a singleton data store that persists for the lifetime of the application.
- public static Map<Long,UserProfile> getUsers(): This static method acts as a global access point to the “database.” Any part of our application can call this method to get a reference to the same, shared map instance.
- The Business Logic Layer ()
This service class contains the core business logic for all CRUD operations. It acts as an intermediary between the web-facing resource layer and the data access layer (DatabaseClass).
package com.tutorialspoint.userprofile.service;
import com.tutorialspoint.userprofile.DAO.DatabaseClass;
import com.tutorialspoint.userprofile.Model.UserProfile;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ProfileService {
private Map<Long,UserProfile> userprofiles = DatabaseClass.getUsers();
public ProfileService() {
UserProfile m1 = new UserProfile(1L,”Tutorials1″,”Point1″,”TutorialsPoint.com”);
UserProfile m2 = new UserProfile(2L,”Tutorials2″,”Point2″,”TutorialsPoint.com2″);
UserProfile m3 = new UserProfile(3L,”Tutorials3″,”Point3″,”TutorialsPoint.com3″);
UserProfile m4 = new UserProfile(4L,”Tutorials4″,”Point4″,”TutorialsPoint.com4″);
userprofiles.put(1L, m1);
userprofiles.put(2L, m2);
userprofiles.put(3L, m3);
userprofiles.put(4L, m4);
}
public List<UserProfile> getAllProfile() {
return new ArrayList<>(userprofiles.values());
}
public UserProfile getProfile(long id) {
return userprofiles.get(id);
}
public UserProfile addProfile(UserProfile userProfile) {
userProfile.setProId(userprofiles.size() + 1);
userprofiles.put(userProfile.getProId(), userProfile);
return userProfile;
}
public UserProfile UpdateProfile(UserProfile userProfile) {
if(userProfile.getProId() <= 0) {
return null;
}
userprofiles.put(userProfile.getProId(), userProfile);
return userProfile;
}
public void RemoveProfile(long id) {
userprofiles.remove(id);
}
}
Code Analysis:
- constructor: The constructor populates the in-memory database with four initial sample data entries when a ProfileService object is created. This ensures we have data to test with as soon as the application starts.
- CRUD Methods:
- getAllProfile(): Retrieves all UserProfile objects from the map’s values and returns them as a List.
- getProfile(long id): Retrieves a single profile from the map using its ID as the key.
- addProfile(UserProfile userProfile): Adds a new profile. It generates a simple new ID based on the current size of the map and then inserts the new profile.
- UpdateProfile(UserProfile userProfile): Updates an existing profile by overwriting the entry in the map that has the same ID.
- RemoveProfile(long id): Deletes a profile from the map using its ID.
- The Resource Layer ()
This class uses JAX-RS (Java API for RESTful Web Services) annotations to expose the business logic in ProfileService as a web service accessible via HTTP. It defines the public API contract for our application.
package com.tutorialspoint.userprofile.Resource;
import com.tutorialspoint.userprofile.Model.UserProfile;
import com.tutorialspoint.userprofile.service.ProfileService;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path(“/Profile”)
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
public class ProfileResource {
ProfileService messageService = new ProfileService();
@GET
public List<UserProfile> getProfile() {
return messageService.getAllProfile();
}
@GET
@Path(“/{ProID}”)
public UserProfile getProfile(@PathParam(“ProID”)long Id) {
return messageService.getProfile(Id);
}
@POST
public UserProfile addProfile(UserProfile profile) {
return messageService.addProfile(profile);
}
@PUT
@Path(“/{proID}”)
public UserProfile UpdateProfile(@PathParam(“proID”)long Id,UserProfile UserProfile) {
UserProfile.setProId(Id);
return messageService.UpdateProfile(UserProfile);
}
@DELETE
@Path(“/{ProID}”)
public void deleteProfile(@PathParam(“ProID”)long Id) {
messageService.RemoveProfile(Id);
}
}
Code Analysis:
- @Path(“/Profile”): This class-level annotation maps this entire resource to the URL path /Profile. All method paths are relative to this base path. This is fundamental to JAX-RS for routing HTTP requests to the correct Java class.
- @Consumes and @Produces: These annotations define the API’s contract regarding data formats. They specify that the methods in this class will accept (@Consumes) and return (@Produces) data formatted as APPLICATION_XML.
- HTTP Method Annotations (@GET, @POST, etc.): These annotations map Java methods to specific HTTP verbs. For example, an HTTP GET request will be handled by a method annotated with @GET.
- @Path(“/{ProID}”) and @PathParam: The @Path annotation at the method level defines a sub-path that includes a variable segment (e.g., {ProID}). The @PathParam annotation is then used to inject the value from that URL segment into a method argument. For example, a GET request to /Profile/1 will trigger the getProfile(long Id) method, with the value 1 passed as the Id parameter. Each method in this class acts as a simple delegate, calling the corresponding business logic method in the ProfileService instance.
5.3 Deployment and Testing
After building the project, run it on a server. You can then access the main endpoint at http://localhost:8080/UserProfile/webapi/Profile. The browser should display an XML representation of all the user profiles in the in-memory database.
To test the different methods, a tool like Postman is required:
- @GET: A GET request to the base URL will fetch all profiles. A GET request to /Profile/{id} will fetch a specific profile.
- @POST: To create a new user, send a POST request to /Profile with the user’s data in XML format in the request body. The server will respond with the newly created user profile, including its server-generated ID.
- @PUT: To update a user, send a PUT request to /Profile/{id}. The ID in the URL specifies which profile to update, and the XML data in the body contains the new information.
5.4 Architectural Analysis and Decomposition
The application we have just built is a classic example of a monolithic, service-oriented application. Although it exposes its functionality as a service, the internal structure is tightly coupled. The ProfileResource.java class acts as a single actor responsible for all four CRUD functionalities. This single point of entry creates a development bottleneck and makes the system less resilient, directly illustrating the theoretical challenges of monolithic design discussed earlier. A change to the ‘add’ logic requires redeploying the code that handles ‘delete’ and ‘update’ as well.
To move towards a microservice architecture, we must first conceptually decompose this monolithic structure. According to the core principles of MSA, one actor should not be responsible for so many distinct business tasks. We can reimagine the architecture with distributed responsibilities:
- End User: The client who initiates a request.
- Application Controller: A lightweight routing layer that receives the request and forwards it to the appropriate manager.
- Resource Manager: A set of specialized components, each responsible for a single job. For instance, there would be an AddUserManager, a DeleteUserManager, and an UpdateUserManager.
In this decomposed model, the single, monolithic responsibility of the ProfileResource class is distributed among different, more focused classes. The Application Controller would receive a POST request and forward it to the AddUserManager, while a DELETE request would be routed to the DeleteUserManager. This conceptual breakdown isolates responsibilities and sets the stage for a true microservice implementation.
This lab has established a baseline SOA application. The next logical step is to move from this conceptual decomposition to a hands-on implementation of a true microservice application that consumes other independent services to perform its work.