vlambda博客
学习文章列表

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》将Web界面添加到Quarkus服务

Adding Web Interfaces to Quarkus Services

到目前为止,我们已经学习了如何使用 Quarkus 构建一个简单的 REST 应用程序,并涵盖了在 Kubernetes 环境中构建、测试和部署我们的应用程序应该采取的操作。

我们可以在这一点上停下来,对我们所取得的成就感到满意;但是,仍有许多里程碑需要达到。例如,我们没有使用任何 Web 界面来访问 Quarkus 服务。正如您将在本章中看到的,Quarkus 具有一些扩展功能,允许我们重用标准企业 API,例如 Servlet 和 Web 套接字。同时,您可以使用更轻量级的 JavaScript/HTML 5 框架作为服务的用户界面。我们将在本章中探讨这两种方法。

在本章中,我们将介绍以下主题

  • Adding web content to Quarkus applications
  • Running our application on Minishift
  • Adding enterprise web components to our application such as Servlet and WebSockets

Adding web content to Quarkus applications

在我们目前讨论的示例中,我们通过添加 RESTful 服务测试了 Quarkus 的 Web 服务器功能。在底层,Quarkus 使用以下核心组件来处理 Web 请求:

  • Vert.x Web server: It is the core web component in Quarkus delivering RESTful services as long as real-time (server push) web applications. We will discuss more in detail about Vert.x in Chapter 9, Unifying Imperative and Reactive with Vert.x of this book.
  • Undertow Web server: It is a flexible product, built by combining different small single-purpose handlers, that comes into play in Quarkus when delivering WebSocket applications.

如前所述,我们可以将静态 Web 内容(HTML、JavaScript、图像)添加到我们的应用程序中,方法是将它们包含在项目的 resources/META-INF/resources 文件夹下。在微服务风格的应用程序中拥有静态 Web 内容的目的是什么?事实上,静态内容可以在多种环境中使用,包括微服务。例如,我们可以为服务本身提供帮助页面。我们还可以将 Quarkus 与 Swagger UI 等现有框架混合搭配,以测试我们的 REST 端点,甚至无需编写复杂的用户界面。

在此前提下,我们将演示如何构建一个创建、读取、更新、删除 (CRUD) 应用程序,该应用程序使用 JSON 来消费和生成数据。然后,我们将使用由 基于 JavaScript 的 Web 框架 制成的瘦 Web 界面来丰富我们的应用程序。

Building a CRUD application

在本章的 GitHub 源文件夹中,您将找到两个示例。第一个位于 Chapter04/customer-service/basic 文件夹中,将在本节中讨论。我们建议您在继续之前将项目导入您的 IDE。

如果您看一下项目的结构,您会发现它由三个主要组件组成:

  1. First of all, there is a model class that records customer entries:
package com.packt.quarkus.chapter4;
 
public class Customer {
     private Integer id;
     private String name;
     private String surname;
 
     public Integer getId() {
         return id;
     }
 
     public void setId(Integer id) {
         this.id = id;
     }
 
     public String getName() {
         return name;
     }
 
     public void setName(String name) {
         this.name = name;
     }
 
     public String getSurname() {
         return surname;
     }
 
     public void setSurname(String surname) {
         this.surname = surname;
     }
 }

Customer 类是Customer 记录的最小定义。它被定义为应该存储在内存中的普通旧 Java 对象。

  1. Next, take a look at the CustomerRepository class, which contains the core functionalities that we'll use to manage our model:
package com.packt.quarkus.chapter4;
 
import javax.enterprise.context.ApplicationScoped;
import java.util.ArrayList;
import java.util.List;
 
 
@ApplicationScoped
public class CustomerRepository {
 
     List<Customer> customerList = new ArrayList();
     int counter;
 
     public int getNextCustomerId() {
         return counter++;
     }
 
     public List<Customer> findAll() {
         return customerList;
     }
 
     public Customer findCustomerById(Integer id) {
         for (Customer c:customerList) {
             if (c.getId().equals(id))  {
                 return c;
             }
         }
         throw new CustomerException("Customer not found!");
     }
 
     public void updateCustomer(Customer customer) {
         Customer customerToUpdate = 
          findCustomerById(customer.getId());
         customerToUpdate.setName(customer.getName());
         customerToUpdate.setSurname(customer.getSurname());
     }
 
     public void createCustomer(Customer customer) {
         customer.setId(getNextCustomerId());
         findAll().add(customer);
     }
 
