天天看點

基于GraphQL的微服務實踐-spring cloud 入門教程

通常,與 REST 相比,GraphQL 被認為是一種革命性的 Web API 設計方式。但是,如果您仔細研究該技術,您會發現它們之間存在很多差異。GraphQL 是一種相對較新的解決方案,已于 2015 年由 Facebook 開源。今天,REST 仍然是最流行的用于公開 API 和微服務之間的服務間通信的範式。GraphQL 會在未來超過 REST 嗎?讓我們來看看如何使用 Spring Boot 和 Apollo 用戶端建立通過 GraphQL API 進行通信的微服務。

讓我們從示例系統的 Spring Boot GraphQL 微服務架構開始。我們有三個微服務,它們使用從 Eureka 服務發現中擷取的 URL 互相通信。

基于GraphQL的微服務實踐-spring cloud 入門教程

1. 為 GraphQL 啟用 Spring Boot 支援

隻需包含一些啟動器,我們就可以輕松地在伺服器端 Spring Boot 應用程式上啟用對 GraphQL 的支援。包含

graphql-spring-boot-starter

GraphQL servlet 後将在 path 下自動通路

/graphql

。我們可以覆寫設定屬性,預設路徑

graphql.servlet.mapping

application.yml

檔案。我們還應該啟用 GraphiQL——一個用于編寫、驗證和測試 GraphQL 查詢的浏覽器内 IDE,以及 GraphQL Java 工具庫,它包含用于建立查詢和突變的有用元件。由于該庫,類路徑上帶有

.graphqls

擴充名的任何檔案都将用于提供模式定義。

<dependency>
   <groupId>com.graphql-java</groupId>
   <artifactId>graphql-spring-boot-starter</artifactId>
   <version>5.0.2</version>
</dependency>
<dependency>
   <groupId>com.graphql-java</groupId>
   <artifactId>graphiql-spring-boot-starter</artifactId>
   <version>5.0.2</version>
</dependency>
<dependency>
   <groupId>com.graphql-java</groupId>
   <artifactId>graphql-java-tools</artifactId>
   <version>5.2.3</version>
</dependency>
           

2. 建構 GraphQL 模式定義

每個模式定義都包含資料類型聲明、它們之間的關系以及一組操作,包括用于搜尋對象的查詢和用于建立、更新或删除資料的更改。通常我們會從建立類型聲明開始,它負責域對象定義。您可以使用

!

char指定該字段是必需的還是使用

[...]

. 定義必須包含類型聲明或對規範中可用的其他類型的引用。

type Employee {
  id: ID!
  organizationId: Int!
  departmentId: Int!
  name: String!
  age: Int!
  position: String!
  salary: Int!
}
           

這是上面可見的 GraphQL 定義的等效 Java 類。GraphQL 類型

Int

也可以映射到 Java 

Long

。該

ID

标量類型代表的唯一辨別符-在這種情況下,它也将成為Java 

Long

public class Employee {

   private Long id;
   private Long organizationId;
   private Long departmentId;
   private String name;
   private int age;
   private String position;
   private int salary;
   
   // constructor
   
   // getters
   // setters
   
}
           

模式定義的下一部分包含查詢和變更聲明。大多數查詢傳回對象清單——用

[Employee]

. 在

EmployeeQueries

type 中我們聲明了所有的 find 方法,而在

EmployeeMutations

type 中添加、更新和删除員工的方法。如果将整個對象傳遞給該方法,則需要将其聲明為

input

類型。

schema {
  query: EmployeeQueries
  mutation: EmployeeMutations
}

type EmployeeQueries {
  employees: [Employee]
  employee(id: ID!): Employee!
  employeesByOrganization(organizationId: Int!): [Employee]
  employeesByDepartment(departmentId: Int!): [Employee]
}

type EmployeeMutations {
  newEmployee(employee: EmployeeInput!): Employee
  deleteEmployee(id: ID!) : Boolean
  updateEmployee(id: ID!, employee: EmployeeInput!): Employee
}

input EmployeeInput {
  organizationId: Int
  departmentId: Int
  name: String
  age: Int
  position: String
  salary: Int
}
           

