【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
本指南以 howtographql 仓库中 Java 后端教程的开篇章节为核心,系统讲解 GraphQL 服务器在 Java 生态中的定位、schema 驱动的开发范式,以及 Java 静态类型系统与 GraphQL 类型系统的天然契合点。读完本篇,你将理解 GraphQL 服务器的本质(解析、校验、执行三层职责)、schema-first 与 code-first 两种 Schema 构建路线的取舍,并获得贯穿本教程后续章节(查询、变更、认证、过滤、分页、实时订阅)的完整学习路径。
Java 生态中的 GraphQL:为什么值得投入
在主流编程语言的热度榜单中长期位居前列的 Java,占据着大量企业级市场,而这些场景恰好落在 GraphQL 的擅长区间之内。尤其值得关注的是两者在类型系统上的互补:GraphQL 的强类型 Schema 与 Java 的静态类型体系在大多数情况下可以高度对齐,类型定义、参数校验、返回结构都能在编译期获得一定程度的保障,这让 Java 成为实现 GraphQL 后端非常自然的语言选择。
需要先说明的是:本仓库中的 Java 教程由社区作者撰写,官方维护者已经明确指出该教程内容相对过时,且其中使用了一些基于 graphql-java 的三方库(如graphql-java-tools、graphql-java-servlet),并未明确区分"graphql-java 核心"与"周边工具库"的边界。作者正在编写更新版本,官方推荐的入门路径是 graphql-java 官网提供的 Spring Boot 集成教程。因此在跟随本教程学习时,请将文中给出的库版本视为历史快照,动手前务必到 Maven 中央仓库核对最新版本。
什么是 GraphQL 服务器
GraphQL 服务器是负责解析(parse)、校验(validate)、执行(execute)GraphQL 查询与变更的软件组件。这一点上它与数据库服务器高度类似:数据库服务器解析并执行 SQL,GraphQL 服务器则解析并执行 GraphQL 操作。
关键区别在于:
- GraphQL 服务器的实现在多种语言中都存在,这意味着你可以把 GraphQL 几乎无痛地引入任何技术栈;
- 查询方(客户端)决定要获取哪些数据,服务器只负责按 Schema 与解析器(resolver)交付结果;
- 传输层是开放的——GraphQL 服务器可以暴露在 HTTP 之上,也可以通过 WebSocket 等任意传输协议承载,并不强制绑定某一种。
本章的目标,就是带你从零开始用 Java 打造一个自定义的 GraphQL 后端,逐步覆盖从 Schema 定义到查询、变更、认证、过滤、分页的完整能力。
Schema 驱动的开发:契约优先为何在 GraphQL 中变得自然
"契约优先(contract-first)"的设计理念早已存在(如 WSDL、Swagger),但长期以来难以落地,原因正如本教程开篇所指出的:
预先开发契约(无论其形式是 WSDL、Swagger 还是其他)往往要求你在最开始就对客户的数据需求有深刻而精确的理解,而这种理解通常只能随着时间推移逐步建立。
GraphQL 恰好消除了这一障碍:获取什么数据由客户端全权决定,这为 API 的平滑演进打开了通路。再加上 GraphQL 天然的"自我描述"能力(通过 introspection 内省查询即可枚举全部类型与操作),契约优先——在 GraphQL 术语中更准确地说是Schema 优先(schema-first)——就变得既自然又轻松。
Schema:客户端与服务器之间的中心契约
在 GraphQL 中,Schema 是客户端与服务器之间的中心契约,它描述了:
- 服务器提供的所有数据类型;
- 针对这些类型可执行的所有操作(查询 query 与变更 mutation);
- 字段的参数、类型、非空约束等元信息。
Schema 优先的开发方式带来的收益不止于客户端与服务器解耦与易于 mock:
- 强制良好实践:Schema 的字段天然分解为一个个小而简单的函数,这恰好呼应了单一职责原则(single responsibility principle),而这些小函数可以顺理成章地充当 resolver;
- 演进友好:字段按需添加、旧字段渐进淘汰,客户端不受破坏性变更的困扰;
- 可验证:Schema 本身就是文档,introspection 让客户端工具(如 GraphiQL)可以实时探索并自动补全。
动态生成 Schema 的替代路线
教程同时预告了另一种值得探索的风格:由于 Java 是静态类型语言,类型信息已经蕴含在代码之中,因此完全可以不手写 Schema 文本,而是基于代码中的类型信息动态生成 Schema。这条路线称为 code-first(代码优先),它消除了 Schema 与 Java 模型之间的重复维护负担——这在教程第 11 章中会以 graphql-spqr 为例展开演示。
两条 Schema 构建路线:programmatic 与 SDL
在正式动手前,有必要先厘清 Schema 的两种构建方式,它们将在后续章节中反复出现:
| 构建方式 | 说明 | 优缺点 |
|---|---|---|
| 编程方式(programmatic) | 在代码中手工组装类型定义 | 字段与其 resolver 紧邻放置,但样板代码多 |
| Schema 定义语言(SDL) | 用文本化的、语言无关的 Schema 语言描述类型,再动态绑定 resolver | 数据与行为清晰分离,示例简洁 |
本教程绝大部分章节采用SDL方式,因为它允许用最简短的示例表达完整结构。一个典型的 SDL 定义如下(对应教程第 1 章的schema.graphqls):
type Link { url: String! description: String! } type Query { allLinks: [Link] } schema { query: Query }值得强调的一点是:resolver 函数是字段定义的有机组成部分,因此 Schema 不只是文档,而是一个运行时对象实例(在本教程中即 Java 对象)。graphql-java-tools正是负责把 SDL 文本解析、与 Java resolver 类装配,最终生成可执行的GraphQLSchema的桥梁。
完整的 Java GraphQL 教程学习路径
本教程共 13 个章节(编号 0 至 12),从零搭建一个 Hackernews 风格的 GraphQL 后端。以下是各章的核心脉络,可作为按需查阅的索引:
第 1 章:项目初始化与第一个 Schema
- 使用 Maven 骨架
maven-archetype-webapp生成 Web 项目,groupId 为com.howtographql.sample,artifactId 为hackernews-graphql-java; - 在
src/main下新建java目录存放全部 Java 源码,并删除src/main/webapp/WEB-INF(否则 Servlet 3.x 风格的注解配置会被忽略); - 依赖层面,严格来说只有
graphql-java是必需的,但为了动态装配 resolver 还需要graphql-java-tools(受 Apollo 的 graphql-tools 启发),为通过 Web 暴露 API 则需要graphql-java-servlet与javax.servlet-api; - 通过 Jetty Maven 插件(
jetty-maven-plugin)在开发期启动服务,mvn jetty:run即可运行在 8080 端口; - 用
@WebServlet(urlPatterns = "/graphql")注解声明GraphQLEndpoint,继承SimpleGraphQLServlet,在构造器中用SchemaParser解析schema.graphqls并生成可执行 Schema。
第 2 章:查询解析器(Queries)
graphql-java-tools 将类划分为两类:数据类(data classes)是建模领域的简单 POJO,解析器类(resolvers)建模查询与变更并承载 resolver 函数。以Link类型为例:
LinkPOJO 只含url、description两个不可变字段与 getter;LinkRepository负责隔离存储细节(初期为内存ArrayList,后续切换为 MongoDB);Query implements GraphQLRootResolver中的allLinks()方法即查询 resolver,返回linkRepository.getAllLinks();- 通过
SchemaParser.newParser().file("schema.graphqls").resolvers(new Query(linkRepository))装配后,访问http://localhost:8080/graphql?query={allLinks{url}}即可看到第一条查询结果。
第 2 章还介绍了GraphiQL这一浏览器内 IDE:将官方示例index.html中的graphiql.css与graphiql.js路径替换为 CDN 引用,保存到src/main/webapp/index.html即可在http://localhost:8080/获得带自动补全、Schema 探索的调试环境。
第 3 章:变更(Mutations)
变更与查询的语法结构相同,区别仅在关键字与语义(变更产生副作用)。步骤为:
- 在 SDL 中新增
type Mutation { createLink(url: String!, description: String!): Link },并将schema根改为同时声明query与mutation; - 新建
Mutation implements GraphQLRootResolver,其中createLink(String url, String description)的参数名与类型须与 Schema 中定义的参数一一对应; - 在
buildSchema中通过.resolvers(new Query(linkRepository), new Mutation(linkRepository))注册。
第 4 章:连接外部存储(Connectors)
resolver 负责获取单个字段的值,因此一次查询响应中的不同字段可以同时来自多个存储或第三方 API,而客户端完全无感知——这正是 GraphQL 架构约束带来的好处。本章以 MongoDB 为例:
- 为
Link增加id: ID!字段,同步重构LinkPOJO(新增id字段与双构造器); - 在
pom.xml引入mongodb-driver; - 由于存储逻辑早已隔离在
LinkRepository中,引入 MongoDB 的影响面极小:将内部List<Link>替换为MongoCollection<Document>,通过ObjectId与Document完成对象映射; GraphQLEndpoint在静态初始化块中连接MongoClient().getDatabase("hackernews")并获取links集合。
章节末尾还讨论了N+1 查询问题:若字段的 resolver 逐条回源数据库,结果集会触发 N 次额外查询。解决策略包括 DataLoader 式批量加载,以及graphql-java提供的BatchedExecutionStrategy(配合@Batched注解的批量DataFetcher)。
第 5 章:认证(Authentication)
GraphQL 规范本身不内置认证机制,需要自行实现。本章演示邮箱-密码认证的完整链路:
- Schema 新增
createUser(name: String!, authProvider: AuthData!): User与input AuthData { email: String!, password: String! },以及User类型(注意教程为保持简洁而明文存储密码,生产环境必须哈希加盐); - 对应新增
User、AuthDataPOJO 与UserRepository(按 email、按 id 查询,保存用户); - 登录变更
signinUser(auth: AuthData): SigninPayload返回{ token, user },其中 token 示例仅为用户 id,真实场景应使用 JWT 等标准方案;密码不匹配时抛出GraphQLException("Invalid credentials"); - 请求认证通过
Authorization头完成:创建AuthContext extends GraphQLContext持有当前用户,并在GraphQLEndpoint中重写createContext,从请求头剥离Bearer前缀后按 id 查找用户; - 为
Link增加postedBy: User关系字段,配套新建LinkResolver implements GraphQLResolver<Link>(非标量关系字段需要"数据类 + 解析器类"双类配合),createLink通过DataFetchingEnvironment注入上下文取得当前用户 id。
第 6 章:更多变更——投票功能与自定义标量
本章实现 Hackernews 的投票特性,完整走一遍"Schema → 数据类 → 解析器 → 仓库 → 注册"的全流程:
- 新增
createVote(linkId: ID, userId: ID): Vote与Vote类型(含createdAt: DateTime!),并声明scalar DateTime; Vote使用ZonedDateTime建模时间,VoteResolver负责user与link两个关系字段的解析;- 自定义标量
Scalars.dateTime通过GraphQLScalarType与Coercing接口实现:serialize负责输出时格式化为 ISO 字符串,parseLiteral负责输入时将字符串解析回ZonedDateTime,最后在SchemaParser上通过.scalars(Scalars.dateTime)注册。
第 7 章:错误处理(Error Handling)
GraphQL 响应的结构是可预测的,固定包含三个字段:
data:操作结果;errors:执行过程中累积的全部错误;extensions(可选):任意元数据。
语法与校验错误由服务器自动处理,而 resolver 中抛出的异常通常需要应用层定制处理策略:
isClientError决定错误消息是否原样发给客户端(默认仅语法与校验错误透传,其余以通用server error掩盖,防止泄露堆栈细节);- 重写
filterGraphQLErrors可对错误做清洗、过滤、包装——教程演示了SanitizedError extends ExceptionWhileDataFetching配合 Jackson 的@JsonIgnore隐藏内部异常,同时把数据获取错误的精确消息透传给客户端; - 更底层可通过自定义
ExecutionStrategy并重写handleDataFetchingException控制"Java 异常 → GraphQL 错误"的翻译过程。
第 8 章:订阅(Subscriptions)
GraphQL 规范定义了名为subscriptions的实时推送机制,但教程明确指出:graphql-java当时仅能解析订阅请求,尚未提供可用的端到端支持,需要大量超出教程范围的手工工作。因此本章暂不展开实现,作者承诺在生态成熟后更新——这是阅读时需要注意的现状边界。
第 9 章:过滤(Filtering)
查询参数本身没有内置语义,含义完全由实现者定义——这正是过滤功能得以轻松实现的原理。本章为allLinks增加filter: LinkFilter参数:
type Query { allLinks(filter: LinkFilter): [Link] } input LinkFilter { description_contains: String url_contains: String }配套要点:
LinkFilterPOJO 通过@JsonProperty将 Java 风格的descriptionContains映射到 Schema 的下划线命名description_contains;LinkRepository#getAllLinks接受可选过滤条件,用 MongoDB 正则regex("description", ".*" + pattern + ".*", "i")实现大小写不敏感的子串匹配,多个条件用and()组合;Query#allLinks(LinkFilter filter)透传参数。
第 10 章:分页(Pagination)
教程采用与 SQL 同源的limit-offset 分页:
type Query { allLinks(filter: LinkFilter, skip: Int = 0, first: Int = 0): [Link] }实现细节中有两个值得记忆的坑:
LinkRepository#getAllLinks用 MongoDB 的documents.skip(skip).limit(first)实现偏移与截取;- resolver 方法参数类型必须声明为
Number,因为graphql-java-tools会根据上下文有时塞入Integer、有时塞入BigInteger,声明为具体类型会引发反序列化问题。
注意:limit-offset 分页与前端 Relay 的 cursor-based(基于 connection 概念)分页不兼容,若前端使用 Relay 需改走 Connection 规范。
第 11 章:Schema 开发的替代路线(Code-first)
这是开篇预告的落点。Schema-first 在 Java 中会导致明显的重复:Link类型在 SDL 里定义一遍、在 POJO 里再写一遍,改动需同步两处,重构风险高;而 code-first 直接从既有模型生成 Schema,天然保持同步,特别适合在存量代码库之上引入 GraphQL。教程以 graphql-spqr 为例:
pom.xml引入io.leangen.graphql:spqr,并开启 javac 的-parameters编译参数以保留方法参数名;- 用
@GraphQLQuery标注查询方法、@GraphQLMutation标注变更方法、@GraphQLArgument(name = "skip", defaultValue = "0")定制参数名与默认值、@GraphQLContext把外部方法接入类型、@GraphQLRootContext直接注入AuthContext(取代DataFetchingEnvironment); - 通过
new GraphQLSchemaGenerator().withOperationsFromSingletons(query, linkResolver, mutation).generate()一键生成 Schema; - 类不再需要实现
GraphQLRootResolver/GraphQLResolver,也不再依赖graphql-java-tools,业务代码几乎可以原样暴露为 GraphQL 操作。
两种风格的取舍要点:
- Schema-first:Schema 先行、清晰分离数据与行为、适合绿地项目,但 Java 下重复度高;
- Code-first:Schema 与模型同步、重构友好、适合存量代码库,但 Schema 在服务器代码写出之前不存在,客户端与服务器端工作存在先后依赖(可先用桩代码生成 Schema 以解耦并行开发)。
学习建议与注意边界
- 版本问题:教程给出的库版本(如
graphql-java3.0.0、graphql-java-tools3.2.0、graphql-java-servlet4.0.0、javax.servlet-api3.0.1)均为撰写时快照,现已过时。动手前务必核对 Maven 最新版本,并注意官方推荐以 graphql-java 的 Spring Boot 集成教程作为入门替代; - 依赖边界:本教程大量使用
graphql-java-tools与graphql-java-servlet,它们属于 graphql-java 生态的工具库而非核心实现,理解这一点有助于你阅读后续版本迁移文档; - 安全提醒:教程中密码明文存储、token 直接用用户 id 都是教学简化,真实项目必须使用密码哈希与 JWT 等标准方案;
- 可延伸主题:教程总结章指出,动态数据结构、恶意查询防护(深度/复杂度限制)、缓存策略等领域仍留待你自己探索。
上述各章节对应的完整文档均位于仓库 content/backend/graphql-java/ 目录下,按编号 0 至 12 顺序阅读即可获得从零到完整的可运行示例;仓库的 写作规范 则解释了教程中<Instruction>操作块、代码注解等排版约定的设计意图,有助于你理解每个章节的阅读节奏。
【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
相关推荐
5个核心优化策略:让One-KVM视频流畅度提升300%的终极指南
5个核心优化策略:让One KVM视频流畅度提升300%的终极指南 One KVM Rust 是一个用 Rust 编写的轻量级 IP KVM 解决方案,可通过网
使用 graphql-code-generator 的 typescript-resolvers 插件构建类型安全的 GraphQL 服务端(SDL-first 实战指南)
使用 graphql code generator 的 typescript resolvers 插件构建类型安全的 GraphQL 服务端(SDL first
开发工具基于 Schema-First 与代码生成构建类型安全的 GraphQL 服务器:gqlgen 完整实战指南
基于 Schema First 与代码生成构建类型安全的 GraphQL 服务器:gqlgen 完整实战指南 gqlgen 是一个基于 Schema First
后端GraphQL代码生成
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考