     public void deleteCustomer(Integer customerId) {
         Customer c = findCustomerById(customerId);
         findAll().remove(c);
     }
}

如您所见,它只是一个存储库的普通实现,用作存储和检索我们数据的模式。在接下来的章节中,我们将添加其他功能,例如持久存储和异步行为。因此,最好从与服务无关的示例开始。

  1. The customer service is completed by the CustomerEndpoint class, which has the following implementation:
package com.packt.quarkus.chapter4;
 
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.util.List;
 
 
@Path("customers")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
public class CustomerEndpoint {
 
     @Inject CustomerRepository customerRepository;
 
     @GET
     public List<Customer> getAll() {
         return customerRepository.findAll();
     }
 
     @POST
     public Response create(Customer customer) {
         customerRepository.createCustomer(customer);
         return Response.status(201).build();
 
     }
 
     @PUT
     public Response update(Customer customer) {
         customerRepository.updateCustomer(customer);
         return Response.status(204).build();
     }
     @DELETE
     public Response delete(@QueryParam("id") Integer customerId) {
         customerRepository.deleteCustomer(customerId);
         return Response.status(204).build();
     }
 
} 

如您所见,CustomerEndpointCustomerRepository 类之上的一个瘦 REST 层,并包含每个 CRUD 操作的方法,其中将每个操作映射到适当的 HTTP 方法。使用这种方法时,整个应用程序有一个单一的 REST 路径 (/customers) 就足够了,因为 REST 引擎将根据 HTTP 请求方法调用适当的方法。

Adding a UI to our customer service

正如我们在第1章中提到的,介绍Quarkus Core Concepts,您可以在 src/main/resources/META-INF/resources 文件夹中包含静态资源,例如 HTML 页面、JavaScript、CSS 或图像。 index.html 页面在我们的项目中作为标记提供,如项目的层次结构所示:

$ tree src
 src
 ├── main
 │   ├── docker
 │   ├── java
 │   │   └── com
 │   │       └── packt
 │   │           └── quarkus
 │   │               └── chapter4
 │   │                   ├── CustomerEndpoint.java
 │   │                   ├── Customer.java
 │   │                   ├── CustomerRepository.java
 │   └── resources
 │       ├── application.properties
 │       └── META-INF
 │           └── resources
 │               ├── index.html 

为了连接到我们的 REST 端点,我们将在 index.html 页面的 head 部分包含一个名为 AngularJS 的 JavaScript 框架和一些 CSS 样式:

<link rel="stylesheet" type="text/css" href="stylesheet.css" media="screen" />
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>

此外,在 index.html 页面的 head 部分中,我们将包含 AngularJS 控制器,它包含一个可用于访问 REST 端点方法的函数。我们将 HTML 表单数据作为参数传递,我们将在下面讨论:

<script type="text/javascript"> var app = angular.module("customerManagement", []); angular.module('customerManagement').constant('SERVER_URL', '/customers'); //Controller Part app.controller("customerManagementController", function ($scope, $http, SERVER_URL) { //Initialize page with default data which is blank in this //example $scope.customers = []; $scope.form = { id: -1, name: "", surname: "" }; //Now load the data from server _refreshPageData(); //HTTP POST/PUT methods for add/edit customers $scope.update = function () { var method = ""; var url = ""; var data = {}; if ($scope.form.id == -1) { //Id is absent so add customers - POST operation method = "POST"; url = SERVER_URL; data.name = $scope.form.name; data.surname = $scope.form.surname; } else { //If Id is present, it's edit operation - PUT operation method = "PUT"; url = SERVER_URL; data.id = $scope.form.id; data.name = $scope.form.name; data.surname = $scope.form.surname; } $http({ method: method, url: url, data: angular.toJson(data), headers: { 'Content-Type': 'application/json' } }).then(_success, _error); }; //HTTP DELETE- delete customer by id $scope.remove = function (customer) { $http({ method: 'DELETE', url: SERVER_URL+'?id='+customer.id }).then(_success, _error); }; //In case of edit customers, populate form with customer // data $scope.edit = function (customer) { $scope.form.name = customer.name; $scope.form.surname = customer.surname; $scope.form.id = customer.id; }; /* Private Methods */ //HTTP GET- get all customers collection function _refreshPageData() { $http({ method: 'GET', url: SERVER_URL }).then(function successCallback(response) { $scope.customers = response.data; }, function errorCallback(response) { console.log(response.statusText); }); } function _success(response) { _refreshPageData(); _clearForm() } function _error(response) { alert(response.data.message || response.statusText); } //Clear the form function _clearForm() { $scope.form.name = ""; $scope.form.surname = ""; $scope.form.id = -1; } }); </script>
 </head>

