В очень запоздалом заключении к моей серии статей о 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" : { }, "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
|
@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" : { }, "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
|
@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" : { "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
|
@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" : { }, "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 . |