☰
GraphQL Java 后端教程收官总结:基于 graphql-java 构建服务的完整要点回顾与后续探索
2026/9/25 5:28:25 网站建设 项目流程

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载

本文是 howtographql 仓库中 GraphQL Java 后端教程(总结篇) 的收官解读。该教程用 Java 生态最主流的graphql-java系列库,从零搭建了一个完整的 Hackernews 风格 GraphQL 服务,覆盖 schema-first 开发、查询/变更解析器、MongoDB 连接器、认证、错误处理、过滤与分页,并介绍了graphql-spqr的 code-first 备选方案。读完本文,你将获得整条教程的路线图、核心代码要点与可验证的源码依据,同时了解动态数据结构、恶意查询防护、缓存等值得继续深入的方向。

GraphQL 的三项核心承诺

总结篇开宗明义地指出,GraphQL 承诺一种清晰而简洁的方式来:

  • 描述和操作数据(describe and manipulate data):通过类型系统与 schema 精确刻画数据形状;
  • 只获取恰好且必需的数据(fetch exactly and only the data that is required):客户端决定取哪些字段,杜绝过度获取;
  • 获得可预测的结果(receive a predictable result):响应结构固定,错误与数据分离。

整个 Java 教程正是围绕这三项承诺展开的:先用 SDL 定义Link类型与allLinks查询,再通过解析器把 schema 与 Java 代码接起来,最终让客户端能以{allLinks{url}}这样的查询精确拿到所需字段。教程开篇还解释了为何 schema-first(契约优先)在 GraphQL 中天然可行:schema 是客户端与服务端之间的中央契约,且得益于 introspection(内省查询),schema 具有自描述能力。

教程路线图:从零到可用的 GraphQL 服务

下面按章节顺序回顾整条教程的技术脉络,每部分都指向仓库中对应的章节文件与核心代码。

1. 项目初始化:Maven 骨架与 SDL 起步

教程使用 Maven 的 webapp 骨架初始化项目(详见 1-getting-started.md):

mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp -DgroupId=com.howtographql.sample -DartifactId=hackernews-graphql-java -Dversion=1.0-SNAPSHOT

随后在src/main/resources/schema.graphqls中定义第一个 schema:

type Link { url: String! description: String! } type Query { allLinks: [Link] } schema { query: Query }

在pom.xml中声明四个依赖:graphql-java(GraphQL 实现本身)、graphql-java-tools(灵感来自 Apollographql-tools的动态解析器装配库)、graphql-java-servlet(开箱即用的 Servlet)以及javax.servlet-api。章节中给出的版本(如graphql-java3.0.0、graphql-java-tools3.2.0、graphql-java-servlet4.0.0)写作时是最新版本,但库的迭代很快,动手前务必检查更新。同时配置jetty-maven-plugin与maven-compiler-plugin(Java 1.8、Servlet 3.1),之后只需mvn jetty:run即可在 8080 端口启动 Jetty。

服务端入口是继承SimpleGraphQLServlet的GraphQLEndpoint,通过SchemaParser解析 schema 文件并生成可执行 schema:

@WebServlet(urlPatterns = "/graphql") public class GraphQLEndpoint extends SimpleGraphQLServlet { public GraphQLEndpoint() { super(SchemaParser.newParser() .file("schema.graphqls") .build() .makeExecutableSchema()); } }

此时访问http://localhost:8080/graphql仍会报错——因为还没有任何解析器被接入,allLinks无从执行。

2. 查询解析器:data class 与 resolver 的分工

graphql-java-tools把 Java 类分成两类(详见 2-queries.md):数据类(data classes,建模领域、通常是纯 POJO)与解析器(resolvers,建模查询与变更、包含解析函数)。一个 GraphQL 类型常常需要两者共同建模:

public class Link { private final String url; private final String description; // 构造器与 getter }
public class Query implements GraphQLRootResolver { private final LinkRepository linkRepository; public List<Link> allLinks() { return linkRepository.getAllLinks(); } }

LinkRepository把链接的存取逻辑隔离起来,初期用内存ArrayList存储。更新GraphQLEndpoint后,访问http://localhost:8080/graphql?query={allLinks{url}}就能看到首个查询结果:

{ "data": { "allLinks": [ { "url": "http://howtographql.com" }, { "url": "http://graphql.org/learn/" } ] } }

随后教程引入 GraphiQL 浏览器 IDE:把index.html中graphiql.css/graphiql.js的引用改为 CDN 地址,存到src/main/webapp/index.html并重启 Jetty,即可在http://localhost:8080/获得带自动补全的交互式测试环境。

3. 变更解析器:带参数的 mutation

定义变更与定义查询同样直接(详见 3-mutations.md)。先在 SDL 中描述createLink变更并把mutation根类型挂到 schema 上:

type Mutation { createLink(url: String!, description: String!): Link } schema { query: Query mutation: Mutation }

再创建根变更解析器Mutation implements GraphQLRootResolver,其方法签名与 schema 中变更的参数名、类型一一对应:

public Link createLink(String url, String description) { Link newLink = new Link(url, description); linkRepository.saveLink(newLink); return newLink; }

最后在GraphQLEndpoint#buildSchema中通过.resolvers(new Query(linkRepository), new Mutation(linkRepository))注册即可。重启 Jetty 后用 GraphiQL 执行变更并重跑allLinks,即可验证新链接已持久化。

4. 数据连接器:接入 MongoDB

纯内存存储无法持久化,教程选择 MongoDB 作为存储(详见 4-connectors.md),并借此强调 GraphQL 架构的一个优点:引入第三方连接器对开发者很轻量,对客户端完全透明——同一查询响应中的不同字段可以来自多个存储或第三方 API。

步骤包括:给Link类型补充id: ID!字段并同步改造Link类;在pom.xml加入mongodb-driver依赖;把LinkRepository从内存列表重构为基于MongoCollection<Document>的实现(findById按_id查询、saveLink用Document追加字段后insertOne);最后在GraphQLEndpoint的静态块中连接本地 MongoDB 的hackernews库并取出links集合。若 Mongo 不在本地 27017 端口,把new MongoClient()改为new MongoClient("<host>:<port>")即可。重启后一切照旧,只是数据不再因断电而丢失。

章节末尾还点出了 N+1 问题:若description存于另一数据库,每解析一个链接的description字段都会触发一次额外查询。解决办法是批量解析,如 SQL 的SELECT * FROM Descriptions WHERE link_id IN (1,2,3);Java 侧可参考graphql-java的BatchedExecutionStrategy(配合@Batched注解的DataFetcher,接收源对象列表、返回结果列表),或使用 Java 版的 DataLoader 工具。

5. 认证:从 signinUser 到 AuthContext

没有用户追踪就谈不上互动,教程实现了注册与登录(详见 5-authentication.md)。

先扩展 schema:新增createUser(name: String!, authProvider: AuthData!): User变更、User类型与AuthData输入类型:

type User { id: ID! name: String! email: String password: String } input AuthData { email: String! password: String! }

配套创建User、AuthData两个数据类与UserRepository(按email/_id查询、保存用户)。教程特意注明:永远不要明文存储密码,这里仅为简化示例。登录侧则定义signinUser(auth: AuthData): SigninPayload变更,SigninPayload包含token与user;由于SigninPayload内含非标量对象User,还需要配套的SigninResolver implements GraphQLResolver<SigninPayload>。登录解析器校验密码,失败时抛出GraphQLException("Invalid credentials"),成功则返回 token——本示例中 token 就是用户 id,生产环境应替换为 JWT 或类似方案。

随后处理请求认证:约定客户端在每次请求的Authorization头中带回 token(如Authorization: Bearer <token>)。由于 GraphiQL 不便发送该头,教程让读者在index.html中硬编码测试。服务端侧的关键是context 对象——它是执行期间传递给所有解析器的数据载体。教程创建AuthContext extends GraphQLContext携带User,并覆写GraphQLEndpoint#createContext从请求头解析用户:

@Override protected GraphQLContext createContext(Optional<HttpServletRequest> request, Optional<HttpServletResponse> response) { User user = request .map(req -> req.getHeader("Authorization")) .filter(id -> !id.isEmpty()) .map(id -> id.replace("Bearer ", "")) .map(userRepository::findById) .orElse(null); return new AuthContext(user, request, response); }

有了用户身份后,教程给Link增加postedBy: User字段,用LinkResolver implements GraphQLResolver<Link>提供postedBy(Link link)解析(按link.getUserId()反查用户),并在createLink解析器中通过DataFetchingEnvironment env注入 context,把当前登录用户记为链接作者。

6. 投票功能与自定义 DateTime 标量

认证之后教程引入投票特性(详见 6-more-mutations.md):定义createVote(linkId: ID, userId: ID): Vote变更与Vote类型,其中createdAt: DateTime!需要一个自定义标量:

type Vote { id: ID! createdAt: DateTime! user: User! link: Link! } scalar DateTime

自定义标量通过GraphQLScalarType实现,负责三类转换:serialize(输出时把ZonedDateTime格式化为 ISO 字符串)、parseValue(输入值解析)与parseLiteral(字面量解析):

public class Scalars { public static GraphQLScalarType dateTime = new GraphQLScalarType("DateTime", "DataTime scalar", new Coercing() { @Override public String serialize(Object input) { return ((ZonedDateTime)input).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); } // parseValue / parseLiteral 相应实现 }); }

配套创建Vote数据类、VoteResolver(解析user与link字段)与VoteRepository(按userId/linkId查询、保存投票),最后在GraphQLEndpoint#buildSchema中同时注册新解析器与标量:.resolvers(... new VoteResolver(linkRepository, userRepository)).scalars(Scalars.dateTime)。createVote解析器用Instant.now().atZone(ZoneOffset.UTC)生成 UTC 时间戳后入库。

7. 错误处理:可预测的响应结构与错误脱敏

GraphQL 服务端的响应结构始终可预测(详见 7-error-handling.md),由三部分组成:

  • data字段:操作结果;
  • errors字段:执行过程中累积的所有错误;
  • 可选的extensions字段:任意内容,通常是响应元数据。

语法错误与校验错误由服务器自动处理并原样告知客户端;而解析器中抛出的异常通常需要应用层定制处理。graphql-java-servlet提供了两个定制入口:

  • isClientError:决定某错误的消息是原样发给客户端,还是被掩盖为通用的 server error。默认只放行语法与校验错误,这能防止异常消息与堆栈泄露敏感信息。
  • filterGraphQLErrors:在错误发送给客户端之前进行脱敏、过滤、包装或转换。

教程的典型做法是用SanitizedError extends ExceptionWhileDataFetching包装数据获取异常,并用@JsonIgnore注解让 Jackson 在序列化时忽略底层异常(堆栈不会到达客户端):

public class SanitizedError extends ExceptionWhileDataFetching { public SanitizedError(ExceptionWhileDataFetching inner) { super(inner.getException()); } @Override @JsonIgnore public Throwable getException() { return super.getException(); } }

再覆写filterGraphQLErrors:只放行数据获取异常与客户端错误,并把前者包装为SanitizedError。这样,signinUser的"Invalid credentials"这类精确消息能到达客户端,而堆栈细节被隐藏。需要更低层控制时,还可以自定义ExecutionStrategy(覆写handleDataFetchingException把 Java 异常翻译成 GraphQL 错误),并在构造函数中传入:super(buildSchema(), new CustomExecutionStrategy())。

8. 订阅:现实限制与演进预期

实时推送是 GraphQL 规范的亮点,但教程订阅章节如实说明:graphql-java虽然能解析订阅请求,但当时的支持程度有限,不做大量手工工作便难以实用,这超出了教程范围。章节承诺一旦生态情况变化会及时更新——这本身也印证了总结篇"Java 生态中 GraphQL 仍处于早期、演进很快"的判断。

9. 过滤:参数没有固有语义,语义由你定义

查询与变更都能通过参数接收输入,而参数本身没有固有语义(详见 9-filtering.md)。教程把这一特性用于过滤:给allLinks增加LinkFilter输入参数:

type Query { allLinks(filter: LinkFilter): [Link] } input LinkFilter { description_contains: String url_contains: String }

LinkFilterPOJO 用@JsonProperty("description_contains")让 getter 名与 schema 的下划线命名对齐。LinkRepository#getAllLinks(LinkFilter filter)把过滤条件翻译成 MongoDB 查询条件(Bson),对非空字段构造.*<pattern>.*的不区分大小写正则,两个条件同时存在时用and(...)合并:

private Bson buildFilter(LinkFilter filter) { // descriptionCondition / urlCondition 分别用 regex("description"/"url", ".*" + pattern + ".*", "i") 构造 // 两者都有时返回 and(descriptionCondition, urlCondition) }

最终Query#allLinks(LinkFilter filter)把参数透传给仓库层。教程特别提醒:这只是过滤的一种实现示例,完全可以用其他格式实现。

10. 分页:limit-offset 方案

链接增多后需要分页(详见 10-pagination.md)。教程采用 SQL 风格的 limit-offset 分页,在 schema 中为allLinks增加带默认值的参数:

type Query { allLinks(filter: LinkFilter, skip: Int = 0, first: Int = 0): [Link] }

仓库层对查询结果链式调用.skip(skip).limit(first);顶层Query方法中参数类型必须声明为Number而非int,因为graphql-java-tools会根据上下文有时塞入Integer、有时塞入BigInteger,用Number再.intValue()转换最稳妥。章节同时指出:这种分页方式与前端 Relay 不兼容——Relay 要求基于 connection 概念的游标分页。若使用 limit-offset,跳过大页时存在明显的性能边界。

11. 备选开发风格:code-first 与 graphql-spqr

教程最后一章反思了 schema-first 在 Java 这类强静态类型语言中的痛点:Link类型在 SDL 与 Java POJO 中各写一遍,信息完全重复,改动需同步进行,重构风险大;对存量项目引入 GraphQL 更是等于重新描述整个模型。code-first风格则从已有模型生成 schema,保持 schema 与模型同步、利于重构,适合在既有代码库上引入 GraphQL;缺点是 schema 在服务端代码写好之前并不存在,客户端与服务端工作产生依赖(可先用桩代码生成 schema 再并行开发)。

教程用graphql-spqr演示 code-first:在pom.xml加入spqr依赖,并开启maven-compiler-plugin的-parametersjavac 选项(保留方法参数名,schema 才能使用参数名),之后必须重新构建项目(如mvn clean package)再重启 Jetty。改造方式是给已有业务方法加注解:

public class Query { @GraphQLQuery public List<Link> allLinks(LinkFilter filter, @GraphQLArgument(name = "skip", defaultValue = "0") Number skip, @GraphQLArgument(name = "first", defaultValue = "0") Number first) { return linkRepository.getAllLinks(filter, skip.intValue(), first.intValue()); } }

要点:实现GraphQLRootResolver/GraphQLResolver不再是必需;@GraphQLQuery、@GraphQLMutation等注解完全可选(但默认配置会在顶层期望它们);@GraphQLArgument用于改参数名与设默认值;@GraphQLContext Link link能把外部方法织入已有类型(语义等同于Link类内含postedBy()方法);@GraphQLRootContext AuthContext可直接注入 context,免去对DataFetchingEnvironment的依赖。最后用生成器从单例业务对象生成 schema:

return new GraphQLSchemaGenerator() .withOperationsFromSingletons(query, linkResolver, mutation) .generate();

同样的 GraphiQL 结果,但再也不用维护显式 schema,也不必把链路逻辑拆进顶层查询、嵌套解析器与变更三个位置——遗留代码与既有最佳实践可以原样保留。

留给你的探索领域

总结篇明确指出,教程所覆盖的只是"如何利用 GraphQL 优势"的基础,以下领域留待读者自行探索:

  • 动态数据结构(dynamic data structures):如何建模与处理结构不固定的数据;
  • 恶意查询防护(protection against malicious queries):深度嵌套、字段爆炸等查询对服务端的资源消耗,需要在应用层设计防护策略;
  • 缓存(caching):GraphQL 的按需取数模型让传统 HTTP 缓存不再直接适用,如何设计高效缓存是经典难题。

建议以教程建立的基础为跳板,针对这些问题寻找更深入的答案。

生态现状与注意事项

总结篇提醒:GraphQL 是新生技术,在 Java 生态中尤其如此——库在变化,思路与最佳实践也在发展与迁移,需要持续关注教程的更新,它可能随时被修订甚至重写以保持相关性。

这一提醒在教程开篇的警告中也能得到印证:该 Java 教程写作时间较早,且在其上叠加了第三方库(如graphql-java-tools),并未明确说明这些并非graphql-java本身;作者正在推进更新版本。因此在实践中,应以graphql-java官方的最新教程与 Spring Boot 集成方案为优先参考,把本教程当作理解架构思想与核心 API 的入门路线,而非照搬版本号。

结语

至此,你已走完"从零到完整 GraphQL 服务"的 Java 全流程:SDL 定义 schema → 查询/变更解析器 → MongoDB 连接器 → 基于 context 的认证 → 自定义标量 → 错误脱敏 → 过滤与分页 → code-first 备选方案。正如总结篇所言:"You made it!" 带着这份路线图,你可以去继续探索动态数据、查询防护与缓存等更深的话题,让 GraphQL 的能力真正为己所用。

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载
上一篇:OpenCore Legacy Patcher终极指南:如何让旧Mac免费升级最新macOS系统
下一篇:攻克AKS中Istio Service Entry的DNS解析难题:从故障排查到根治方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询