对 AngularJS 的深入讨论超出了本书的范围。然而,简而言之,Angular 应用程序依赖于控制器来管理它们的数据流。每个控制器都接受 $scope 作为参数。该参数指的是控制器需要处理的模块或应用程序。

我们控制器的目的是使用不同的 HTTP 方法(GETPOSTPUT 访问我们的 REST 应用程序删除)。

index.html 页面的最后部分包含表单数据,可用于插入新客户和编辑现有客户:

<body ng-app="customerManagement" ng-controller="customerManagementController">
 <div class="divTable blueTable">
     <h1>Quarkus CRUD Example</h1>
     <h2>Enter Customer:</h2>
     <form ng-submit="update()">
         <div class="divTableRow">
             <div class="divTableCell">Name:</div>
             <div class="divTableCell"><input type="text" placeholder="Name" ng-model= "form.name" size="60"/></div>
         </div>
         <div class="divTableRow">
             <div class="divTableCell">Surname:</div>
             <div class="divTableCell"><input type="text" placeholder="Surname" ng-model="form.surname" size="60"/>
        </div>
         </div>
         <input type="submit" value="Save"/>
     </form>
     <div class="divTable blueTable">
         <div class="divTableHeading">
             <div class="divTableHead">Customer Name</div>
             <div class="divTableHead">Customer Address</div>
             <div class="divTableHead">Action</div>
         </div>
         <div class="divTableRow" ng-repeat="customer in customers">
             <div class="divTableCell">{{ customer.name }}</div>
             <div class="divTableCell">{{ customer.surname }}</div>
             <div class="divTableCell"><a ng-click="edit( customer )" class="myButton">Edit</a>
             <a ng-click="remove( customer )" class="myButton">Remove</a></div>
         </div>
     </div>
 </body>
</html>

现在我们已经完成了 index.html 页面,我们可以为我们的应用程序编写一个测试类。

Testing our application

在测试我们的应用程序之前,值得一提的是,quarkus-jsonb 依赖项已包含在此项目中,以便通过 REST 端点生成 JSON 内容并在测试类中以编程方式创建 JSON 对象。以下是我们在 pom.xml 文件中包含的依赖项:

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-jsonb</artifactId>
</dependency>

以下是我们的 CustomerEndpointTest 类,可用于验证 Customer 应用程序:

@QuarkusTest
public class CustomerEndpointTest {
 
     @Test
     public void testCustomerService() {
 
         JsonObject obj = Json.createObjectBuilder()
                 .add("name", "John")
                 .add("surname", "Smith").build();
 
         // Test POST
         given()
                 .contentType("application/json")
                 .body(obj.toString())
                 .when()
                 .post("/customers")
                 .then()
                 .statusCode(201);
 
         // Test GET
         given()
                 .when().get("/customers")
                 .then()
                 .statusCode(200)
                 .body(containsString("John"),
                       containsString("Smith"));
 
         obj = Json.createObjectBuilder()
                 .add("id", "0")
                 .add("name", "Donald")
                 .add("surname", "Duck").build();
 
         // Test PUT
         given()
                 .contentType("application/json")
                 .body(obj.toString())
                 .when()
                 .put("/customers")
                 .then()
                 .statusCode(204);
 
         // Test DELETE
         given()
                 .contentType("application/json")
                 .when()
                 .delete("/customers?id=0")
                 .then()
                 .statusCode(204);
 
     }
 } 

让我们换个角度,更仔细地看一下测试课。这里的大部分内容你应该很熟悉,除了 Json.createObjectBuilder API,它是一种方便的工厂方法,我们可以使用它来流畅地创建 JSON 对象。在我们的代码中,我们使用它来生成 javax.json.JsonObject 的两个实例。第一个已被序列化为字符串并通过 HTTP POST 调用发送到我们的 CustomerEndpoint。第二个已被采用通过 HTTP PUT 调用更新客户。

您可以使用以下命令打包和测试应用程序:

$ mvn package

输出将显示测试结果,应该是成功的:

[INFO] Results:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

