Spring in Action Covers Spring 5-1--11 — страница 32 из 63

Жесткое кодирование URL-адреса это достаточно плохая идея. Если ваши амбиции Taco Cloud не ограничиваются только тем, что приложение запускается только на вашей собственной машине где разрабатывается ресурс, вам нужен способ не хардкодить localhost:8080. К счастью, Spring HATEOAS предоставляет помощь в виде компоновщиков ссылок.

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

Используя ControllerLinkBuilder, вы можете переписать хардкордное задание Link в RecentTacos() следующими строками:

Resources> recentResources = Resources.wrap(tacos);

recentResources.add(

 ControllerLinkBuilder.linkTo(DesignTacoController.class)

   .slash("recent")

   .withRel("recents"));

Вам не только больше не нужно хардкодить имя хоста, вам также не нужно указывать путь /design. Вместо этого вы запрашиваете ссылку на DesignTacoController, базовый путь которого /design. ControllerLinkBuilder использует базовый путь контроллера в качестве основы создаваемого вами объекта Link.

Далее следует вызов одного из моих любимых методов в любом Spring проекте : slash (). Мне нравится этот метод, потому что он так кратко описывает, что именно он собирается делать. Он буквально добавляет косую черту (/) и заданное значение в URL. В результате путь URL-адреса /design/recent.

Наконец, вы указываете имя отношения для ссылки. В этом примере отношение называется recents.

Хотя я большой поклонник метода slash(), у ControllerLinkBuilder есть еще один метод, который может помочь устранить любое жесткое кодирование, связанное с URL-адресами ссылок. Вместо того, чтобы вызывать slash(), вы можете вызвать linkTo(), передав ему в метод контроллер, чтобы ControllerLinkBuilder получал базовый URL как из базового пути контроллера, так и из сопоставленного пути метода. Следующий код написан с использованием метода linkTo():

Resources> recentResources = Resources.wrap(tacos);

recentResources.add(

 linkTo(methodOn(DesignTacoController.class).recentTacos())

   .withRel("recents"));

Здесь я решил статически включить методы linkTo() и methodOn() (оба из ControllerLinkBuilder), чтобы облегчить чтение кода. Метод methodOn() берет класс контроллера и позволяет вам вызвать метод recentTacos(), который перехватывается ControllerLinkBuilder и используется для определения не только базового пути контроллера, но и пути, сопоставленного с recentTacos(). Теперь весь URL-адрес получен из сопоставлений контроллера, и нет никакого хардкода. Великолепно!

6.2.2 Создание ресурсов ассемблеров (assemblers)

Теперь вам нужно добавить ссылки на ресурс тако, содержащийся в списке. Один из вариантов - циклически проходить по каждому из элементов Resource, содержащихся в объекте Resources, добавляя ссылку на каждый из них по отдельности. Но это немного утомительно, и вам нужно будет повторять этот код цикла в API везде, где вы возвращаете список ресурсов тако.

Нам нужна другая тактика.

Вместо того чтобы позволить Resources.wrap () создать объект Resource для каждого тако в списке, вы определите служебный класс, который преобразует объекты Taco в новый объект TacoResource. Объект TacoResource будет очень похож на Taco, но он также будет иметь возможность переносить ссылки. Следующий листинг показывает, как может выглядеть TacoResource.

Листинг 6.5. Тако-ресурс, несущий данные домена и список гиперссылок

package tacos.web.api;

import java.util.Date;

import java.util.List;

import org.springframework.hateoas.ResourceSupport;

import lombok.Getter;

import tacos.Ingredient;

import tacos.Taco;

public class TacoResource extends ResourceSupport {

 @Getter

 private final String name;

 @Getter

 private final Date createdAt;

 @Getter

 private final List ingredients;

 public TacoResource(Taco taco) {

   this.name = taco.getName();

   this.createdAt = taco.getCreatedAt();

   this.ingredients = taco.getIngredients();

 }

}

Во многом TacoResource ничем не отличается от доменного типа Taco. У них обоих есть свойства name, createAt и ingredients. Но TacoResource расширяет ResourceSupport для наследования списка объектов Link и методов для управления списком ссылок.

Более того, TacoResource не включает свойство id из Taco. Это потому, что нет необходимости предоставлять какие-либо специфичные для базы данных идентификаторы в API. Самостоятельная ссылка ресурса будет служить идентификатором ресурса с точки зрения клиента API.