3. 查詢和​​變異實作

多虧了 GraphQL Java 工具和 Spring Boot GraphQL 自動配置,我們不需要做太多的事情來在我們的應用程式中實作查詢和更改。該

EmployeesQuery

豆有

GraphQLQueryResolver

接口。基于這一點,Spring 将能夠自動檢測并調用正确的方法,作為對模式内聲明的 GraphQL 查詢之一的響應。這是一個包含查詢實作的類。

@Component
public class EmployeeQueries implements GraphQLQueryResolver {

   private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeQueries.class);
   
   @Autowired
   EmployeeRepository repository;
   
   public List<Employee> employees() {
      LOGGER.info("Employees find");
      return repository.findAll();
   }
   
   public List<Employee> employeesByOrganization(Long organizationId) {
      LOGGER.info("Employees find: organizationId={}", organizationId);
      return repository.findByOrganization(organizationId);
   }

   public List<Employee> employeesByDepartment(Long departmentId) {
      LOGGER.info("Employees find: departmentId={}", departmentId);
      return repository.findByDepartment(departmentId);
   }
   
   public Employee employee(Long id) {
      LOGGER.info("Employee find: id={}", id);
      return repository.findById(id);
   }
   
}
           

如果您想調用例如方法,

employee(Long id)

您應該建構以下查詢。您可以使用 path 下提供的 GraphiQL 工具在您的應用程式中輕松測試它

/graphiql

基于GraphQL的微服務實踐-spring cloud 入門教程

負責執行變異方法的 bean 需要實作

GraphQLMutationResolver

. 盡管聲明了

EmployeeInput

我們仍然使用與查詢傳回相同的域對象 - 

Employee

@Component
public class EmployeeMutations implements GraphQLMutationResolver {

   private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeQueries.class);
   
   @Autowired
   EmployeeRepository repository;
   
   public Employee newEmployee(Employee employee) {
      LOGGER.info("Employee add: employee={}", employee);
      return repository.add(employee);
   }
   
   public boolean deleteEmployee(Long id) {
      LOGGER.info("Employee delete: id={}", id);
      return repository.delete(id);
   }
   
   public Employee updateEmployee(Long id, Employee employee) {
      LOGGER.info("Employee update: id={}, employee={}", id, employee);
      return repository.update(id, employee);
   }
   
}
           

我們還可以使用 GraphiQL 來測試突變。這是添加新員工并接收帶有員工 ID 和姓名的響應的指令。

基于GraphQL的微服務實踐-spring cloud 入門教程

4. 生成用戶端類

好的,我們已經成功建立了一個伺服器端應用程式。我們已經使用 GraphiQL 測試了一些查詢。但我們的主要目标是建立一些其他微服務,

employee-service

通過 GraphQL API與應用程式通信。這裡是大部分關于 Spring Boot 和 GraphQL 結尾的教程。

為了能夠通過 GraphQL API 與我們的第一個應用程式通信,我們有兩種選擇。我們可以使用 HTTP GET 請求或使用現有的 Java 用戶端之一來擷取标準的 REST 用戶端并自己實作 GraphQL API。令人驚訝的是,可用的 GraphQL Java 用戶端實作并不多。最嚴重的選擇是适用于 Android 的 Apollo GraphQL 用戶端。當然,它不僅是為 Android 裝置設計的,您也可以在您的微服務 Java 應用程式中成功使用它。

在使用用戶端之前,我們需要從模式和

.grapql

檔案生成類。推薦的方法是通過 Apollo Gradle 插件。還有一些 Maven 插件,但它們都沒有提供 Gradle 插件的自動化級别,例如它會自動下載下傳生成用戶端類所需的 node.js。是以,第一步是将 Apollo 插件和運作時添加到項目依賴項中。

buildscript {
  repositories {
    jcenter()
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
  }
  dependencies {
    classpath 'com.apollographql.apollo:apollo-gradle-plugin:1.0.1-SNAPSHOT'
  }
}

apply plugin: 'com.apollographql.android'

dependencies {
  compile 'com.apollographql.apollo:apollo-runtime:1.0.1-SNAPSHOT'
}
           

