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 概念,它依然是个不错的起点。