Статьи

Не ненавидите HATEOAS Part Deux: Весна для HATEOAS

В очень запоздалом заключении к моей серии статей о HATEOAS мы рассмотрим, как реализовать HATEOAS с использованием Spring-Data-REST и Spring-HATEOAS. Это весна для HATEOAS!

Я собрал работающий проект, который продемонстрирует примеры кода, которые у меня есть ниже, а также несколько других функций. Проект можно найти здесь: https://github.com/in-the-keyhole/hateoas-demo-II . Требуются JDK 8 и Maven, но для запуска проекта не требуется никаких внешних зависимостей.

Обслуживание ресурса

Взаимодействие с веб-сервисом через его ресурсы является одним из основных конструктивных ограничений REST. Используя Spring-Data и Spring-MVC, не так уж сложно начать обслуживать ресурс. Вам нужно будет добавить Repository для объекта, который вы хотите обслуживать, и реализовать контроллер для его обслуживания. Spring-Data-REST, однако, делает этот процесс еще проще и обеспечивает более богатый ресурс в процессе (т. Е. Добавление гипермедиа разметки).

1
2
3
@RepositoryRestResource
public interface ItemRepo extends CrudRepository<Item, Long> {
}

И это так просто. Если вы загрузите свое приложение Spring-Boot и перейдете по http://localhost:8080/items (а также выполнили некоторые другие необходимые конфигурации ), вы должны получить возврат JSON, который выглядит примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
  "_embedded" : {
    "items" : [ {
      "name" : "Independence Day",
      "description" : "Best. Movie. Speech. Ever!",
      "price" : 10.0,
      "type" : "Movies",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/items/21"
        },
        "item" : {
          "href" : "http://localhost:8080/api/items/21"
        }
      }
    },
    ...
    ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/items/"
    },
    "profile" : {
    },
    "search" : {
      "href" : "http://localhost:8080/items/search"
    }
  }
}

Наряду с простой в демонстрации функциональностью GET , Spring-Data-REST также добавляет возможность PUT (Spring-Data-REST по некоторым причинам решил использовать PUT как для создания, так и для обновления) и DELETE ресурс, а также извлекать ресурс по его идентификатору. Это много функциональности только для двух строк кода!

Разбивка на страницы и сортировка

Ресурсы часто имеют много записей. Обычно вы не хотите возвращать все эти записи по запросу из-за высокой стоимости ресурсов на всех уровнях. Разбиение на страницы — это часто используемое решение для решения этой проблемы, а Spring-Data-REST делает его чрезвычайно простым в реализации.

Другой распространенной потребностью является возможность разрешать клиентам сортировать возвращаемые данные из ресурса, и здесь снова приходит на помощь Spring-Data-REST. Чтобы реализовать эту функциональность с ресурсом Item , нам нужно перейти от расширения CrudRepository к PagingAndSortingRepository следующим образом:

1
2
3
@RepositoryRestResource
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
}

При перезапуске приложения и возврате к http://localhost:8080/items наши возвраты изначально выглядят одинаково, но в нижней части страницы мы видим несколько новых объектов JSON:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  ...   
  "_links" : {
    "first" : {
    },
    "self" : {
      "href" : "http://localhost:8080/items"
    },
    "next" : {
    },
    "last" : {
    },
    "profile" : {
    },
    "search" : {
      "href" : "http://localhost:8080/items/search"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 23,
    "totalPages" : 2,
    "number" : 0
  }
}

Spring-Data-REST отображает элементы управления гипермедиа для навигации по страницам возвратов для ресурса; last, next, prev и first, когда это применимо (примечание: Spring-Data-REST использует массив на основе 0 для разбивки на страницы). Если вы посмотрите внимательно, вы также заметите, как Spring-Data-REST позволяет клиенту манипулировать количеством возвратов на страницу ( .../items?size=x ). Наконец, сортировка также была добавлена ​​и может быть выполнена с параметрами URL: .../items?sort=name&name.dir=desc .

Поиск ресурса

Таким образом, мы обслуживаем ресурс, разбиваем на страницы результаты и позволяем клиентам сортировать эти результаты. Все это очень полезно, но часто клиенты захотят выполнить поиск определенного подмножества ресурса. Это еще одна задача, которую Spring-Data-REST делает чрезвычайно простой.

01
02
03
04
05
06
07
08
09
10
11
12
13
@RepositoryRestResource
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
 
    List<Item> findByType(@Param("type") String type);
 
    @RestResource(path = "byMaxPrice")
    @Query("SELECT i FROM Item i WHERE i.price <= :maxPrice")
    List<Item> findItemsLessThan(@Param("maxPrice") double maxPrice);
 
    @RestResource(path = "byMaxPriceAndType")
    @Query("SELECT i FROM Item i WHERE i.price <= :maxPrice AND i.type = :type")
    List<Item> findItemsLessThanAndType(@Param("maxPrice") double maxPrice, @Param("type") String type);
}

