This is Part 6 of a 6-part series that demonstrates how to take a monolithic Java application and gradually modernize both the application and its data using Azure tools and services. This article examines how to improve application scalability and maintenance and takes further steps toward becoming truly cloud-native with cloud-hosted, decoupled services.
Monolithic applications provide tightly coupled services for highly efficient operation. Before multi-gigabit broadband networks, we had to construct applications this way to optimize performance. However, now that they have high-bandwidth public networks, we can break applications into decoupled services that aggregate data from one or many services.
Decoupling services provides a “separation of concerns” that allows each service to focus on only one aspect of the solution. As a result, each service is lightweight and can start, stop, or change without affecting any other service. These secure, decoupled services aggregated using lightweight interfaces over a public network form the basis of cloud-native architecture.
Containerizing the PetClinic
application improved certain aspects, but its monolithic nature still requires regression testing after making changes. Now that the data is in the cloud and there’s an implementation available that can handle the volume, the monolith can be separated into decoupled component services, easing maintenance and the development process.
This demonstration will extend PetClinic
to use the new capability added in the previous article. It must support two use cases:
- Each vet needs a list of visits within a time range to schedule accordingly and ensure that they have the required resources.
- To schedule a new appointment, the application must show whether the veterinarian is available.
A single report can support both use cases. The query needed to produce a report of scheduled visits has to know which veterinarian is the subject of the search and the period of interest.
RESTful APIs provide a simple interface for calling an API using a URL hosted by a web service. Azure Functions offer lightweight web hosts that launch on demand and are especially suited to hosting event handlers. They support several types of events, but since we’re implementing a RESTful API, we’ll trigger an event based on the route in a URL and the HTTP request method.
Designing RESTful APIs is an important topic worth special consideration, but you can gain some crucial insights from the RESTful web API design article. Following that article, the APIs you’ll implement use this pattern:
GET /vets/{id}/visits --> lists all visits for a vet
GET /vets/{id}/visits?startDate=x&endDate=y --> same but filtered to the ranges
An example of the URLs for these APIs is:
http://localhost:7071/vets/7/visits?startDate=01-01-2010&endDate=01-01-2020
Once you have the data service, you need a consumer to call the API and show the report. You implement this with a new HTML page in PetClinic
.
This article doesn’t discuss the topic of securing Azure Functions. You can find an overview of features to do this in this article.
Implementing the Function
Before you can implement this new feature, there are a few prerequisites:
Create the Project
First, make a new project in IntelliJ to create the Azure Function for an HTTP Trigger.
Click Next and fill in the Maven data.
Verify the project name and directory for the project and click Finish to create it.
Add Dependencies to the POM.XML File
You can save time if you add these dependencies to the pox.xml file before writing the code. The first set loads the Java Database Connectivity (JDBC) driver for PostgreSQL and the second set loads the Jackson Object Mapper. You’ll see where to use these when we discuss the getVisitsByVetReport
function.
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>LATEST</version>
</dependency>
Implement the Function Class
To refactor the class name (and the file name to match), you must change it to VisitByVetFunction
and change the package to org.petclinic.functions
. You may implement additional functions in the future, so this is an effective means of separating them from your supporting classes.
The function declaration in the code below sets several vital parameters. It sets the trigger event type, which is HTTP Trigger, as well as:
- The methods to which this trigger applies (in this case,
GET
) - Any authorization requirements
- The route that triggers this event
It also sets the binding pattern that extracts the vet_id
from the route:
@FunctionName("VisitsByVet")
public HttpResponseMessage run(
@HttpTrigger(
name = "req",
methods = {HttpMethod.GET},
authLevel = AuthorizationLevel.ANONYMOUS,
route = "vets/{id}/visits")
HttpRequestMessage<Optional<String>> request,
@BindingName("id") String vetId,
final ExecutionContext context)
Most of the remaining code in the function handles parsing the parameters from the query string. Then, it connects to the database and calls a function to get the report. You can review the full implementation in the VisitByVetFunction.java file in the attached source code.
Implement the Database Connection
This application uses standard JDBC methods. There are many ways to implement functions like this, but while you must do a little extra work to implement JDBC, it’s quite straightforward and readily adaptable to your preferred environment.
The code to handle the database connection is in three places. The properties are in the applications.properties file. You load the properties in the static block at the top of the VisitByVetFunction
class and in the try
/catch
block near the end of that function.
The JDBC connection values in the application.properties file are:
url=jdbc:postgresql://c.pet-clinic-demo-group.postgres.database.azure.com:5432/citus?ssl=true&sslmode=require&user=petClinicAdmin
username=citus@petClinicAdmin
password=P@ssword
Here’s the static
block from VisitByVetFunction.java:
static
{
System.setProperty("java.util.logging.SimpleFormatter.format",
"[%4$-7s] %5$s %n");
log = Logger.getLogger(VisitByVetFunction.class.getName());
try
{
properties.load(VisitByVetFunction.class.
getClassLoader().getResourceAsStream("application.properties"));
}
catch(Exception e)
{
log.severe("Could not load the application properties.");
throw new RuntimeException(e.getMessage());
}
}
Then, within the VisitByVetFunction
, once you have the properties, you can create a connection:
try
{
log.info("Connecting to the database");
connection =
DriverManager.getConnection(properties.getProperty("url"),
properties);
jsonResults = Reports.getVisitsByVetReport(connection, id,
reportStart, reportEnd);
log.info("Closing database connection");
connection.close();
}
Ensure that you close the connection to prevent resource leaks.
Implement the DTO
The data transfer object is in Visit.java. It contains the fields below with associated getters and setters:
private String vetFirstName;
private String vetLastName;
private String petName;
private Date visit_date;
private String description;
private String ownerFirstName;
private String ownerLastName;
Implement the Report Class
The getVisitsByVetReport
function that the Azure Function calls is in VisitsByVetReport.java. The core of the function is below, with some code omitted to emphasize the structure.
The function prepares and executes the query, then converts the results to an ArrayList
of Visit
objects. You use the Jackson Object Mapper to convert the ArrayList
to a JSON object.
PreparedStatement readStatement = connection.prepareStatement(query);
ResultSet resultSet = readStatement.executeQuery();
ArrayList<Visit> visitList = new ArrayList<Visit>();
while(resultSet.next())
{
Visit visit = new Visit();
visit.setVetFirstName(resultSet.getString("vet_first_name"));
visitList.add(visit);
}
ObjectMapper mapper = new ObjectMapper();
jsonResults = mapper.writeValueAsString(visitList);
Debug Locally
The command-line tools for the Azure Functions support complete local debugging of Azure Functions. This package is fully compatible with the Azure platform, so you don’t have to deploy your application to the cloud to test it.
When you run the function (as opposed to debugging it), you’ll see this on the console:
The last line in this image provides the full URL that triggers your function. Verify that this is correct. Then open a browser page with it to verify that the code executes correctly. For example:
Deploy To Azure
When the function is working, you can deploy it to Azure with just a few clicks. First, log in to Azure by selecting Tools/Azure/Azure Sign In:
Select your authentication method. For this demonstration, select OAuth 2.0:
Work through the Azure sign-in process. Back on the main screen, right-click the project and then select Azure/Deploy to Azure Functions:
Click the plus sign at the right side of the Function field to show the Instance Details. Set the Name to any name you like and set the Platform to Windows-Java-11. Then click OK.
In the main window, click Run to start the process. When the deployment completes, you’ll see this:
Open a browser to retrieve the displayed URL with the same vet_id
you provided previously. You should see the same results (not all rows appear below):
Update PetClinic
You’ve built and deployed the function to implement the API, so now it’s time to integrate it into the PetClinic
application by making the following changes:
- Create the
ReportVisit
Data Transfer Object (DTO). - Create the
VisitReportController
class. - Create the
VisitRequest
class to support the new web page. - Create the
VisitList
web page. - Update the layout.html fragment to add a link to the
Visits
page. - Add the URL to the API in the application.properties file.
The ReportVisit Data Transfer Object
The fields in the ReportVisit
Data Transfer Object (DTO) match the data returned by the API function and the class provides the getters and setters needed to deserialize the data:
private String vetFirstName;
private String vetLastName;
private String petName;
private String description;
private String ownerFirstName;
private String ownerLastName;
private LocalDate visitDate;
The only significant line of code in the class translates the date types:
public void setVisitDate(Date date) {
this.visitDate =
date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
The Layout.html Fragment
When users need a list of visits for a veterinarian, they click the Visits link in the page header, which the layout.html fragment provides. The change you made was to add a list item to the list of links:
<li th:replace="::menuItem ('/visits.html','visits','visits by veterinarian',
'th-list','Visits')">
<span class="fa fa-th-list" aria-hidden="true"></span>
<span>Visits</span>
</li>
The VisitList Web Page
Clicking the Visits link sends a GET
request to the PetClinic
application. The application returns a page containing a form followed by a list of visits. The user selects a veterinarian from the list, provides a start and end date for the search, and clicks Get Visits. This triggers the form action: a POST
request sent to /visits.html.
Input fragments in inputField.html implement the date fields. The PetClinic
application provides this and implementations of these fields appear in several other web pages:
<input th:replace="~{fragments/inputField :: input ('Start Date', 'startDate', 'date')}"/>
The vet list is the same as that added to the createOrUpdateVisitForm.html form:
<td rowspan="2">
<div class="form-group">
<label class="col-sm-2 control-label">Vet</label>
<div class="col-sm-10">
<select class="form-control" th:object="${vetList}" id="vetId" name="vetId">
<option th:each="vet : ${vetList}" th:value="${vet.id}"
th:text="${vet.firstName}+' '+${vet.lastName}"
th:selected="${vet.id==visitRequest.vetId}"/>
</select>
<span class="fa fa-ok form-control-feedback" aria-hidden="true"/>
</div>
</div>
</td>
You could use the input fragment, which can provide a list, but it doesn’t support the selection logic needed. So, you must implement the list directly.
The list of visits returned by the POST
request appears in a table, implemented as follows:
<table width=100% id="reportVisits" class="table table-striped">
<thead>
<tr>
<th>Pet Name</th>
<th>Date</th>
<th>Description</th>
<th>Owner</th>
</tr>
</thead>
<tbody>
<tr th:each="reportVisit : ${visitList}">
<td th:text="${reportVisit.petName}"></td>
<td th:text="${reportVisit.visitDate}"></td>
<td th:text="${reportVisit.description}"></td>
<td th:text="${reportVisit.ownerFirstName + ' ' +
reportVisit.ownerLastName}">
</td>
</tr>
</tbody>
</table>
Update the application.properties File
To call the new API, the VisitReportController
needs the URL of the API function found in the application.properties file:
# Azure Function API
azure.visit.report.function.url=https://app-petclinicfunctions-220223181608.azurewebsites.net/
vets/{id}/visits?startDate={startDate}&endDate={endDate}
The class retrieves this value using the @Value
annotation:
@Value("${azure.visit.report.function.url}")
private String azureVisitReportFunctionUrl;
The VisitReportController Class
As discussed above, this controller class responds to GET
and POST
requests to the /visits.html URL. The controller receives an instance of the VetRepository
when created. The @GetMapping
identifies the GET
handler, which builds the model and displays the initial web page.
public VisitReportController(VetRepository vets) {
this.vets = vets;
}
@GetMapping("/visits.html")
public String getVisitReport(@RequestParam(defaultValue = "1") int page, Model model) {
VisitRequest visitRequest = new VisitRequest(1, LocalDate.now(), LocalDate.now());
model.addAttribute("visitRequest", visitRequest);
model.addAttribute("vetList", vets.findAll());
model.addAttribute("visitList", null);
return "visits/visitList";
}
The @PostMapping
marks the handler for the form action. Any errors in the request cause the return of the original page. Otherwise, a RestTemplate
makes a GET
request to the API to extract the dates and veterinarian ID from the form data.
To prepare the response to the call, you need to add the original VisitRequest
object and a new VetList
to the model object. The Jackson JSON Object Mapper converts the JSON from the API response into a list (or ArrayList
) of ReportVisit
objects, which it adds to the model for the user.
@PostMapping("/visits.html")
public String postVisitReport(@Valid VisitRequest visitRequest,
BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("visitRequest", visitRequest);
model.addAttribute("vetList", vets.findAll());
model.addAttribute("visitList", null);
return "visits/visitList";
}
LocalDate startDate = visitRequest.getStartDate();
LocalDate endDate = visitRequest.getEndDate();
int vetId = visitRequest.getVetId();
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(azureVisitReportFunctionUrl,
String.class, vetId, startDate, endDate);
model.addAttribute("visitRequest", visitRequest);
model.addAttribute("vetList", vets.findAll());
if (result == null) {
ArrayList<ReportVisit> visitList = new ArrayList<ReportVisit>();
model.addAttribute("visitList", visitList);
}
else {
try {
ObjectMapper mapper = new ObjectMapper();
ReportVisit[] visitArray = mapper.readValue(result,
ReportVisit[].class);
List<ReportVisit> visitList = Arrays.asList(visitArray);
model.addAttribute("visitList", visitList);
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
return "visits/visitList";
}
The VisitRequest Class
The VisitRequest
class packages the user-provided model data and includes default or current settings to the web page. The class would be trivial, but you must convert date values from the form that Thymeleaf expects to the form the API function uses. To do this, add these annotations to tell Thymeleaf to expect the International Organization for Standardization (ISO) format:
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate startDate;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate endDate;
Additionally, some setters can accept either a LocalDate
or the date as a string
to handle serialization.
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public void setStartDate(String startDate) {
this.startDate = LocalDate.parse(startDate);
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
public void setEndDate(String endDate) {
this.endDate = LocalDate.parse(endDate);
}
With these changes, the application can now provide the new report:
Summary
This article demonstrated how to use incremental improvements to convert a monolithic legacy application into a complete cloud application. Cloud-native applications are an aggregation of decoupled services that are available using lightweight remote procedure calls. These are often implemented using RESTful APIs. In contrast, monolithic applications use tightly coupled services on a single platform. We can convert a legacy application to a cloud application by replacing application-provided services with new remote services.
This migration process assumes that the new services can access the application’s data, so the series started by migrating the data to the cloud. After liberating the data from the application, you were able to add a new service to retrieve visit data using APIs.
One additional step toward becoming cloud native is to extend create a new API to replace the existing method of creating new visits. This might appear as follows:
POST /vets/{id}/visits
POST BODY: pet_id, Visit timestamp
You would add a new function in VisitByVetFunction.java to handle it, and you would have full access to the data services that support the existing function. You could then modify PetClinic
to use this, rather than the JPA implementation with full knowledge of the database structure.
As you continue creating a cloud-native application, you can replace the PostgreSQL Citus relational database with a NoSQL database. This provides significant operational savings and supports application scaling. The APIs isolate the application from the database and the application will not write queries directly to the database. So, while change is still significant, the scope of the changes to back-end services is limited.
The code associated with this article provides a platform for continuing the investigation of the migration process. A fully cloud-native version of PetClinic
is available on GitHub for your exploration.
To continue learning about cloud-native architecture and how to transform your monolith into a set of scalable services, this Microsoft article introduces the topic and provides additional resources with comprehensive information. With these resources, you can begin planning the future of your application migration.
To see how four companies transformed their mission-critical Java applications to achieve better performance, security, and customer experiences, check out the e-book Modernize Your Java Apps.