Перейти к основному содержимому

JSON API в приложении Spring

· 5 мин. чтения

1. Обзор

В этой статье мы начнем изучать спецификацию JSON-API и то, как ее можно интегрировать в REST API, поддерживаемый Spring.

Мы будем использовать реализацию JSON-API Katharsis на Java — и мы настроим приложение Spring на основе Katharsis — так что все, что нам нужно, — это приложение Spring.

2. Мавен

Во-первых, давайте посмотрим на нашу конфигурацию maven — нам нужно добавить следующую зависимость в наш pom.xml :

<dependency>
<groupId>io.katharsis</groupId>
<artifactId>katharsis-spring</artifactId>
<version>3.0.2</version>
</dependency>

3. Пользовательский ресурс

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

@JsonApiResource(type = "users")
public class User {

@JsonApiId
private Long id;

private String name;

private String email;
}

Обратите внимание, что:

  • Аннотация @JsonApiResource используется для определения пользователя нашего ресурса.
  • Аннотация @JsonApiId используется для определения идентификатора ресурса.

И очень кратко — постоянством для этого примера будет репозиторий Spring Data:

public interface UserRepository extends JpaRepository<User, Long> {}

4. Репозиторий ресурсов

Далее давайте обсудим наш репозиторий ресурсов — каждый ресурс должен иметь ResourceRepositoryV2 для публикации доступных на нем операций API:

@Component
public class UserResourceRepository implements ResourceRepositoryV2<User, Long> {

@Autowired
private UserRepository userRepository;

@Override
public User findOne(Long id, QuerySpec querySpec) {
Optional<User> user = userRepository.findById(id);
return user.isPresent()? user.get() : null;
}

@Override
public ResourceList<User> findAll(QuerySpec querySpec) {
return querySpec.apply(userRepository.findAll());
}

@Override
public ResourceList<User> findAll(Iterable<Long> ids, QuerySpec querySpec) {
return querySpec.apply(userRepository.findAllById(ids));
}

@Override
public <S extends User> S save(S entity) {
return userRepository.save(entity);
}

@Override
public void delete(Long id) {
userRepository.deleteById(id);
}

@Override
public Class<User> getResourceClass() {
return User.class;
}

@Override
public <S extends User> S create(S entity) {
return save(entity);
}
}

Небольшое замечание — это, конечно, очень похоже на контроллер Spring .

5. Конфигурация катарсиса

Поскольку мы используем katharsis-spring , все, что нам нужно сделать, это импортировать KatharsisConfigV3 в наше приложение Spring Boot:

@Import(KatharsisConfigV3.class)

И настраиваем параметры Катарсиса в нашем application.properties :

katharsis.domainName=http://localhost:8080
katharsis.pathPrefix=/

Теперь мы можем начать использовать API; Например:

  • ПОЛУЧИТЬ « http://localhost:8080/users »: чтобы получить всех пользователей.
  • POST « http://localhost:8080/users »: добавить нового пользователя и т. д.

6. Отношения

Далее давайте обсудим, как обрабатывать отношения сущностей в нашем JSON API.

6.1. Ролевой ресурс

Во-первых, давайте представим новый ресурс — Роль :

@JsonApiResource(type = "roles")
public class Role {

@JsonApiId
private Long id;

private String name;

@JsonApiRelation
private Set<User> users;
}

Затем настройте отношение «многие ко многим» между User и Role :

@JsonApiRelation(serialize=SerializeType.EAGER)
private Set<Role> roles;

6.2. Репозиторий ресурсов ролей

Очень быстро — вот наш репозиторий ресурсов ролей :

@Component
public class RoleResourceRepository implements ResourceRepositoryV2<Role, Long> {

@Autowired
private RoleRepository roleRepository;

@Override
public Role findOne(Long id, QuerySpec querySpec) {
Optional<Role> role = roleRepository.findById(id);
return role.isPresent()? role.get() : null;
}

@Override
public ResourceList<Role> findAll(QuerySpec querySpec) {
return querySpec.apply(roleRepository.findAll());
}

@Override
public ResourceList<Role> findAll(Iterable<Long> ids, QuerySpec querySpec) {
return querySpec.apply(roleRepository.findAllById(ids));
}

@Override
public <S extends Role> S save(S entity) {
return roleRepository.save(entity);
}

@Override
public void delete(Long id) {
roleRepository.deleteById(id);
}

@Override
public Class<Role> getResourceClass() {
return Role.class;
}

@Override
public <S extends Role> S create(S entity) {
return save(entity);
}
}

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

6.3. Репозиторий отношений