Выше несколько запросов, по которым пользователи могут искать элементы: тип элемента, максимальная цена элемента, а затем эти два параметра объединены. При переходе по http://localhost:8080/items/search Spring-Data-REST отображает все доступные параметры поиска, а также способы их взаимодействия. Функции пагинации и сортировки, доступные в конечной точке корневого ресурса, включаются также и при взаимодействии с конечными точками поиска!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
...
    "findItemsLessThan" : {
      "href" : "http://localhost:8080/items/search/byMaxPrice{?maxPrice}",
      "templated" : true
    },
    "findByType" : {
      "href" : "http://localhost:8080/items/search/findByType{?type}",
      "templated" : true
    },
    "findItemsLessThanAndType" : {
      "href" : "http://localhost:8080/items/search/byMaxPriceAndType{?maxPrice,type}",
      "templated" : true
    },
...

Изменение формы ресурса

Будут времена, когда выгодно изменить форму объекта, которому служит конечная точка; Вы можете сгладить дерево объектов, скрыть поля или изменить имя полей, чтобы сохранить контракт. Spring-Data-REST предлагает функциональность для управления формой с помощью проекций.

Сначала нам нужно создать интерфейс и аннотировать его с помощью @Projection :

1
2
3
4
5
@Projection(name = "itemSummary", types = { Item.class })
public interface ItemSummary {
    String getName();
    String getPrice();
}

Это позволит Spring-Data-REST обслуживать нашу сущность Item в форме ItemSummary по запросу: http://localhost:8080/api/items/1?projection=itemSummary . Если мы хотим сделать ItemSummary по умолчанию, мы возвращаемся при достижении конечной точки /items чего можно добиться, добавив excerptProjectio n к аннотации ItemRepo .

1
2
@RepositoryRestResource(excerptProjection = ItemSummary.class)
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {

Теперь, когда мы ../items , наши результаты выглядят так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
...
{
      "name" : "Sony 55 TV",
      "price" : "1350.0",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/items/2"
        },
        "item" : {
          "href" : "http://localhost:8080/api/items/2{?projection}",
          "templated" : true
        }
      }
}
...

Настройка конечной точки ресурса

Имя объекта не всегда может быть желательным в качестве имени конечной точки ресурса; он может не соответствовать унаследованным потребностям, может потребоваться префикс конечной точки ресурса или просто требуется другое имя. Spring-Data-REST предлагает крючки для всех этих потребностей.

Для изменения названия ресурса:

1
2
3
@RepositoryRestResource(collectionResourceRel = "merchandise", path = "merchandise")
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
}

И добавив базовый путь:

1
2
3
4
5
6
7
8
9
@Configuration
public class RestConfiguration extends RepositoryRestConfigurerAdapter {
 
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.setBasePath("api");
    }
 
}

Теперь вместо сущностей Item, обслуживаемых в ../items , они будут обслуживаться из ../api/merchandise .

Обеспечение ресурса

Безопасность — очень важная и сложная тема. Даже целые разговоры едва царапают поверхность. Так что считайте эту часть незначительным ссадиной на эту тему.

Скрытие полей

Как упоминалось в предыдущем разделе, проекции являются одним из способов скрытия полей. Другой, более безопасный способ — использовать @JsonIgnore в поле, как @JsonIgnore ниже, чтобы предотвратить его возвращение:

1
2
3
4
5
public class Item implements Serializable, Identifiable<Long> {
    @JsonIgnore
    @Column(name = "secret_field")
    private String secretField;
}

Ограничение доступа по HTTP

Могут быть случаи, когда функциональность вообще не должна быть доступна через HTTP, независимо от того, кто вы. Этого можно достичь с помощью @RestResource(exported = false) , который сообщает Spring-Data-REST, что вообще не публикуйте этот ресурс или часть ресурса в Интернете. Это может быть установлено как на уровне типа, так и на уровне метода. Уровень Типа также может быть переопределен на уровне Метода, если вы хотите широко отрицать, но затем явно определить, что должно быть доступно.

Уровень метода:

1
2
3
4
5
6
public interface OrderRepo extends CrudRepository<Order, Long> {
 
    @Override
    @RestResource(exported = false)
    <S extends Order> S save(S entity);
}

Тип уровня с переопределением уровня метода:

1
2
3
4
5
6
7
@RestResource(exported = false)
public interface OrderRepo extends CrudRepository<Order, Long> {
 
    @Override
    @RestResource(exported = true)
    <S extends Order> S save(S entity);
}