testCustomerService 方法成功完成。现在我们有一个经过测试的 REST 应用程序,我们将学习如何让我们的应用程序在浏览器中运行。

Running the example

现在完整的项目触手可及,让我们看看它的实际效果!您可以使用以下命令启动应用程序:

$ mvn quarkus:dev

然后,转到 http://localhost:8080 的主页。您应该能够看到以下 UI,您可以在其中添加新客户:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》将Web界面添加到Quarkus服务
As you already know, the embedded Vert.x server will serve content from under the root context. If you want to vary this, you can configure the quarkus.http.root-path key in application.properties to set the context path.

一旦你有了一些数据,其他操作(例如 Edit 删除) 将可用:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》将Web界面添加到Quarkus服务

酷!您可以尝试编辑和删除数据以验证所有 REST 方法是否正常工作。现在,我们将学习如何在 Minishift 上部署我们的应用程序。

Running our application on Minishift

像往常一样启动您的 Minishift 环境并执行以下命令来构建应用程序的本机可执行 Docker 映像并将其部署在 Pod 中:

$ mvn package -Pnative -Dnative-image.docker-build=true

构建应用程序的本机映像将需要一分钟左右的时间。接下来,我们将把应用程序作为二进制构建上传到 Minishift 命名空间。您应该已经熟悉这些步骤,因此我们将只包含要执行的脚本以及一些内联注释。执行每一行并验证所有命令的输出是否成功:

#Create a new Project named quarkus-customer-service
$ oc new-project quarkus-customer-service

# Binary Build definition 
$ oc new-build --binary --name=quarkus-customer-service -l app=quarkus-customer-service

# Add the dockerfilePath location to our Binary Build
$ oc patch bc/quarkus-customer-service -p '{"spec":{"strategy":{"dockerStrategy":{"dockerfilePath":"src/main/docker/Dockerfile.native"}}}}'

# Uploading directory "." as binary input for the build
$ oc start-build quarkus-customer-service --from-dir=. --follow

# Create a new application using as source the Binary Build
$ oc new-app --image-stream=quarkus-customer-service:latest

# Create a Route for external clients
$ oc expose svc/quarkus-customer-service

现在,您应该可以在 Overview 面板中看到您的应用程序正在运行的 Pod,可以通过 Route - External Traffic 链接:http://quarkus-customer-service-quarkus-customer-service.192.168.42.53.nip.io(实际路由地址取决于IP已分配给您的环境的地址):

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》将Web界面添加到Quarkus服务

通过单击 Route - External Traffic 链接,您将能够验证您的应用程序是否在 Kubernetes 环境中工作,就像您的本地副本一样。

Configuring Cross-Origin Resource Sharing in Quarkus

在本章中,我们使用 JavaScript 将请求驱动到 Quarkus 的服务中。在更复杂的场景中,您的 JavaScript 代码部署在不同主机或上下文上的自己的服务中,您必须实现 跨源资源共享 (CORS) 以让它起作用。简而言之,CORS 允许 Web 客户端向托管在不同来源的服务器发出 HTTP 请求。 origin 是指 URI 方案、主机名和端口号的组合。

这对于 JavaScript 等客户端语言尤其具有挑战性,因为所有现代浏览器都需要脚本语言的同源策略。

为了完成这项工作,我们需要让我们的服务器应用程序负责决定谁可以发出请求,以及在使用 HTTP 标头时允许哪些类型的请求。在实践中,当服务器收到来自不同来源的请求时,它可以回复并说明允许哪些客户端访问 API,允许哪些 HTTP 方法或标头,最后是否允许在请求中使用 cookie。

这如何转化为 Quarkus 配置?您可能已经猜到,配置必须应用于 quarkus.http.cors 命名空间下的 application.properties 文件。以下是一个示例配置,它允许对所有域、所有 HTTP 方法和所有常见标头进行 CORS:

quarkus.http.cors=true
quarkus.http.cors.origins=*
quarkus.http.cors.methods=GET,PUT,POST,DELETE, OPTIONS
quarkus.http.cors.headers=X-Custom,accept, authorization, content-type, x-requested-with
quarkus.http.cors.exposed-headers=Content-Disposition

在实际场景中,您可能会将允许的来源列表设置为要求远程连接的域,如下所示:

quarkus.http.cors.origins=http://custom.origin.com

现在我们已经澄清了这一点,我们可以看另一个示例,在该示例中,我们将使用 Java Enterprise 组件(例如 WebSocket)来访问我们的 Quarkus 服务。

