GraphQL будущее микросервисов?

в 9:38, , рубрики: api, gql, groovy, java, Microservices, spring, Разработка веб-сайтов

GraphQL часто представляют как революционный путь дизайна веб API по сравнению с REST. Однако, если вы ближе посмотрите на эти технологии, то вы увидите, что между ними очень много различий. GraphQL относительно новое решение, исходники которого были открыты сообществу Фейсбуком в 2015 году. Сегодня REST все еще самая популярная парадигма, используемая для предоставления API и взаимодействия между микросервисами. Сможет ли GraphQL обогнать REST в будущем? Давайте посмотрим, как происходит микросервисное взаимодействие через GraphQL API с ипользованием Spring Boot и библиотеки GQL.

Начнем с архитектуры примера нашей системы. Предположим, что у нас есть три микросервиса, которые общаются друг с другом через URL полученные из Spring Cloud Eureka приложения.

image

Включаем поддержку GraphQL в Spring Boot

Мы можем легко включить поддержку для GraphQL на серверной стороне приложения Spring Boot с помощью стартеров. После добавления graphql-spring-boot-starter сервлет GraphQL будет автоматически доступен по пути /graphql. Мы можем переопределить этот путь по умолчанию через указание свойства graphql.servlet.mapping в application.yml файле. Мы также включаем GraphiQL – браузерную IDE для написание, проверки и тестирования GraphQL запросов и библиотеку GraphQL Java Tools, которая содержит полезные компоненты для создания запросов и мутаций. Благодаря этой библиотеки все файлы в classpath с расширением .graphqls будут использованы для создания определения схемы.

compile('com.graphql-java:graphql-spring-boot-starter:5.0.2')
compile('com.graphql-java:graphiql-spring-boot-starter:5.0.2')

compile('com.graphql-java:graphql-java-tools:5.2.3')

Описание GrpahQL схемы

Каждое описание схемы содержит декларацию типов, связи между ними и множество операций включающих запросы для поиска объектов и мутаций для создания, обновления или удаления данных. Обычно мы начинаем с определения типа, который отвечает за домен описываемого объекта. Вы можете указать является ли поле обязательным с помощью ! символа или если это массив – […]. Описание должно содержать декларируемы тип или ссылку на другие типы доступные в спецификации.

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

Следующая часть определения схемы содержит декларации запросов и мутаций. Большинство запросов возвращают список объектов, которые помечены в схеме как [Employee]. Внутри типа EmployeeQueries мы объявляем все методы поиска, в то время как в типе EmployeeMutations методы для добавления, обновления и удаления сотрудников. Если вы передаете целый объект в метод, вы должны объявить его как 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
}

Реализация запросов и мутаций

Благодаря автоконфигурации GraphQL Java Tools и 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 employees() {
        LOGGER.info("Employees find");
        return repository.findAll();
    }
     
    public List employeesByOrganization(Long organizationId) {
        LOGGER.info("Employees find: organizationId={}", organizationId);
        return repository.findByOrganization(organizationId);
    }
    public List 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), напишите следующий запрос. Чтобы протестировать его в вашем приложении используйте GraphiQL, который доступен по адресу /graphiql.

image

Бину ответственному за имплементацию методов мутаций нужно реализовать интерфейс 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 для тестирования мутаций. Вот команда, которая добавляет новый employee и принимает ответ с id и name сотрудника.

image

На этом я приостанавливаю перевод данной статьи и пишу свое «лирическое отступление», а фактически подменяю описание части микросервисного взаимодейстивя через Apollo Client, на взаимодействие через библиотеку GQL и Unirest – библиотеки для выполнения HTTP запросов.

GraphQL клиент на Groovy.

Для создания GraphQL запросов в микросервисе department-servive я буду использовать Query builders:

String queryString = DSL.buildQuery {
    query('employeesByDepartment', [departmentId: departmentId]) {
        returns {
            id
            name
            position
            salary
        }
    }
}

Данная конструкция на DSL GQL создает запрос вида:

{  
  employeesByDepartment (departmentId: 1) { 
    id
    name
    position
    salary
  } 
}

И, далее, я буду выполнять HTTP запрос по переданному в метод адресу.
Как формируется адрес запроса узнаем далее.

(Unirest.post(serverUrl)
        .body(JsonOutput.toJson([query: queryString]))
        .asJson()
        .body.jsonObject['data']['employeesByDepartment'] as List)
        .collect { JsonUtils.jsonToData(it.toString(), Employee.class) }

После получения ответа выполняем его преобразование из JSONObject к виду списка Employee.

GrpahQL клиент для микросервиса сотрудников

Рассмотрим реализацию микросервиса сотрудников. В этом примере я пользовался Eureka клиентом напрямую. eurekaClient получает все запущенные экземпляры сервисов, зарегистрированные как employee-service. Затем он случайно выбирает какой-то экземпляр из зарегистрированных (2). Далее, берет номер его порта и формирует адрес запроса (3) и передает его в объект EmployeeGQL который является GraphQL клиентом на Groovy и, который, описан в предыдущем пункте.

@Component
public class EmployeeClient {

  private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeClient.class);
  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) {
    Application app = discoveryClient.getApplication(SERVICE_NAME);
    InstanceInfo ii = app.getInstances().get(r.nextInt(app.size())); // (2)
    String serverUrl = String.format(SERVER_URL, ii.getPort()); // (3)
    EmployeeGQL clientGQL = new EmployeeGQL();
    return clientGQL.getEmployeesByDepartmentQuery(serverUrl, departmentId.intValue()); // (4)
  }

}

Далее, я «передаю» снова слово автору, точнее продолжаю перевод его статьи.

И наконец, EmployeeClient внедряется в класс который отвечает на запросы DepartmentQueries и используется внутри запроса departmentsByOrganizationWithEmployees.

public List<Department> departmentsByOrganizationWithEmployees(Long organizationId) {
  LOGGER.info("Departments find: organizationId={}", organizationId);
  List<Department> departments = repository.findByOrganization(organizationId);
  for (int i = 0; i < departments.size(); i++) {
    departments.get(i).setEmployees(employeeClient.findByDepartment(departments.get(i).getId()));
  }
  return departments;
}

Перед выполнением нужного запросы нам следуем взглянуть на схему созданную для 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, то есть мы можем увидеть его по адресу http://localhost:8091/graphiql

Заключение

Возможно, GraphQL может быть интересной альтернативой стандартным REST API. Однако, нам не следует рассматривать его как замену REST. Существуют несколько случаев, где GraphQL может быть лучшим выбором, но те, где лучшим выбором будет REST. Если вашим клиентам не нужно иметь все поля, возвращаемые серверной частью, и более того у вас есть много клиентов с разными требованиями для одной точки входа, то GraphQL хороший выбор. Если посмотреть на то, что есть в микросервисном сообществе, то можно увидеть, что сейчас там нет решение основанных на Java, которые позволяют вам использовать GraphQL вместе с обнаружением сервисов, балансировщиком или API gateway из коробки. В этой статье я показал пример использования GQL и Unirest для создания GraphQL клиента вместе с Spring Cloud Eureka для микросервисной коммуникации. Пример кода автора статьи на английском на GitHub github.com/piomin/sample-graphql-microservices.git.

Пример моего когда с библиотекой GQL: github.com/lynx-r/sample-graphql-microservices

Автор: Алексей

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js