«1. Обзор

В этой статье объясняется процесс создания веб-службы REST на основе гипермедиа с использованием проекта Spring HATEOAS.

2. Spring-HATEOAS

Проект Spring HATEOAS представляет собой библиотеку API-интерфейсов, которую мы можем использовать для простого создания представлений REST, соответствующих принципу HATEOAS (гипертекст как механизм состояния приложения).

Вообще говоря, принцип подразумевает, что API должен направлять клиента через приложение, возвращая соответствующую информацию о следующих возможных шагах вместе с каждым ответом.

В этой статье мы собираемся создать пример с использованием Spring HATEOAS с целью разъединения клиента и сервера и теоретически позволить API изменять свою схему URI без нарушения работы клиентов.

3. Подготовка

Сначала добавим зависимость Spring HATEOAS:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
    <version>2.1.4.RELEASE</version>
</dependency>

Если мы не используем Spring Boot, мы можем добавить в наш проект следующие библиотеки:

<dependency>
    <groupId>org.springframework.hateoas</groupId>
    <artifactId>spring-hateoas</artifactId>
    <version>0.25.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.plugin</groupId>
    <artifactId>spring-plugin-core</artifactId>
    <version>1.2.0.RELEASE</version>
</dependency>

Как всегда , мы можем искать последние версии начального HATEOAS, зависимостей spring-hateoas и spring-plugin-core в Maven Central.

Далее у нас есть ресурс Customer без поддержки Spring HATEOAS:

public class Customer {

    private String customerId;
    private String customerName;
    private String companyName;

    // standard getters and setters
}

И у нас есть класс контроллера без поддержки Spring HATEOAS:

@RestController
@RequestMapping(value = "/customers")
public class CustomerController {
    @Autowired
    private CustomerService customerService;

    @GetMapping("/{customerId}")
    public Customer getCustomerById(@PathVariable String customerId) {
        return customerService.getCustomerDetail(customerId);
    }
}

Наконец, представление ресурса Customer:

{
    "customerId": "10A",
    "customerName": "Jane",
    "customerCompany": "ABC Company"
}

~~ ~ 4. Добавление поддержки HATEOAS

В проекте Spring HATEOAS нам не нужно ни искать контекст сервлета, ни конкатенировать переменную пути к базовому URI.

Вместо этого Spring HATEOAS предлагает три абстракции для создания URI — RepresentationModel, Link и WebMvcLinkBuilder. Мы можем использовать их для создания метаданных и связывания их с представлением ресурса.

4.1. Добавление поддержки гипермедиа к ресурсу

Проект предоставляет базовый класс RepresentationModel для наследования при создании представления ресурса:

public class Customer extends RepresentationModel<Customer> {
    private String customerId;
    private String customerName;
    private String companyName;
 
    // standard getters and setters
}

Ресурс Customer расширяется от класса RepresentationModel, чтобы наследовать метод add(). Поэтому, как только мы создадим ссылку, мы можем легко установить это значение для представления ресурса, не добавляя к нему никаких новых полей.

4.2. Создание ссылок

Spring HATEOAS предоставляет объект Link для хранения метаданных (местоположение или URI ресурса).

Сначала мы создадим простую ссылку вручную:

Link link = new Link("http://localhost:8080/spring-security-rest/api/customers/10A");

Объект Link следует синтаксису ссылки Atom и состоит из rel, который идентифицирует отношение к ресурсу, и атрибута href, который является самой ссылкой.

Вот как теперь выглядит ресурс Customer, содержащий новую ссылку:

{
    "customerId": "10A",
    "customerName": "Jane",
    "customerCompany": "ABC Company",
    "_links":{
        "self":{
            "href":"http://localhost:8080/spring-security-rest/api/customers/10A"
         }
    }
}

URI, связанный с ответом, квалифицируется как собственная ссылка. Семантика самоотношения ясна — это просто каноническое местоположение, в котором можно получить доступ к ресурсу.

4.3. Создание лучших ссылок

Другой очень важной абстракцией, предлагаемой библиотекой, является WebMvcLinkBuilder, который упрощает создание URI, избегая жестко запрограммированных ссылок.

В следующем фрагменте показано создание самоссылки клиента с использованием класса WebMvcLinkBuilder:

linkTo(CustomerController.class).slash(customer.getCustomerId()).withSelfRel();

Давайте посмотрим:

    метод linkTo() проверяет класс контроллера и получает его корень, отображающий косую черту(). метод добавляет значение customerId в качестве переменной пути ссылки, наконец, withSelfMethod() квалифицирует отношение как самоссылку

5. Отношения

В предыдущем разделе мы показали отношение, ссылающееся на самого себя. Однако более сложные системы могут включать и другие отношения.

Например, клиент может иметь отношения с заказами. Давайте также смоделируем класс Order как ресурс:

public class Order extends RepresentationModel<Order> {
    private String orderId;
    private double price;
    private int quantity;

    // standard getters and setters
}

На этом этапе мы можем расширить CustomerController с помощью метода, который возвращает все заказы конкретного клиента:

@GetMapping(value = "/{customerId}/orders", produces = { "application/hal+json" })
public CollectionModel<Order> getOrdersForCustomer(@PathVariable final String customerId) {
    List<Order> orders = orderService.getAllOrdersForCustomer(customerId);
    for (final Order order : orders) {
        Link selfLink = linkTo(methodOn(CustomerController.class)
          .getOrderById(customerId, order.getOrderId())).withSelfRel();
        order.add(selfLink);
    }
 
    Link link = linkTo(methodOn(CustomerController.class)
      .getOrdersForCustomer(customerId)).withSelfRel();
    CollectionModel<Order> result = CollectionModel.of(orders, link);
    return result;
}

Наш метод возвращает объект CollectionModel для соответствия возвращаемому типу HAL, а также ссылку «_self» для каждого из заказов и полный список.

«Здесь важно отметить, что гиперссылка для заказов клиентов зависит от отображения метода getOrdersForCustomer(). Мы будем называть эти типы ссылок ссылками на методы и покажем, как WebMvcLinkBuilder может помочь в их создании.

6. Ссылки на методы контроллера

WebMvcLinkBuilder предлагает расширенную поддержку контроллеров Spring MVC. В следующем примере показано, как создавать гиперссылки HATEOAS на основе метода getOrdersForCustomer() класса CustomerController: customerId в качестве переменной пути URI.

Link ordersLink = linkTo(methodOn(CustomerController.class)
  .getOrdersForCustomer(customerId)).withRel("allOrders");

7. Spring HATEOAS в действии

Давайте объединим самоссылку и создание ссылки на метод в методе getAllCustomers():

Затем вызовем метод getAllCustomers():

@GetMapping(produces = { "application/hal+json" })
public CollectionModel<Customer> getAllCustomers() {
    List<Customer> allCustomers = customerService.allCustomers();

    for (Customer customer : allCustomers) {
        String customerId = customer.getCustomerId();
        Link selfLink = linkTo(CustomerController.class).slash(customerId).withSelfRel();
        customer.add(selfLink);
        if (orderService.getAllOrdersForCustomer(customerId).size() > 0) {
            Link ordersLink = linkTo(methodOn(CustomerController.class)
              .getOrdersForCustomer(customerId)).withRel("allOrders");
            customer.add(ordersLink);
        }
    }

    Link link = linkTo(CustomerController.class).withSelfRel();
    CollectionModel<Customer> result = CollectionModel.of(allCustomers, link);
    return result;
}

И проверьте результат:

curl http://localhost:8080/spring-security-rest/api/customers

В каждом представлении ресурса есть ссылка self и ссылка allOrders для извлечения всех заказов клиента. Если у клиента нет заказов, ссылка на заказы не появится.

{
  "_embedded": {
    "customerList": [{
        "customerId": "10A",
        "customerName": "Jane",
        "companyName": "ABC Company",
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/10A"
          },
          "allOrders": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/10A/orders"
          }
        }
      },{
        "customerId": "20B",
        "customerName": "Bob",
        "companyName": "XYZ Company",
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/20B"
          },
          "allOrders": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/20B/orders"
          }
        }
      },{
        "customerId": "30C",
        "customerName": "Tim",
        "companyName": "CKV Company",
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/30C"
          }
        }
      }]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/spring-security-rest/api/customers"
    }
  }
}

Этот пример демонстрирует, как Spring HATEOAS способствует обнаружению API в веб-службе отдыха. Если ссылка существует, клиент может перейти по ней и получить все заказы для клиента:

curl http://localhost:8080/spring-security-rest/api/customers/10A/orders
{
  "_embedded": {
    "orderList": [{
        "orderId": "001A",
        "price": 150,
        "quantity": 25,
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/10A/001A"
          }
        }
      },{
        "orderId": "002A",
        "price": 250,
        "quantity": 15,
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/10A/002A"
          }
        }
      }]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/spring-security-rest/api/customers/10A/orders"
    }
  }
}

8. Заключение

В этом руководстве мы обсудили, как создать Spring REST на основе гипермедиа. веб-сервис с использованием проекта Spring HATEOAS.

В примере мы видим, что у клиента может быть единственная точка входа в приложение, и дальнейшие действия могут быть предприняты на основе метаданных в представлении ответа.

Это позволяет серверу изменять свою схему URI без нарушения работы клиента. Кроме того, приложение может рекламировать новые возможности, помещая в представление новые ссылки или URI.

Наконец, полную реализацию этой статьи можно найти в проекте GitHub.