Альтернативный метод (если вы того пожелаете) — вместо этого расширить интерфейс репозитория и определить только те методы, к которым у клиентов должен быть доступ.

1
2
3
4
5
public interface PaymentRepo extends Repository<Payment, Long> {
    Payment findOne(Long id);
 
    <S extends Payment> S save(S entity);
}

Ограничение доступа по роли

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

1
2
3
4
5
6
7
8
@RepositoryRestResource(collectionResourceRel = "merchandise", path = "merchandise")
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
    @PreAuthorize("hasRole('ADMIN')")
    <S extends Item> S save(S entity);
 
    @PreAuthorize("hasRole('ADMIN')")
    <S extends Item> Iterable<S> save(Iterable<S> entities);
}

Хотя я не думаю, что это строго необходимо, из-за некоторого необычного взаимодействия, возможно, с фильтрами Spring-MVC, для обеспечения безопасности на основе ролей требуется некоторая дополнительная конфигурация URL. (Я потратил много часов на изучение этой проблемы.) Однако в любом случае реализация нескольких уровней безопасности, как правило, является хорошей практикой, так что это тоже не обязательно неправильно:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN")//
                .and().withUser("user").password("password").roles("USER");
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .antMatcher("/merchandise").authorizeRequests().antMatchers(HttpMethod.POST).hasAnyRole("ADMIN")//
                .and().antMatcher("/merchandise").authorizeRequests().antMatchers(HttpMethod.PUT).hasAnyRole("ADMIN")//
                .and().antMatcher("/**").authorizeRequests().antMatchers(HttpMethod.DELETE).denyAll()//
                .and().antMatcher("/merchandise").authorizeRequests().antMatchers(HttpMethod.GET).permitAll()//
                .and().antMatcher("/**").authorizeRequests().anyRequest().authenticated()
                .and().httpBasic();
    }
}

Как и @RestResource , @PreAuthorize также может быть размещен на уровне типа и переопределен на уровне метода.

1
2
3
@PreAuthorize("hasRole('USER')")
public interface OrderRepo extends CrudRepository<Order, Long> {
}

Дополнительная настройка с Spring-HATEOAS

До этого момента я демонстрировал все возможности Spring-Data-REST и то, как он делает внедрение службы HATEOAS быстрым. Увы, есть ограничения на то, что вы можете делать с Spring-Data-REST. К счастью, есть еще один проект Spring, Spring-HATEOAS , который оттуда взялся за дело.

Spring-HATEOAS облегчает процесс добавления гипермедиа разметки к ресурсу и полезен для обработки пользовательских взаимодействий между ресурсами. Например, добавление товара в заказ:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@RequestMapping("/{id}")
public ResponseEntity<Resource<Item>> viewItem(@PathVariable String id) {
  Item item = itemRepo.findOne(Long.valueOf(id));
   
  Resource<Item> resource = new Resource<Item>(item);
  if (hasExistingOrder()) {
    // Provide a link to an existing Order
    resource.add(entityLinks.linkToSingleResource(retrieveExistingOrder()).withRel("addToCart"));
  } else {
    // Provide a link to create a new Order
    resource.add(entityLinks.linkFor(Order.class).withRel("addToCart"));
  }
  resource.add(entityLinks.linkToSingleResource(item).withSelfRel());
  return ResponseEntity.ok(resource);
}

При этом мы перезаписали функциональность по умолчанию /merchandise/(id) которую предоставляет Spring-Data-REST, и теперь вернем этот результат:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
{
  "name" : "Samsung 55 TV",
  "description" : "Samsung 55 LCD HD TV",
  "price" : 1500.0,
  "type" : "Electronics",
  "_links" : {
    "addToCart" : {
      "href" : "http://localhost:8080/api/orders"
    },
    "self" : {
      "href" : "http://localhost:8080/api/merchandise/1{?projection}",
      "templated" : true
    }
  }
}

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

Выводы

HATEOAS — часто пропускаемая часть спецификации REST, главным образом потому, что на ее реализацию и сопровождение может уйти довольно много времени. Spring-Data-REST и Spring-HATEOAS значительно сокращают как время внедрения, так и время обслуживания, что делает HATEOAS гораздо более практичным для реализации в вашем сервисе RESTful.

Я смог коснуться только некоторых функций, которые Spring-Data-REST и Spring-HATEOAS могут предложить. Для полного описания их соответствующего набора функций, я рекомендую проверить справочные документы, связанные ниже. Если у вас есть какие-либо вопросы или вам нужны дальнейшие объяснения, пожалуйста, не стесняйтесь спрашивать в разделе комментариев ниже.

Дополнительные ресурсы

Ссылка: Не ненавидите HATEOAS Part Deux: Весна для HATEOAS от нашего партнера по JCG Билли Корандо в блоге Keyhole Software .