Чтобы обрабатывать отношения «многие ко многим» между пользователем и ролью , нам нужно создать новый стиль репозитория:

@Component
public class UserToRoleRelationshipRepository implements RelationshipRepositoryV2<User, Long, Role, Long> {

@Autowired
private UserRepository userRepository;

@Autowired
private RoleRepository roleRepository;

@Override
public void setRelation(User User, Long roleId, String fieldName) {}

@Override
public void setRelations(User user, Iterable<Long> roleIds, String fieldName) {
Set<Role> roles = new HashSet<Role>();
roles.addAll(roleRepository.findAllById(roleIds));
user.setRoles(roles);
userRepository.save(user);
}

@Override
public void addRelations(User user, Iterable<Long> roleIds, String fieldName) {
Set<Role> roles = user.getRoles();
roles.addAll(roleRepository.findAllById(roleIds));
user.setRoles(roles);
userRepository.save(user);
}

@Override
public void removeRelations(User user, Iterable<Long> roleIds, String fieldName) {
Set<Role> roles = user.getRoles();
roles.removeAll(roleRepository.findAllById(roleIds));
user.setRoles(roles);
userRepository.save(user);
}

@Override
public Role findOneTarget(Long sourceId, String fieldName, QuerySpec querySpec) {
return null;
}

@Override
public ResourceList<Role> findManyTargets(Long sourceId, String fieldName, QuerySpec querySpec) {
final Optional<User> userOptional = userRepository.findById(sourceId);
User user = userOptional.isPresent() ? userOptional.get() : new User();
return querySpec.apply(user.getRoles());
}

@Override
public Class<User> getSourceResourceClass() {
return User.class;
}

@Override
public Class<Role> getTargetResourceClass() {
return Role.class;
}
}

Здесь, в репозитории отношений, мы игнорируем единичные методы.

7. Тест

Наконец, давайте проанализируем несколько запросов и действительно поймем, как выглядит вывод JSON-API.

Мы собираемся начать извлечение одного пользовательского ресурса (с id = 2):

ПОЛУЧИТЬ http://localhost:8080/users/2

{
"data":{
"type":"users",
"id":"2",
"attributes":{
"email":"tom@test.com",
"username":"tom"
},
"relationships":{
"roles":{
"links":{
"self":"http://localhost:8080/users/2/relationships/roles",
"related":"http://localhost:8080/users/2/roles"
}
}
},
"links":{
"self":"http://localhost:8080/users/2"
}
},
"included":[
{
"type":"roles",
"id":"1",
"attributes":{
"name":"ROLE_USER"
},
"relationships":{
"users":{
"links":{
"self":"http://localhost:8080/roles/1/relationships/users",
"related":"http://localhost:8080/roles/1/users"
}
}
},
"links":{
"self":"http://localhost:8080/roles/1"
}
}
]
}

Выводы:

  • Основные атрибуты ресурса находятся в data.attributes.
  • Основные отношения Ресурса находятся в data.relationships.
  • Поскольку мы использовали @JsonApiRelation(serialize=SerializeType.EAGER) для отношения ролей , он включен в JSON и находится в включенном узле.

Далее — получаем ресурс коллекции, содержащий Роли:

ПОЛУЧИТЬ http://localhost:8080/роли

{
"data":[
{
"type":"roles",
"id":"1",
"attributes":{
"name":"ROLE_USER"
},
"relationships":{
"users":{
"links":{
"self":"http://localhost:8080/roles/1/relationships/users",
"related":"http://localhost:8080/roles/1/users"
}
}
},
"links":{
"self":"http://localhost:8080/roles/1"
}
},
{
"type":"roles",
"id":"2",
"attributes":{
"name":"ROLE_ADMIN"
},
"relationships":{
"users":{
"links":{
"self":"http://localhost:8080/roles/2/relationships/users",
"related":"http://localhost:8080/roles/2/users"
}
}
},
"links":{
"self":"http://localhost:8080/roles/2"
}
}
],
"included":[

]
}

Быстрый вывод здесь заключается в том, что мы получаем все роли в системе — в виде массива в узле данных .

8. Заключение

JSON-API — это фантастическая спецификация, которая, наконец, добавляет некоторую структуру в то, как мы используем JSON в наших API, и действительно обеспечивает настоящий Hypermedia API.

В этой части был рассмотрен один из способов его настройки в приложении Spring. Но независимо от этой реализации, сама спецификация, на мой взгляд, является очень многообещающей работой.

Полный исходный код примера доступен на GitHub . Это проект Maven, который можно импортировать и запускать как есть.