Azure 上的 Java 应用和数据现代化 - 第六部分:成为云原生





2.00/5 (1投票)
如何将遗留应用程序的功能迁移到 Azure 基于函数的微服务。
单体应用程序提供高度耦合的服务以实现高效运行。在多千兆宽带网络出现之前,我们必须以这种方式构建应用程序以优化性能。然而,现在有了高带宽的公共网络,我们可以将应用程序分解为解耦的服务,这些服务可以聚合来自一个或多个服务的数据。
解耦服务提供了“关注点分离”,使每个服务只能专注于解决方案的一个方面。因此,每个服务都是轻量级的,可以独立启动、停止或更改,而不会影响任何其他服务。这些通过公共网络上的轻量级接口聚合的安全、解耦的服务构成了云原生架构的基础。
容器化 PetClinic 应用程序在某些方面有所改进,但其单体性质在进行更改后仍然需要回归测试。现在数据已迁移到云端,并且有一个可以处理该数据的实现,单体应用程序可以分解为解耦的组件服务,从而简化维护和开发过程。
本次演示将扩展 PetClinic 以使用 上一篇文章 中添加的新功能。它必须支持两种用例:
- 每位兽医都需要一份在特定时间范围内的就诊列表,以便他们可以相应地安排日程并确保拥有所需的资源。
- 要安排新的预约,应用程序必须显示兽医是否可用。
一个单一的报告可以同时支持这两种用例。用于生成已安排就诊报告的查询需要知道要搜索的兽医以及感兴趣的时间段。
RESTful API 提供了一个简单的接口,可以通过 Web 服务托管的 URL 调用 API。Azure Functions 提供轻量级的 Web 主机,它们按需启动,特别适合托管事件处理程序。它们支持多种类型的事件,但由于我们正在实现 RESTful API,我们将根据 URL 中的路由和 HTTP 请求方法触发事件。
设计 RESTful API 是一个值得特别关注的重要主题,但您可以从 RESTful Web API 设计 文章中获得一些关键见解。遵循该文章,您将实现的 API 使用此模式:
GET /vets/{id}/visits --> lists all visits for a vet
GET /vets/{id}/visits?startDate=x&endDate=y --> same but filtered to the ranges
这些 API 的 URL 示例为:
https://:7071/vets/7/visits?startDate=01-01-2010&endDate=01-01-2020
一旦有了数据服务,您就需要一个消费者来调用 API 并显示报告。您将在 PetClinic 中使用新的 HTML 页面来实现这一点。
本文不讨论保护 Azure Functions 的主题。您可以在 这篇 文章中找到相关功能的概述。
实现 Function
在实现此新功能之前,需要满足一些先决条件:
- IntelliJ IDEA Community Edition
- Azure Function Core Tools v4
- Maven
创建项目
首先,在 IntelliJ 中创建一个新项目以创建用于 HTTP 触发器的 Azure Function。
点击 Next (下一步) 并填写 Maven 数据。
验证项目的名称和目录,然后点击 Finish (完成) 创建项目。
将依赖项添加到 POM.XML 文件
如果您在编写代码之前将这些依赖项添加到 pom.xml 文件中,可以节省时间。第一组加载 PostgreSQL 的 Java 数据库连接 (JDBC) 驱动程序,第二组加载 Jackson Object Mapper。当我们将讨论 getVisitsByVetReport 函数时,您将看到它们的使用位置。
<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>
实现 Function 类
要重构类名(以及与之匹配的文件名),您必须将其更改为 VisitByVetFunction,并将包更改为 org.petclinic.functions。将来您可能会实现其他函数,因此这是将它们与支持类分离的有效方法。
下面的代码中的函数声明设置了几个重要的参数。它将触发事件类型设置为 HTTP Trigger,以及:
- 此触发器适用的方法(在本例中为 GET)
- 任何授权要求
- 触发此事件的路由
它还设置了用于从路由中提取 vet_id 的绑定模式。
    /**
     * 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)
函数中剩余的大部分代码用于解析查询字符串中的参数。然后,它连接到数据库并调用一个函数来获取报告。您可以在附件的 源代码 的 VisitByVetFunction.java 文件中查看完整实现。
实现数据库连接
此应用程序使用标准的 JDBC 方法。有许多方法可以实现此类函数,但尽管您必须做一些额外的工作来实现 JDBC,但它非常直接,并且很容易适应您偏好的环境。
处理数据库连接的代码位于三个位置。属性在 application.properties 文件中。您可以在 VisitByVetFunction 类顶部的静态块中加载属性,并在该函数末尾附近的 try/catch 块中加载属性。
application.properties 文件中的 JDBC 连接值如下:
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 
这是 VisitByVetFunction.java 中的 static 块:
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());
    }
}
然后,在 VisitByVetFunction 中,一旦您获得了属性,就可以创建一个连接:
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();
 }
确保关闭连接以防止资源泄露。
实现 DTO
数据传输对象 (DTO) 位于 Visit.java 中。它包含以下字段以及关联的 getter 和 setter:
    private String vetFirstName;
    private String vetLastName;
    private String petName;
    private Date visit_date;
    private String description;
    private String ownerFirstName;
    private String ownerLastName;
实现 Report 类
Azure Function 调用 的 getVisitsByVetReport 函数位于 VisitsByVetReport.java 中。函数的关键部分如下,省略了一些代码以强调结构:
该函数准备并执行查询,然后将结果转换为 Visit 对象的 ArrayList。您使用 Jackson Object Mapper 将 ArrayList 转换为 JSON 对象。
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);
本地调试
Azure Functions 的命令行工具支持对 Azure Functions 进行完整的本地调试。此包与 Azure 平台完全兼容,因此您无需将应用程序部署到云端即可进行测试。
当您运行函数(而不是调试它)时,您将在控制台上看到此内容:
此图像中的最后一行提供了触发您的函数的完整 URL。验证其是否正确。然后,在浏览器中打开该 URL 以验证代码是否正确执行。例如:
部署到 Azure
当函数正常工作后,只需点击几下即可将其部署到 Azure。首先,通过选择 Tools/Azure/Azure Sign In (工具/Azure/Azure 登录) 来登录 Azure。
选择您的身份验证方法。本次演示选择 OAuth 2.0。
完成 Azure 登录过程。回到主屏幕,右键单击项目,然后选择 Azure/Deploy to Azure Functions (Azure/部署到 Azure Functions)。
点击 Function (函数) 字段右侧的加号以显示 Instance Details (实例详细信息)。将 Name (名称) 设置为您喜欢的任何名称,并将 Platform (平台) 设置为 Windows-Java-11。然后点击 OK (确定)。
在主窗口中,点击 Run (运行) 开始该过程。部署完成后,您将看到此内容:
打开浏览器,使用您之前提供的相同 vet_id 来检索显示的 URL。您应该看到相同的结果(下方未显示所有行):
更新 PetClinic
您已构建并部署了用于实现 API 的函数,现在是时候将其集成到 PetClinic 应用程序中,并进行以下更改:
- 创建 ReportVisit数据传输对象 (DTO)。
- 创建 VisitReportController类。
- 创建 VisitRequest类以支持新的网页。
- 创建 VisitList网页。
- 更新 layout.html 片段,添加一个指向 Visits页面的链接。
- 将 API 的 URL 添加到 application.properties 文件中。
ReportVisit 数据传输对象
ReportVisit 数据传输对象 (DTO) 中的字段与 API 函数返回的数据匹配,并且该类提供了反序列化数据所需的 getter 和 setter。
private String vetFirstName;
private String vetLastName;
private String petName;
private String description;
private String ownerFirstName;
private String ownerLastName;
private LocalDate visitDate;
类中唯一的显着代码行是转换日期类型。
public void setVisitDate(Date date) {
  this.visitDate =
       date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
Layout.html 片段
当用户需要兽医的就诊列表时,他们会点击页面标题中的 Visits (就诊) 链接,该链接由 layout.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>
VisitList 网页
点击 Visits (就诊) 链接会向 PetClinic 应用程序发送一个 GET 请求。应用程序返回一个页面,其中包含一个表单,后面跟着一个就诊列表。用户从列表中选择一位兽医,提供搜索的开始日期和结束日期,然后点击 Get Visits (获取就诊)。这将触发表单操作:一个发送到 /visits.html 的 POST 请求。
inputField.html 中的输入片段实现了日期字段。PetClinic 应用程序提供了此功能,并且这些字段的实现出现在其他几个网页中。
<input th:replace="~{fragments/inputField :: input ('Start Date', 'startDate', 'date')}"/>
兽医列表与添加到 createOrUpdateVisitForm.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>
您可以使用输入片段,它提供了列表,但它不支持所需的选择逻辑。因此,您必须直接实现列表。
POST 请求返回的就诊列表显示在一个表格中,实现如下:
<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>
更新 application.properties 文件
为了调用新的 API,VisitReportController 需要 API 函数的 URL,该 URL 位于 application.properties 文件中。
# Azure Function API
azure.visit.report.function.url=https://app-petclinicfunctions-220223181608.azurewebsites.net/
vets/{id}/visits?startDate={startDate}&endDate={endDate}
该类使用 @Value 注解检索此值。
@Value("${azure.visit.report.function.url}")
private String azureVisitReportFunctionUrl;
VisitReportController 类
如上所述,此控制器类响应对 /visits.html URL 的 GET 和 POST 请求。控制器在创建时接收 VetRepository 的实例。@GetMapping 标识 GET 处理程序,该处理程序构建模型并显示初始网页。
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";
}
@PostMapping 标记表单操作的处理程序。请求中的任何错误都会导致返回原始页面。否则,RestTemplate 会向 API 发送一个 GET 请求,以从表单数据中提取日期和兽医 ID。
为了准备对调用的响应,您需要将原始 VisitRequest 对象和一个新的 VetList 添加到模型对象中。Jackson JSON Object Mapper 将 API 响应中的 JSON 转换为 ReportVisit 对象列表(或 ArrayList),然后将其添加到模型中供用户使用。
@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";
}
VisitRequest 类
VisitRequest 类将用户提供的数据打包,并包含到网页的默认或当前设置。该类本应很简单,但您必须将 Thymeleaf 期望的日期值从表单转换为 API 函数使用的格式。为此,请添加这些注解,告知 Thymeleaf 期望 ISO (国际标准化组织) 格式:
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate startDate;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate endDate;
此外,一些 setter 可以接受 LocalDate 或 string 格式的日期,以处理序列化。
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);
}
通过这些更改,应用程序现在可以提供新的报告。
摘要
本文演示了如何通过增量改进将单体遗留应用程序转换为完整的云应用程序。云原生应用程序是使用轻量级远程过程调用(通常使用 RESTful API 实现)可用的解耦服务的聚合。相比之下,单体应用程序在单个平台上使用高度耦合的服务。我们可以通过用新的远程服务替换应用程序提供的服务来将遗留应用程序转换为云应用程序。
此迁移过程假定新服务可以访问应用程序的数据,因此该系列文章以将数据迁移到云端开始。在将数据从应用程序中解放出来后,您就可以添加一个新服务来通过 API 检索就诊数据。
向云原生迈出的另一个步骤是扩展并创建一个新 API 来替换现有的创建新就诊的方法。这可能如下所示:
POST /vets/{id}/visits
POST BODY: pet_id, Visit timestamp
您将在 VisitByVetFunction.java 中添加一个新函数来处理它,您将可以完全访问支持现有函数的数据服务。然后,您可以修改 PetClinic 来使用它,而不是使用完全了解数据库结构的 JPA 实现。
在继续创建云原生应用程序的过程中,您可以将 PostgreSQL Citus 关系数据库替换为 NoSQL 数据库。这可以节省大量运营成本并支持应用程序扩展。API 将应用程序与数据库隔离,应用程序不会直接向数据库编写查询。因此,尽管更改仍然很大,但后端服务的更改范围有限。
与本文相关的代码提供了一个继续研究迁移过程的平台。PetClinic 的完全云原生版本可在 GitHub 上供您探索。
要继续学习云原生架构以及如何将您的单体应用程序转换为可扩展的服务集,请参阅这篇 Microsoft 文章,它介绍了该主题并提供了额外的资源以及全面的信息。借助这些资源,您可以开始规划您的应用程序迁移的未来。
要了解四家公司如何成功地将他们任务关键型的 Java 应用程序转型以获得更好的性能、安全性和客户体验,请查阅电子书 Modernize Your Java Apps。















