Click here to Skip to main content
15,885,777 members
Articles / Hosted Services / Azure
Article

Modernizing Java Apps and Data on Azure - Part Six: Becoming Cloud Native

Rate me:
Please Sign up or sign in to vote.
2.00/5 (1 vote)
15 Apr 2022CPOL10 min read 2.4K   18   2  
How to begin the legacy app’s functionality into Azure function-based microservices
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.

This article is a sponsored article. Articles such as these are intended to provide you with information on products and services that we consider useful and of value to developers

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.

Image 1

Click Next and fill in the Maven data.

Image 2

Verify the project name and directory for the project and click Finish to create it.

Image 3

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.

XML
<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:

Java
/**
 * This function listens at endpoint "/vets/{id}/visits?startDate=2022-01-01&endDate=2022-02-01".
 */
@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:

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:

Java
try
{
  log.info("Connecting to the database");
  connection =
       DriverManager.getConnection(properties.getProperty("url"),
         properties);

   // Get the visits for this vet
   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:

Java
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.

Java
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"));
  //... other setters here....

  visitList.add(visit);
}

// Error handling and debugging code removed

// return JSON from to the client
// Generate document
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:

Image 4

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:

Image 5

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:

Image 6

Select your authentication method. For this demonstration, select OAuth 2.0:

Image 7

Work through the Azure sign-in process. Back on the main screen, right-click the project and then select Azure/Deploy to Azure Functions:

Image 8

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.

Image 9

In the main window, click Run to start the process. When the deployment completes, you’ll see this:

Image 10

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):

Image 11

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:

Java
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:

Java
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:

HTML
<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.

Image 12

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:

HTML
<input th:replace="~{fragments/inputField :: input ('Start Date', 'startDate', 'date')}"/>

The vet list is the same as that added to the createOrUpdateVisitForm.html form:

HTML
<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:

HTML
<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:

Java
@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.

Java
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.

Java
@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:

Java
@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.

Java
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:

Image 13

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.

This article is part of the series 'Modernizing Java Apps and Data on Azure View All

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --