«1. Обзор

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

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

2. Maven

Во-первых, давайте посмотрим на нашу конфигурацию 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

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

@Import(KatharsisConfigV3.class)

И настроить параметры Katharsis в нашем application.properties: ~ ~~

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

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

    GET «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;
}

А затем настроим отношение «многие ко многим» между пользователем и ролью:

@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):

GET http://localhost:8080/users/2

{
    "data":{
        "type":"users",
        "id":"2",
        "attributes":{
            "email":"[email protected]",
            "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 и находятся в включенном узле ~ ~~ Далее — давайте получим ресурс коллекции, содержащий роли:

GET http://localhost:8080/roles


Быстрый вывод: мы получаем все роли в системе â – в виде массива в узле данных

{
    "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, который можно импортировать и запускать как есть.

«