Adding Enterprise web components

在我们的客户服务示例中,前端应用程序使用 JavaScript 结构框架 (AngularJS) 来测试我们的应用程序。现在,我们将考虑一个不同的用例:一个新的外部服务将使用不同的协议栈连接到我们的应用程序。除了 JAX-RS 端点之外,Quarkus 还原生支持在嵌入式 Undertow Web 服务器中运行的 WebSocket 技术。因此,在此示例中,我们将向现有应用程序添加一个 WebSocket 端点。这将与在不同应用程序中运行的另一个 WebSocket 配对。

Introducing WebSockets

首先,让我们简要介绍一下我们的应用程序的新组件。 WebSocket,正如其企业规范所定义的那样,是一种在浏览器和服务器端点之间建立 socket 连接的 API。这与标准 TCP 套接字非常相似,因为它在客户端和服务器之间保持持久连接,双方都可以随时开始发送数据。

通常,您只需在 JavaScript 代码中调用 WebSocket 构造函数即可打开 WebSocket 连接:

var connection = new WebSocket('ws://localhost:8080/hello');
Notice the URL schema for WebSocket connections ( ws:). We also have wss: for secure WebSocket connections, which is used in the same way as https: is for secure HTTP connections.

我们可以将一些事件处理程序附加到连接上,以帮助我们确定连接状态何时打开、接收消息或何时发生错误。

我们可以通过使用 @ServerEndpoint 注释在服务器端声明一个 Java 类 WebSocket 服务器端点。还需要指定部署端点的 URI,如以下示例所示:

@ServerEndpoint(value = "/hello")
public class WebSocketEndpoint {
  
     @OnOpen
     public void onOpen(Session session) throws IOException {
         // Establish connection
     }
  
     @OnMessage
     public void onMessage(Session session, Message message) throws 
    IOException {
         // Handle Websocket messages
     }
  
     @OnClose
     public void onClose(Session session) throws IOException {
     }
  
     @OnError
     public void onError(Session session, Throwable throwable) {
     }
}

在下一节中,我们将向现有项目添加一个 WebSocket 层,然后创建另一个瘦项目来远程访问 WebSocket 并添加新客户。

Building a project that uses Websockets

您将在本书 GitHub 存储库的 Chapter04/customer-service/websockets 文件夹中找到两个不同的项目:

  • An updated customer-service project that ships with a WebSocket endpoint
  • A project called customer-service-fe that features a minimal JavaScript frontend for our WebSocket application

在继续之前,您应该将这两个项目都导入您的 IDE。

首先,让我们讨论 customer-service 项目。我们添加的主要增强功能是一个 WebSocket 端点,它负责插入一个新客户(使用 CustomerRepository bean)并返回我们客户的表格视图。以下是WebsocketEndpoint类的内容:

@ServerEndpoint(value="/customers", encoders = {MessageEncoder.class})
 
public class WebsocketEndpoint {
     @Inject
     CustomerRepository customerRepository;

     public List<Customer>  addCustomer(String message, Session 
     session) {
        Jsonb jsonb = JsonbBuilder.create();

        Customer customer = jsonb.fromJson(message, Customer.class);
        customerRepository.createCustomer(customer);
        return customerRepository.findAll();
     }
     @OnOpen
     public void myOnOpen(Session session) {
         System.out.println("WebSocket opened: " + session.getId());
     }
     @OnClose
     public void myOnClose(CloseReason reason) {
         System.out.println("Closing a due to " + 
          reason.getReasonPhrase());
     }
     @OnError
     public void error(Throwable t) {
 
     }
 
 }

这里有两点需要注意,如下:

  • The method tagged with @OnMessage receives the customer to be added in JSON format as input and returns an updated list of customers.
  • This class uses an encoder in order to customize the message that's returned to the client. An encoder takes a Java object and produces its serialized representation, which can then be transmitted to the client. For example, an encoder is typically in charge of producing JSON, XML, and binary representations. In our case, it encodes the customer list in JSON format.

现在,让我们看一下 MessageEncoder 类:

public class MessageEncoder implements Encoder.Text<java.util.List<Customer>>  {
 