GraphQL Gradle 插件嘗試查找具有

.graphql

擴充名和

schema.json

内部

src/main/graphql

目錄的檔案。可以通過調用 resource 從 Spring Boot 應用程式擷取 GraphQL JSON 模式

/graphql/schema.json

。檔案

.graphql

包含查詢定義。查詢

employeesByOrganization

将由 調用

organization-service

,而

employeesByDepartment

department-service

和調用

organization-service

。這兩個應用程式在響應中需要一些不同的資料集。應用程式

department-service

需要比

organization-service

. 在這種情況下,GraphQL 是一個很好的解決方案,因為我們可以在用戶端的響應中定義所需的資料集。這是

employeesByOrganization

調用者的查詢定義

organization-service

query EmployeesByOrganization($organizationId: Int!) {
  employeesByOrganization(organizationId: $organizationId) {
    id
    name
  }
}
           

應用程式

organization-service

也會調用

employeesByDepartment

查詢。

query EmployeesByDepartment($departmentId: Int!) {
  employeesByDepartment(departmentId: $departmentId) {
    id
    name
  }
}
           

查詢

employeesByDepartment

也被調用

department-service

,它不僅需要

id

and

name

字段,還需要

position

and 

salary

query EmployeesByDepartment($departmentId: Int!) {
  employeesByDepartment(departmentId: $departmentId) {
    id
    name
    position
    salary
  }
}
           

所有生成的類都在

build/generated/source/apollo

目錄下可用。

5. 使用發現建構 Apollo 用戶端

在生成所有必需的類并将它們包含到調用微服務中後,我們可以繼續進行用戶端實作。Apollo 用戶端有兩個重要的特性會影響我們的開發:

  • 它隻提供基于回調的異步方法
  • 它沒有與基于 Spring Cloud Netflix Eureka 的服務發現內建

這是

employee-service

client inside的一個實作

department-service

。我

EurekaClient

直接使用(1)。它将所有正在運作的執行個體注冊為

EMPLOYEE-SERVICE

. 然後它從可用執行個體清單中随機選擇一個執行個體(2)。該執行個體的端口号被傳遞給

ApolloClient

 (3)。在調用我們

enqueue

提供的異步方法之前,

ApolloClient

我們建立了 lock (4),它最大等待。5 秒釋放(8)。方法 enqueue 在回調方法

onResponse

 (5) 中傳回響應。我們将響應主體從 GraphQL

Employee

對象映射到傳回的對象(6),然後釋放鎖(7)。

@Component
public class EmployeeClient {

   private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeClient.class);
   private static final int TIMEOUT = 5000;
   private static final String SERVICE_NAME = "EMPLOYEE-SERVICE"; 
   private static final String SERVER_URL = "http://localhost:%d/graphql";
   
   Random r = new Random();
   
   @Autowired
   private EurekaClient discoveryClient; // (1)
   
   public List<Employee> findByDepartment(Long departmentId) throws InterruptedException {
      List<Employee> employees = new ArrayList<>();
      Application app = discoveryClient.getApplication(SERVICE_NAME); // (2)
      InstanceInfo ii = app.getInstances().get(r.nextInt(app.size()));
      ApolloClient client = ApolloClient.builder().serverUrl(String.format(SERVER_URL, ii.getPort())).build(); // (3)
      CountDownLatch lock = new CountDownLatch(1); // (4)
      client.query(EmployeesByDepartmentQuery.builder().build()).enqueue(new Callback<EmployeesByDepartmentQuery.Data>() {

         @Override
         public void onFailure(ApolloException ex) {
            LOGGER.info("Err: {}", ex);
            lock.countDown();
         }

         @Override
         public void onResponse(Response<EmployeesByDepartmentQuery.Data> res) { // (5)
            LOGGER.info("Res: {}", res);
            employees.addAll(res.data().employees().stream().map(emp -> new Employee(Long.valueOf(emp.id()), emp.name(), emp.position(), emp.salary())).collect(Collectors.toList())); // (6)
            lock.countDown(); // (7)
         }

      });
      lock.await(TIMEOUT, TimeUnit.MILLISECONDS); // (8)
      return employees;
   }
   
}
           