ПРИМЕЧАНИЕ Домены и ресурсы: отдельные или одинаковые? Некоторые разработчики Spring могут объединить свои доменные типы и ресурсные в один тип, если их типы доменов расширяют ResourceSupport. Так тоже можно, нет правильного или неправильного ответа относительно того, какой путь правильный. Я выбрал создание отдельного типа ресурса, чтобы в Taco не было ненужных загромождений ссылками на ресурсы в тех случаях, когда ссылки не нужны. Кроме того, создав отдельный тип ресурса, я смог легко опустить свойство id, чтобы оно не отображалось в API.

TacoResource имеет единственный конструктор, который принимает Taco и копирует соответствующие свойства из Taco в его собственные свойства. Это облегчает преобразование объекта Taco в TacoResource. Но если вы остановитесь на этом, вам все равно понадобится цикл для преобразования списка объектов Taco в Resources.

Чтобы помочь в преобразовании объектов Taco в объекты TacoResource, вы также собираетесь создать ассемблер ресурсов. Следующий список - это то, что вам нужно.

Листинг 6.6. Ассемблер ресурсов, который собирает тако-ресурсы

package tacos.web.api;

import org.springframework.hateoas.mvc.ResourceAssemblerSupport;

import tacos.Taco;

public class TacoResourceAssembler

   extends ResourceAssemblerSupport {

 public TacoResourceAssembler() {

   super(DesignTacoController.class, TacoResource.class);

 }

@Override

 protected TacoResource instantiateResource(Taco taco) {

   return new TacoResource(taco);

 }

 @Override

 public TacoResource toResource(Taco taco) {

   return createResourceWithId(taco.getId(), taco);

 }

}

TacoResourceAssembler имеет конструктор по умолчанию, который сообщает суперклассу (ResourceAssemblerSupport), что он будет использовать DesignTacoController для определения базового пути для любых URL-адресов в ссылках, которые он создает при создании TacoResource.

Метод instantiateResource() переопределяется для создания экземпляра TacoResource с данным Taco. Этот метод необязательный, если TacoResource имеет конструктор по умолчанию. В этом случае, однако, TacoResource  требует для построения Taco, поэтому вы должны переопределить его.

Метод toResource() является единственным методом, строго обязательным при расширении ResourceAssemblerSupport. Здесь вы говорите ему создать объект TacoResource из Taco и автоматически дать ему собственную ссылку с URL-адресом, полученным из свойства id объекта Taco.

На первый взгляд, toResource(), похоже, имеет аналогичное назначение что и instantiateResource(), но они служат несколько иным целям. В то время как instantiateResource() предназначен только для создания экземпляра объекта Resource, метод toResource() предназначен не только для создания объекта Resource, но и для заполнения его ссылками. Под капотом toResource() находиться вызов instantiateResource().

Теперь настройте метод recentTacos(), чтобы использовать TacoResourceAssembler:

@GetMapping("/recent")

public Resources recentTacos() {

 PageRequest page = PageRequest.of(

     0, 12, Sort.by("createdAt").descending());

 List tacos = tacoRepo.findAll(page).getContent();

 List tacoResources =

   new TacoResourceAssembler().toResources(tacos);

 Resources recentResources =

   new Resources(tacoResources);

 recentResources.add(

   linkTo(methodOn(DesignTacoController.class).recentTacos())

       .withRel("recents"));

 return recentResources;

}

Вместо того чтобы возвращать Resources>, recentTacos() теперь возвращает Resources, чтобы воспользоваться вашим новым типом TacoResource. После извлечения тако из репозитория вы передаете список объектов Taco методу toResources() класса TacoResourceAssembler. Этот удобный метод циклически перебирает все объекты Taco, вызывая метод toResource(), который вы переопределили в TacoResourceAssembler, чтобы создать список объектов TacoResource.

Используюя этот список TacoResource вы затем создаете объект Resources, а затем заполняете его ссылкой на recents, как и в предыдущей версии recentTacos().

На этом этапе GET-запрос /design/ recent создаст список тако, каждый из которых имеет self ссылку и recents ссылку в самом списке. Но ингредиенты все равно останутся без ссылок. Чтобы решить эту проблему, вы создадите новый ассемблер ресурсов для ингредиентов:

package tacos.web.api;

import org.springframework.hateoas.mvc.ResourceAssemblerSupport;

import tacos.Ingredient;

 class IngredientResourceAssembler extends

       ResourceAssemblerSupport {

 public IngredientResourceAssembler() {

   super(IngredientController2.class, IngredientResource.class);

 }

 @Override

 public IngredientResource toResource(Ingredient ingredient) {

   return createResourceWithId(ingredient.getId(), ingredient);