В очень запоздалом заключении к моей серии статей о 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
|
@RepositoryRestResourcepublic 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" : { }, "item" : { } } }, ... ] }, "_links" : { "self" : { }, "profile" : { }, "search" : { } }} |
Наряду с простой в демонстрации функциональностью GET , Spring-Data-REST также добавляет возможность PUT (Spring-Data-REST по некоторым причинам решил использовать PUT как для создания, так и для обновления) и DELETE ресурс, а также извлекать ресурс по его идентификатору. Это много функциональности только для двух строк кода!
Разбивка на страницы и сортировка
Ресурсы часто имеют много записей. Обычно вы не хотите возвращать все эти записи по запросу из-за высокой стоимости ресурсов на всех уровнях. Разбиение на страницы — это часто используемое решение для решения этой проблемы, а Spring-Data-REST делает его чрезвычайно простым в реализации.
Другой распространенной потребностью является возможность разрешать клиентам сортировать возвращаемые данные из ресурса, и здесь снова приходит на помощь Spring-Data-REST. Чтобы реализовать эту функциональность с ресурсом Item , нам нужно перейти от расширения CrudRepository к PagingAndSortingRepository следующим образом:
|
1
2
3
|
@RepositoryRestResourcepublic 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" : { }, "next" : { }, "last" : { }, "profile" : { }, "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
|
@RepositoryRestResourcepublic 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" : { "templated" : true }, "findByType" : { "templated" : true }, "findItemsLessThanAndType" : { "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" : { }, "item" : { "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
|
@Configurationpublic 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" : { }, "self" : { "templated" : true } }} |
Таким образом, наш клиентский код теперь может отображать ссылку, позволяющую пользователю легко добавить товар в свою корзину или создать новую корзину и добавить в нее товар.
Выводы
HATEOAS — часто пропускаемая часть спецификации REST, главным образом потому, что на ее реализацию и сопровождение может уйти довольно много времени. Spring-Data-REST и Spring-HATEOAS значительно сокращают как время внедрения, так и время обслуживания, что делает HATEOAS гораздо более практичным для реализации в вашем сервисе RESTful.
Я смог коснуться только некоторых функций, которые Spring-Data-REST и Spring-HATEOAS могут предложить. Для полного описания их соответствующего набора функций, я рекомендую проверить справочные документы, связанные ниже. Если у вас есть какие-либо вопросы или вам нужны дальнейшие объяснения, пожалуйста, не стесняйтесь спрашивать в разделе комментариев ниже.
Дополнительные ресурсы
- http://docs.spring.io/spring-data/rest/docs/2.5.1.RELEASE/reference/html/
- http://docs.spring.io/spring-hateoas/docs/0.19.0.RELEASE/reference/html/
| Ссылка: | Не ненавидите HATEOAS Part Deux: Весна для HATEOAS от нашего партнера по JCG Билли Корандо в блоге Keyhole Software . |