     @Override
     public String encode(List<Customer> list) throws EncodeException {
         JsonArrayBuilder jsonArray = Json.createArrayBuilder();
         for(Customer c : list) {
             jsonArray.add(Json.createObjectBuilder()
                     .add("Name", c.getName())
                     .add("Surname", c.getSurname()));
         }
         JsonArray array = jsonArray.build();
         StringWriter buffer = new StringWriter();
         Json.createWriter(buffer).writeArray(array);
         return buffer.toString();
     }
 
     @Override
     public void init(EndpointConfig config) {
         System.out.println("Init");
     }
 
     @Override
     public void destroy() {
         System.out.println("destroy");
     }
 
}

如您所见,Encoder 必须实现以下任一接口:

  • Encoder.Text<T> for text messages
  • Encoder.Binary<T> for binary messages

在我们的例子中,List encode 方法中作为泛型类型被接收并转换为 JSON 字符串数组。

要编译,我们的项目需要 quarkus-undertow-websockets 扩展,可以手动添加到 pom.xml 文件中。或者,您可以使用以下命令让 Maven 插件为您执行此操作:

$ mvn quarkus:add-extension -Dextensions="quarkus-undertow-websockets"

您将在控制台上看到以下输出,确认扩展已添加到我们的配置中:

Adding extension io.quarkus:quarkus-undertow-websockets

服务器项目现已完成。您可以使用以下命令编译并运行它:

$ mvn compile quarkus:dev

现在,让我们用一个瘦 WebSocket JavaScript 客户端创建一个新的前端项目。

Creating a WebSocket client project

WebSocket 客户端,就像它们的服务器对应物一样,可以用许多不同的语言编写。由于现代浏览器原生支持 WebSocket,我们将编写一个简单的 JavaScript 客户端,这样我们就不必安装任何额外的工具或 SDK 来运行我们的示例。

customer-service-fe 文件夹中,您将找到可用于访问我们的 WebSocket 示例的前端项目。

我们的项目包含一个名为 index.html 的登录页面,当我们请求应用程序的根 Web 上下文时会提供该页面。在此页面中,我们包含了一个 HTML 表单和一个用于显示客户列表的表格:

<html>
 <head>
     <meta http-equiv="content-type" content="text/html; charset=ISO- 8859-1">
     <link rel="stylesheet" type="text/css" href="stylesheet.css" media="screen" />
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1 /jquery.min.js"></script>
     <script src="functions.js"></script>
 </head>
 
 <meta charset="utf-8">
 <body>
 <h1 style="text-align: center;">Connect to Quarkus Websocket Endpoint</h1>
 <br>
 
 <div>
     
     <form id="form1" action="">
         <div><h3>Enter Customer</h3></div>
         <div class="divTableRow">
             <div class="divTableCell">Name:</div>
             <div class="divTableCell"><input type="text" placeholder="Name" name="name" size="60"/></div>
         </div>
         <div class="divTableRow">
             <div class="divTableCell">Surname:</div>
             <div class="divTableCell"><input type="text" placeholder="Surname" name="surname" size="60"/></div>
         </div>
 
         <br/>
         <input onclick="send_message()" value="Insert" type="button" class="myButton">
     </form>
     <br/>
 
 </div>
 
 <table id="customerDataTable" class="blueTable" />
 <div id="output"></div>
 </body>
</html>

WebSocket 端点的连接发生在名为 function.js 的外部 JavaScript 文件中(您可以在 customer-service-fe/src/main /resources/META-INF/resources 文件夹在本书的 GitHub 存储库中)。然后以下是该文件的内容:

var wsUri = "ws://localhost:8080/customers";

function init() {
     output = document.getElementById("output");
}
 
function send_message() {
     websocket = new WebSocket(wsUri);
     websocket.onopen = function(evt) {
         onOpen(evt)
     };
     websocket.onmessage = function(evt) {
         onMessage(evt)
     };
     websocket.onerror = function(evt) {
         onError(evt)
     };
}
 
function onOpen(evt) {
     doSend(name.value);
}
 
function onMessage(evt) {
     buildHtmlTable('#customerDataTable', evt.data);
}
 
