Статьи

Построение HATEOAS API с помощью JAX-RS и Spring

В моем предыдущем сообщении в блоге я показал, как легко настроить Джерси с помощью Spring Boot. Мои исследования Spring Boot и Jersey не закончились, и я исследовал возможность использования Spring HATEOAS вместе с Jersey в приложении Spring Boot. Spring HATEOS позволяет создавать представления REST, которые следуют принципу HATEOAS и (на момент написания этой статьи) имеют базовую поддержку JAX-RS для работы со ссылками. В этом посте я поделюсь некоторыми примерами того, как я интегрировал Spring HATEOAS с Jersey в приложение Spring Boot.

Вступление

В качестве основы для этой статьи я использовал созданный ранее пример: ( https://github.com/kolorobot/spring-boot-jersey-demo ).

Чтобы начать работу с Spring HATEOAS, я добавил действительную зависимость в build.gradle :

1
compile("org.springframework.hateoas:spring-hateoas:0.16.0.RELEASE")

Быстрый подход с помощником Resources

Самый быстрый подход к генерации представления объекта сущности ( Customer ) — использование помощников Spring HATEOAS Resource and Resources . Последние обертывают коллекцию сущностей, возвращаемых CustomerRepository . Для генерации ссылки я использовал JaxRsLinkBuilder который помогает JaxRsLinkBuilder ссылки на ресурсы с ресурсами JAX-RS, обнаруживая пути на @Path аннотации @Path .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@Component
@Path("/customer")
@Produces(MediaType.APPLICATION_JSON)
public class CustomerController {
 
    @Inject
    private CustomerRepository customerRepository;
 
    @GET
    public Response findAll() {
        Resources<Customer> resources = new Resources<>(
                customerRepository.findAll(),
                JaxRsLinkBuilder
                        .linkTo(CustomerController.class)
                        .withSelfRel()
        );
        return Response.ok(resources).build();
    }

Результатом вызова вышеупомянутого метода будет ресурс коллекции со ссылкой на себя:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
{
  "links": [
    {
      "rel": "self",
      "href": "http://localhost:8080/customer"
    }
  ],
  "content": [
    {
      "id": 1,
      "firstname": "Dave",
      "lastname": "Matthews",
      "emailAddress": {
        "value": "[email protected]"
      }
    }
  ]
}

Создание представлений с помощью класса ResourceAssemblerSupport

Помощники Resource , Resources , PagedResources очень удобны, но в некоторых ситуациях требуется больший контроль над созданными ресурсами.

Для создания пользовательского объекта переноса из сущности можно использовать базовый класс ResourceSupport :

1
2
3
4
5
6
public class CustomerResource extends ResourceSupport {
 
    private String fullName;
    private String email;
 
}

Чтобы собрать CustomerResource из сущности и автоматически добавить к нему ссылку на себя, следует использовать класс ResourceAssemblerSupport . В основном этот класс отвечает за создание экземпляра ресурса и добавление ссылки с rel self, указывающей на ресурс:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public class CustomerResourceAssembler extends ResourceAssemblerSupport<Customer, CustomerResource> {
 
    public CustomerResourceAssembler() {
        super(CustomerController.class, CustomerResource.class);
    }
 
    @Override
    public CustomerResource toResource(Customer entity) {
            CustomerResource resource = createResourceWithId(
                    entity.getId(),
                    entity
            );
 
            // initialize the resource       
 
            return resource;
    }
}

Проблема, с которой я столкнулся в приведенном выше коде, заключается в том, что класс ResourceAssemblerSupport внутренне использует построитель ссылок для построения ссылок на контроллеры Spring MVC ( ControllerLinkBuilder ). Это приводит к тому, что ссылки являются недействительными.

Я не нашел другого способа, кроме создания нового класса поддержки, который расширяется из ResourceAssemblerSupport и переопределяет поведение его родителя:

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
public abstract class JaxRsResourceAssemblerSupport<T, D extends ResourceSupport>
        extends ResourceAssemblerSupport<T, D> {
 
    private final Class<?> controllerClass;
 
    public JaxRsResourceAssemblerSupport(
            Class<?> controllerClass, Class<D> resourceType) {
 
        super(controllerClass, resourceType);
        this.controllerClass = controllerClass;
    }
 
    @Override
    protected D createResourceWithId(Object id, T entity, Object... parameters) {
        Assert.notNull(entity);
        Assert.notNull(id);
 
        D instance = instantiateResource(entity);
 
        instance.add(
                JaxRsLinkBuilder.linkTo(controllerClass, parameters)
                        .slash(id)
                        .withSelfRel());
        return instance;
    }
}

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

Мой ассемблер расширяется от недавно созданного JaxRsResourceAssemblerSupport :

1
2
3
4
public class CustomerResourceAssembler
        extends JaxRsResourceAssemblerSupport<Customer, CustomerResource> {
 
}

Наконец, я мог изменить метод контроллера, чтобы он возвращал ресурсы, собранные моим ассемблером. Обратите внимание, что ResourceAssemblerSupport предоставляет удобный метод для преобразования всех данных объектов в ресурсы:

01
02
03
04
05
06
07
08
09
10
@GET
@Path("/resources")
public Response findAll() {
    Iterable<Customer> customers = customerRepository.findAll();
 
    CustomerResourceAssembler assembler = new CustomerResourceAssembler();
    List<CustomerResource> resources = assembler.toResources(customers);
 
    return Response.ok(wrapped).build();
}

Чтобы добавить ссылку с собственной ссылкой на ресурс коллекции, мне нужно было обернуть ее, используя ранее упомянутый класс Resources :

1
2
3
4
5
6
7
// wrap to add link
Resources<CustomerResource> wrapped = new Resources<>(resources);
wrapped.add(
        JaxRsLinkBuilder
                .linkTo(CustomerController.class)
                .withSelfRel()
);

Теперь возвращаемое представление выглядит более HATEOAS:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
{
  "links": [
    {
      "rel": "self",
      "href": "http://localhost:8080/customer"
    }
  ],
  "content": [
    {
      "fullName": "Matthews, Dave",
      "email": "[email protected]",
      "links": [
        {
          "rel": "self",
          "href": "http://localhost:8080/customer/1"
        }
      ]
    }
  ]
}

Использование LinksBuilder

Интерфейс EntityLinks предоставляет API для создания ссылок на основе типа сущности и доступен для внедрения зависимостей, когда @EnableEntityLinks или @EnableHypermadiaSupport используются с @ExposesResourceFor . @ExposesResourceFor какой тип сущности управляется контроллером Spring MVC или ресурсом JAX-RS.

В классе конфигурации нам нужно активировать ссылки на сущности:

1
2
3
4
5
@SpringBootApplication
@EnableEntityLinks
public class Application {
 
}

Примечание. Обратите внимание, что при использовании ссылок на сущности и @EnableEntityLinks на пути к классам должна быть @EnableEntityLinks следующая зависимость:

1
compile("org.springframework.plugin:spring-plugin-core:1.1.0.RELEASE")

Любой ресурс JAX-RS, поддерживающий тип объекта, должен быть помечен @ExposesResourceFor , чтобы можно было EntityLinks :

1
2
3
4
5
@ExposesResourceFor(Customer.class)
public class CustomerController {
    @Inject
    private EntityLinks entityLinks;
}

По EntityLinks интерфейс EntityLinks предоставляет методы, возвращающие ссылки на ресурс коллекции или отдельный ресурс. Пример:

1
2
3
Link selfRel = entityLinks.linkToSingleResource(
        Customer.class, customer.getId()
).withSelfRel();

Резюме

Spring HATEOAS — это не единственный вариант создания HATEOAS API с использованием JAX-RS и Jersey, но с возможностью включения Jersey в приложение Spring Boot Spring HATEOAS может быть хорошим дополнением, особенно если он разработан с учетом JAX-RS.

Примечание: эта статья — только исследование, которое я провел относительно описанной темы. Я не использовал этот подход ни в одном проекте.

Ресурсы