最後,

EmployeeClient

注入查詢解析器類 – 

DepartmentQueries

,并在 query 内部使用

departmentsByOrganizationWithEmployees

@Component
public class DepartmentQueries implements GraphQLQueryResolver {

   private static final Logger LOGGER = LoggerFactory.getLogger(DepartmentQueries.class);
   
   @Autowired
   EmployeeClient employeeClient;
   @Autowired
   DepartmentRepository repository;

   public List<Department> departmentsByOrganizationWithEmployees(Long organizationId) {
      LOGGER.info("Departments find: organizationId={}", organizationId);
      List<Department> departments = repository.findByOrganization(organizationId);
      departments.forEach(d -> {
         try {
            d.setEmployees(employeeClient.findByDepartment(d.getId()));
         } catch (InterruptedException e) {
            LOGGER.error("Error calling employee-service", e);
         }
      });
      return departments;
   }
   
   // other queries
   
}
           

在調用目标查詢之前,我們應該檢視為

department-service

. 每個

Department

對象都可以包含配置設定的員工清單,是以我們還定義了

Employee

Department

類型引用的類型。

schema {
  query: DepartmentQueries
  mutation: DepartmentMutations
}

type DepartmentQueries {
  departments: [Department]
  department(id: ID!): Department!
  departmentsByOrganization(organizationId: Int!): [Department]
  departmentsByOrganizationWithEmployees(organizationId: Int!): [Department]
}

type DepartmentMutations {
  newDepartment(department: DepartmentInput!): Department
  deleteDepartment(id: ID!) : Boolean
  updateDepartment(id: ID!, department: DepartmentInput!): Department
}

input DepartmentInput {
  organizationId: Int!
  name: String!
}

type Department {
  id: ID!
  organizationId: Int!
  name: String!
  employees: [Employee]
}

type Employee {
  id: ID!
  name: String!
  position: String!
  salary: Int!
}
           

現在,我們可以使用 GraphiQL 使用必填字段清單調用我們的測試查詢。

department-service

預設情況下,應用程式在端口 8091 下可用,是以我們可以使用 address 調用它

http://localhost:8091/graphiql

基于GraphQL的微服務實踐-spring cloud 入門教程

結論

GraphQL 似乎是标準 REST API 的有趣替代品。但是,我們不應将其視為 REST 的替代品。在某些用例中,GraphQL 可能是更好的選擇,而在某些用例中,REST 是更好的選擇。如果您的用戶端不需要伺服器端傳回的完整字段集,而且您有許多用戶端對單個端點有不同的要求,那麼 GraphQL 是一個不錯的選擇。對于 Spring Boot 微服務,沒有基于 Java 的解決方案允許您将 GraphQL 與服務發現、負載平衡或開箱即用的 API 網關一起使用。

使用 Zuul、Ribbon、Feign、Eureka 和 Sleuth、Zipkin 建立簡單spring cloud微服務用例-spring cloud 入門教程

微服務內建SPRING CLOUD SLEUTH、ELK 和 ZIPKIN 進行監控-spring cloud 入門教程

使用Hystrix 、Feign 和 Ribbon建構微服務-spring cloud 入門教程

使用 Spring Boot Admin 監控微服務-spring cloud 入門教程

基于Redis做Spring Cloud Gateway 中的速率限制實踐-spring cloud 入門教程

內建SWAGGER2服務-spring cloud 入門教程

Hystrix 簡介-spring cloud 入門教程

Hystrix 原理深入分析-spring cloud 入門教程 

使用Apache Camel建構微服務-spring cloud 入門教程

內建 Kubernetes 來建構微服務-spring cloud 入門教程

內建SPRINGDOC OPENAPI 的微服務實踐-spring cloud 入門教程

SPRING CLOUD 微服務快速指南-spring cloud 入門教程

基于GraphQL的微服務實踐-spring cloud 入門教程

最火的Spring Cloud Gateway 為經過身份驗證的使用者啟用速率限制實踐-spring cloud 入門教程

繼續閱讀