function onError(evt) {
     writeToScreen('<span style="color: red;">ERROR:</span> 
      ' + evt.data);
}
 
function doSend(message) {
     var json = toJSONString(document.getElementById("form1"));
     websocket.send(json);
 
}

如您所见,一旦建立连接,就有几个回调方法(onOpenonMessageonError)与服务器事件耦合。在这里,我们将在 doSend 方法中添加一个序列化为 JSON 字符串的新客户,而 onMessage 回调方法将接收由我们的 WebSocket 编码器。此数据最终将包含在 HTML 表中。

您可以使用以下命令运行项目:

$ mvn compile quarkus:dev -Dquarkus.http.port=9080 -Ddebug=6005

如您所见,我们已经将 HTTP 和调试端口偏移了 1000,这样它就不会与 customer-service 项目冲突。

浏览到 http://localhost:9080 将让您进入 WebSocket 客户端应用程序。添加一些示例数据以验证客户可以包含在表中:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》将Web界面添加到Quarkus服务

验证在 http://localhost:8080 中提供的 AngularJS 前端中是否也显示了相同的数据。

Adding an AJAX handler

在测试我们的 WebSocket 示例时,主要需要我们的 JavaScript 客户端。但是,您将在此项目中发现的另一个增强功能是 Java Servlet,它可以让您删除任何到后端的硬编码链接,以便在我们将示例移动到不同的机器或端口时两个服务仍然可以通信。

以下 Servlet 使用 ws://localhost:8080/customers 字符串从名为 CUSTOMER_SERVICE 的环境变量中确定服务器端点信息:

@WebServlet("/AjaxHandler")
public class AjaxHandler extends HttpServlet {
 
     public AjaxHandler() {
         super();
     }
 
     protected void doGet(HttpServletRequest request, 
      HttpServletResponse response)
      throws ServletException, IOException {
         String endpoint = System.getenv("CUSTOMER_SERVICE")
         != null ? System.getenv("CUSTOMER_SERVICE") : 
         "ws://localhost:8080/customers";
 
         PrintWriter out = response.getWriter();
         out.println(endpoint);
         out.flush();
 
     }
 
     protected void doPost(HttpServletRequest request, 
         HttpServletResponse response) throws
         ServletException, IOException {
         doGet(request, response);
     }
}

此更改需要反映在我们的 JavaScript 客户端中,这样它就不会为我们的 Web 套接字使用硬编码的端点。在 function.js 文件的最终版本中,您将找到以下 JavaScript 函数,它通过 AJAX 查询我们的 Servlet:

var wsUri = "";
function callAjax() {
 
     httpRequest = new XMLHttpRequest();
 
     if (!httpRequest) {
         console.log('Unable to create XMLHTTP instance');
         return false;
     }
     httpRequest.open('GET', 'AjaxHandler');
     httpRequest.responseType = 'text';
     httpRequest.send();
     httpRequest.onreadystatechange = function() {
         if (httpRequest.readyState === XMLHttpRequest.DONE) {
             if (httpRequest.status === 200) {
                 wsUri = httpRequest.response;
             } else {
                 console.log('Something went wrong..!!');
             }
         }
     }
}

加载 HTML 页面时调用此函数:

<body onload="callAjax()">

现在,从同一个 shell 启动服务器,以便读取环境变量:

$ mvn quarkus:dev

现在,转到 http://localhost:9080 并验证 WebSocket 请求产生的输出是否与静态定义服务器端点地址时的输出相同。

您可以通过在您的 customer-service 应用程序中改变 <​​kbd>quarkus.http.port 将这个示例更进一步。例如,您可以将其设置为 8888

$ mvn quarkus:dev -Dquarkus.http.port=8888

一旦您相应地设置了 CUSTOMER_SERVICE 环境变量,customer-service-fe 将能够连接到 WebSocket 端点:

$ export CUSTOMER_SERVICE=ws://localhost:8888/customers

伟大的!在本节中,我们从客户端应用程序中删除了所有静态硬编码信息,该应用程序现在使用环境变量来联系客户服务。

Summary

在本章中,我们研究了将 Web 内容添加到 Quarkus 应用程序的不同路径。首先,我们学习了如何创建一个 CRUD 内存应用程序来管理一组 Java 对象。然后,示例应用程序由带有一些特殊 API 的 JavaScript 层 (AngularJS) 访问,以处理 REST 调用。我们还查看了在 Quarkus 项目中启用 CORS 时所需的一些配置参数。接下来,我们添加了一个 WebSocket 层来引入初始项目和客户端前端之间的全双工通信。

通过完成本章,您现在知道如何使用嵌入式 Vert.x 和 Undertow 服务器来利用 REST API (quarkus-resteasy) 和 WebSocket/Servlet API (quarkus-undertow-websockets)。

在下一章中,我们将使用 Hibernate ORM 和 Hibernate Panache 扩展为我们的应用程序添加数据库存储。