API JSON em um aplicativo Spring

API JSON em um aplicativo Spring

1. Visão geral

Neste artigo, vamos começar a explorarthe JSON-API spec e como isso pode ser integrado a uma API REST apoiada por Spring.

Usaremos a implementaçãoKatharsis de JSON-API em Java - e configuraremos um aplicativo Spring baseado em Katharsis - então tudo que precisamos é um aplicativo Spring.

2. Maven

Primeiro, vamos dar uma olhada em nossa configuração de maven - precisamos adicionar a seguinte dependência em nossopom.xml:


    io.katharsis
    katharsis-spring
    3.0.2

3. Um recurso de usuário

A seguir, vamos dar uma olhada em nosso recurso de usuário:

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

    @JsonApiId
    private Long id;

    private String name;

    private String email;
}

Observe que:

  • A anotação@JsonApiResource é usada para definir nosso recursoUser

  • A anotação@JsonApiId é usada para definir o identificador do recurso

E muito brevemente - a persistência para este exemplo será um repositório Spring Data aqui:

public interface UserRepository extends JpaRepository {}

4. Um Repositório de Recursos

A seguir, vamos discutir nosso repositório de recursos - cada recurso deve ter umResourceRepositoryV2 para publicar as operações de API disponíveis nele:

@Component
public class UserResourceRepository implements ResourceRepositoryV2 {

    @Autowired
    private UserRepository userRepository;

    @Override
    public User findOne(Long id, QuerySpec querySpec) {
        return userRepository.findOne(id);
    }

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

    @Override
    public ResourceList findAll(Iterable ids, QuerySpec querySpec) {
        return querySpec.apply(userRepository.findAll(ids));
    }

    @Override
    public  S save(S entity) {
        return userRepository.save(entity);
    }

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

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

    @Override
    public  S create(S entity) {
        return save(entity);
    }
}

Uma nota rápida aqui - é clarovery similar to a Spring controller.

5. Configuração Katharsis

Como estamos usandokatharsis-spring, tudo o que precisamos fazer é importarKatharsisConfigV3 em nosso aplicativo Spring Boot:

@Import(KatharsisConfigV3.class)

E configure os parâmetros do Katharsis em nossoapplication.properties:

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

Com isso - agora podemos começar a consumir a API; por exemplo:

6. Relacionamentos

A seguir, vamos discutir como lidar com relacionamentos de entidades em nossa API JSON.

6.1. Recurso de Função

Primeiro, vamos apresentar um novo recurso -Role:

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

    @JsonApiId
    private Long id;

    private String name;

    @JsonApiRelation
    private Set users;
}

E, em seguida, configure uma relação muitos para muitos entreUser eRole:

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

6.2. Repositório de recursos de funções

Muito rapidamente - aqui está o nosso repositório de recursosRole:

@Component
public class RoleResourceRepository implements ResourceRepositoryV2 {

    @Autowired
    private RoleRepository roleRepository;

    @Override
    public Role findOne(Long id, QuerySpec querySpec) {
        return roleRepository.findOne(id);
    }

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

    @Override
    public ResourceList findAll(Iterable ids, QuerySpec querySpec) {
        return querySpec.apply(roleRepository.findAll(ids));
    }

    @Override
    public  S save(S entity) {
        return roleRepository.save(entity);
    }

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

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

    @Override
    public  S create(S entity) {
        return save(entity);
    }
}

É importante entender aqui que este repo de recurso único não lida com o aspecto do relacionamento - que leva um repositório separado.

6.3. Repositório de Relacionamento

Para lidar com a relação muitos para muitos entreUser -Role, precisamos criar um novo estilo de repositório:

@Component
public class UserToRoleRelationshipRepository implements RelationshipRepositoryV2 {

    @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 roleIds, String fieldName) {
        Set roles = new HashSet();
        roles.addAll(roleRepository.findAll(roleIds));
        user.setRoles(roles);
        userRepository.save(user);
    }

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

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

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

    @Override
    public ResourceList findManyTargets(Long sourceId, String fieldName, QuerySpec querySpec) {
        User user = userRepository.findOne(sourceId);
        return  querySpec.apply(user.getRoles());
    }

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

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

Estamos ignorando os métodos singulares aqui, no repositório de relacionamento.

7. Test

Por fim, vamos analisar algumas solicitações e realmente entender a aparência da saída JSON-API.

Vamos começar a recuperar um único recurso de usuário (com id = 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"
            }
        }
    ]
}

Aprendizado:

  • Os principais atributos do Recurso são encontrados emdata.attributes

  • Os principais relacionamentos do Recurso são encontrados emdata.relationships

  • Como usamos@JsonApiRelation(serialize=SerializeType.EAGER) para o relacionamentoroles, ele é incluído no JSON e encontrado no nóincluded

A seguir - vamos obter o recurso de coleta que contém as funções:

{
    "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":[

    ]
}

A conclusão rápida aqui é que obtemos todas as funções no sistema - como uma matriz no nódata

8. Conclusão

A JSON-API é uma especificação fantástica - finalmente adicionando alguma estrutura na maneira como usamos o JSON em nossas APIs e realmente potenciando uma verdadeira API Hypermedia.

Esta peça explorou uma maneira de configurá-la em um aplicativo Spring. Mas, independentemente dessa implementação, as especificações em si são - na minha opinião - um trabalho muito, muito promissor.

O código-fonte completo para o exemplo está disponível emon GitHub. É um projeto Maven que pode ser importado e executado como está.