1. 概述

本文将带你入门 JSON-API 规范,并展示如何将其集成到基于 Spring 的 RESTful 接口中。

我们会采用 Java 生态中的 Katharsis 框架来实现 JSON-API。整个过程基于一个标准的 Spring Boot 应用,因此你只需要准备好一个 Spring 项目即可开干。

✅ JSON-API 是一种结构化 JSON 响应格式的规范,强调资源、关系和超媒体链接,适合构建标准化、可预测的 RESTful 接口。


2. Maven 依赖配置

首先,在 pom.xml 中引入 Katharsis 的 Spring 集成依赖:

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

⚠️ 注意版本兼容性。Katharsis 3.x 适用于 Spring Boot 1.x/2.x 早期版本,若使用较新 Spring 版本需评估是否适配或考虑替代方案(如 jsonapi-converter + 手动集成)。


3. 用户资源定义

定义一个 User 实体类,作为 JSON-API 中的资源(Resource):

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

    @JsonApiId
    private Long id;

    private String name;

    private String email;
}

关键点说明:

  • @JsonApiResource(type = "users"):声明该类为 JSON-API 资源,对外暴露的资源类型为 "users"
  • @JsonApiId:标识主键字段,用于唯一确定资源。
  • ❌ 不需要额外继承或实现接口,Katharsis 通过注解自动识别。

持久层使用 Spring Data JPA:

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

简单粗暴,标准写法,无需改动。


4. 资源仓库(Resource Repository)

每个资源需要一个仓库来暴露 CRUD 操作。Katharsis 使用 ResourceRepositoryV2 接口:

@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);
    }
}

📌 踩坑提示:

  • 这个仓库看起来很像 Controller,但它不是。Katharsis 会在内部将其映射为 /users 接口。
  • QuerySpec 是 Katharsis 的查询 DSL,支持分页、排序、过滤等,直接对接 JPA。
  • ResourceList<T> 是包装了资源列表的类型,支持元数据和分页信息。

5. Katharsis 配置

由于使用的是 katharsis-spring,集成非常简单。

在 Spring Boot 主类或配置类上添加:

@Import(KatharsisConfigV3.class)

然后在 application.properties 中配置基本参数:

katharsis.domainName=http://localhost:8080
katharsis.pathPrefix=/
  • domainName:生成链接时使用的根域名。
  • pathPrefix:API 的路径前缀,设为 / 表示根路径。

✅ 配置完成后,以下接口自动生效:

  • GET http://localhost:8080/users:获取所有用户
  • POST http://localhost:8080/users:创建新用户
  • GET http://localhost:8080/users/1:获取 ID 为 1 的用户
  • DELETE http://localhost:8080/users/1:删除用户

无需写一行 Controller,全靠 Katharsis 自动映射。


6. 资源关系处理

JSON-API 的核心优势之一是标准化资源间的关系(Relationships)。

6.1 角色资源定义

新增 Role 资源,并与 User 建立多对多关系:

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

    @JsonApiId
    private Long id;

    private String name;

    @JsonApiRelation
    private Set<User> users;
}

User 类中添加反向关系:

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

📌 关键参数说明:

  • serialize = SerializeType.EAGER:表示该关系在查询用户时自动内联包含(即出现在 included 节点中)。
  • 若设为 LAZY,则只生成链接,不包含数据。

6.2 角色资源仓库

User 类似,为 Role 创建资源仓库:

@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);
    }
}

⚠️ 注意:这个仓库只管理 Role 自身的 CRUD,不处理与 User 的关系


6.3 关系仓库(Relationship Repository)

多对多关系需要单独的关系仓库来管理:

@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<>();
        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;
    }
}

📌 核心方法说明:

  • findManyTargets:查询某个用户的所有角色(GET /users/1/roles
  • addRelations:为用户添加角色(POST /users/1/relationships/roles
  • setRelations:替换用户所有角色(PATCH /users/1/relationships/roles
  • removeRelations:移除用户部分角色(DELETE /users/1/relationships/roles

✅ 这些方法会自动生成标准 JSON-API 关系操作接口。


7. 接口测试与响应示例

获取单个用户(含角色)

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

{
    "data": {
        "type": "users",
        "id": "2",
        "attributes": {
            "email": "tom@example.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:关系链接(self 和 related)
  • included:预加载的关联资源(因 EAGER 模式)

获取所有角色

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": []
}

📌 特点:

  • data 是资源数组
  • included 为空,因为没有主动加载关联数据

8. 总结

JSON-API 是一个极具潜力的规范,它为 RESTful 接口提供了:

  • ✅ 标准化的响应结构
  • ✅ 清晰的资源与关系定义
  • ✅ 内置分页、排序、过滤支持
  • ✅ 超媒体链接(HATEOAS)友好

Katharsis 作为 Java 端的实现,虽然项目活跃度有所下降,但在 Spring 环境下集成简单,适合快速构建符合 JSON-API 的后端服务。

📌 项目源码已托管至 GitHub:https://github.com/eugenp/tutorials/tree/master/spring-katharsis
可直接导入为 Maven 项目运行验证。

⚠️ 建议:在生产环境使用前,评估 Katharsis 的维护状态,或考虑使用其他现代替代方案(如手动实现 + jsonapi-converter 库)。但对于学习 JSON-API 概念,它依然是个不错的起点。


原始标题:JSON API in a Java Web Application