DrillLab
第 17 / 17 节LESSON 17 / 17约 90 分钟~90 min

从零重写:空目录到 10 个测试全过Rewrite it: from an empty directory to all 10 tests passing

不给答案。给 schema、给数据源、给测试、给四级提示。这一关是分界线。No answer key. You get the schema, the data source, the tests, and four levels of hints. This stage is the dividing line.

2 个练习2 exercisesFederation · 第 6 部分Federation · Part 6
这一页有什么On this page4
学完这节你会After this lesson you can
  • 在没有参考代码的情况下从空目录搭出一个 federation subgraphBuild a federation subgraph from an empty directory, with no reference code
  • 独立实现四个 resolver 并自己发现三处埋雷Write the four resolvers on your own and find the three hidden problems yourself
  • 独立实现六个 Spring 端点并选对状态码Write the six Spring endpoints on your own and pick the right status code for each
  • 用测试 + verify 脚本 + curl 三种方式验证自己的实现Check your own work in three ways: the tests, the verify script, and curl
这在考试里考什么What the exam does with this

填空和跟写只证明你看懂了。真正的考试是打开一个空编辑器。这一关比真实考试更难 —— 连脚手架都要你自己搭。Filling in blanks and typing along only prove that you followed the text. The real exam starts with an empty editor. This stage is harder than the real exam, because even the project setup is yours to write.

这节课要看的真实文件Real files this lesson looks at1 项 · 1 个可以展开看原文1 items · 1 can be opened
graphql-federation-practice/参考项目 —— 做完之后再对照,不要提前看The reference project — compare against it after you finish; do not look early
Textgraphql-federation-practice/ · tree源项目From source
1graphql-federation-practice/
2├── java-service/
3│ ├── src/
4│ │ ├── main/
5│ │ │ ├── java/
6│ │ │ │ └── com/
7│ │ │ │ └── techflow/
8│ │ │ │ └── orders/
9│ │ │ │ ├── config/
10│ │ │ │ │ ├── CorrelationIdFilter.java
11│ │ │ │ │ └── MetricsConfig.java
12│ │ │ │ ├── controller/
13│ │ │ │ │ └── OrderController.java
14│ │ │ │ ├── dto/
15│ │ │ │ │ ├── CreateOrderRequest.java
16│ │ │ │ │ └── OrderItemRequest.java
17│ │ │ │ ├── exception/
18│ │ │ │ │ ├── EntityNotFoundException.java
19│ │ │ │ │ └── GlobalExceptionHandler.java
20│ │ │ │ ├── model/
21│ │ │ │ │ ├── Order.java
22│ │ │ │ │ ├── OrderItem.java
23│ │ │ │ │ └── OrderStatus.java
24│ │ │ │ ├── repository/
25│ │ │ │ │ ├── InMemoryOrderRepository.java
26│ │ │ │ │ └── OrderRepository.java
27│ │ │ │ ├── service/
28│ │ │ │ │ └── OrderService.java
29│ │ │ │ └── OrderServiceApplication.java
30│ │ │ └── resources/
31│ │ │ └── application.properties
32│ │ └── test/
33│ │ └── java/
34│ │ └── com/
35│ │ └── techflow/
36│ │ └── orders/
37│ │ └── OrderControllerTest.java
38│ ├── orders.db
39│ └── pom.xml
40├── node-subgraph/
41│ ├── __tests__/
42│ │ └── resolvers.test.js
43│ ├── src/
44│ │ ├── dataSources/
45│ │ │ └── orderDataSource.js
46│ │ ├── resolvers/
47│ │ │ └── orderResolvers.js
48│ │ ├── index.js
49│ │ └── schema.graphql
50│ ├── package-lock.json
51│ └── package.json
52├── QUESTIONS.md
53└── README.md
Source: graphql-federation-practice/
§01

为什么必须做这一关Why this stage is required

前面每一节的 L3 练习里,你已经分别写过四个 resolver 和六个端点。这一关是把它们放回一个完整项目里—— 加上你自己搭的 schema 加载、context 构造、依赖配置。

而且这一关会强迫你面对一件事: 没有人告诉你埋雷在哪。你要自己写 loader、自己写 mutation —— 如果你在这里犯了和 starter 一样的错 (方法名、签名、catch 吞错误), 那说明前面那几节只是「看懂了」。

不要跳过这一关直接看答案。撞墙的地方才是你真正的薄弱点。

In the L3 exercises of the earlier lessons you have already written all four resolvers and all six endpoints, separately. This stage puts them back inside one complete project — along with schema loading, context construction and dependency setup that you build yourself.

And this stage forces you to face one thing: nobody tells you where the traps are. You write the loader yourself, you write the mutation yourself. If you make the same mistakes the starter made (method name, signature, catch swallowing the error), then those earlier lessons only got you as far as “I followed along”.

Do not skip this and go straight to the answer. The wall you hit is where your real weak spot is.

§02

建议的做法A suggested order of work

  1. 新建目录,不要在源项目里改。比如 ~/Downloads/my-order-subgraph。 源项目留着最后对照。
  2. 先让空服务器能起来。npm init → 装依赖 → 写一个最小 schema (只有 type Query { ping: String })→npm start 能看到Subgraph ready at ... 再往下走。这是所有项目的正确起手式。
  3. 把 schema、数据源、测试抄进去。这三样是「题目」,不是「答案」。 抄它们等于把考场搭起来。
  4. 一个测试一个测试地攻。先让 User.orders 的两条过, 再 shippingInfo,依次推进。
  5. 10 个测试全绿之后,写 verify 脚本。测试只覆盖单元层面;_service_entities 要自己验。
  6. Java 那半独立做。它和 subgraph 没有代码关联,可以完全分开。
  7. 卡住超过 20 分钟再看提示。提示是四级递进的。
  1. New directory. Do not edit inside the source project. Something like ~/Downloads/my-order-subgraph. Keep the source project for comparing at the end.
  2. Get an empty server running first. npm init → install dependencies → write a minimal schema (just type Query { ping: String }) → run npm start and see Subgraph ready at ... before you go any further. That is the right opening move on any project.
  3. Copy in the schema, the data sources and the tests. Those three are the question, not the answer. Copying them is how you set up the exam room.
  4. Attack one test at a time. Get the two User.orders tests green, then shippingInfo, and keep going in order.
  5. Once all 10 tests are green, write the verify script. The tests only cover the unit level; _service and _entities you have to check yourself.
  6. Do the Java half on its own. It shares no code with the subgraph, so you can keep them fully separate.
  7. Stuck for more than 20 minutes? Then look at a hint. The hints come in four escalating levels.
练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L4从零重写Rebuild from scratch从零重建 Task 1 · Orders subgraphRebuild Task 1 · the Orders subgraph

空目录开始,搭出一个 Apollo Federation subgraph, 实现四个 resolver 加一个 mutation,让 10 个测试全过, 并且 _service_entities 都能正常工作。不要打开源项目的 orderResolvers.js。

Starting from an empty directory, build an Apollo Federation subgraph. Write four resolvers plus one mutation, get all 10 tests passing, and make both _service and _entities work. Do not open orderResolvers.js from the source project.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • 用 @apollo/server + @apollo/subgraph 起一个 subgraph,监听 4000Start a subgraph with @apollo/server + @apollo/subgraph, listening on 4000
  • schema 从 .graphql 文件读入,用 buildSubgraphSchema 组装Read the schema from a .graphql file and assemble it with buildSubgraphSchema
  • 每个请求构造 context:三个数据源、两个 DataLoader、一个 correlationIdBuild the context per request: three data sources, two DataLoaders, one correlationId
  • correlationId 优先取请求头 x-correlation-id,没有就生成Take correlationId from the x-correlation-id request header, and generate one when it is absent
  • 实现 User.__resolveReference:把 representation 变成本地对象Write User.__resolveReference: turn the representation into a local object
  • 实现 User.orders:按 user.id 取订单,[Order!]! 所以绝不返回 nullWrite User.orders: read orders by user.id; the type is [Order!]!, so never return null
  • 实现 Order.shippingInfo:必须走 DataLoader 防 N+1;可空,找不到返回 nullWrite Order.shippingInfo: it must go through the DataLoader to prevent N+1; it is nullable, so return null when nothing is found
  • 实现 Query.order:走 DataLoader;找不到抛带 ORDER_NOT_FOUND 的 GraphQLErrorWrite Query.order: go through the DataLoader; when nothing is found, throw a GraphQLError carrying ORDER_NOT_FOUND
  • 实现 Query.orders:校验 userId;[Order!]! 所以兜底 []Write Query.orders: validate userId; the type is [Order!]!, so fall back to []
  • 实现 Mutation.createOrder:先查商品价格补全 items,再创建;校验失败抛 INVALID_INPUTWrite Mutation.createOrder: look up product prices to complete items first, then create; throw INVALID_INPUT when validation fails
  • 两个 DataLoader 的 batch 函数:返回数组的长度与顺序必须和 keys 一一对应The batch function of both DataLoaders: the array it returns must match keys in both length and order
  • 所有 resolver 都用 try/catch,catch 第一行放行已有的 GraphQLErrorWrap every resolver in try/catch, and let an existing GraphQLError pass through on the first line of catch
  • 所有日志和错误 extensions 里带上 correlationIdCarry correlationId in every log line and in the extensions of every error
你需要自己建的文件Files you create yourself
文件清单File list
package.json自己写:type: module、start / test script(test 要带 NODE_OPTIONS=--experimental-vm-modules)、依赖 @apollo/server @apollo/subgraph graphql graphql-tag dataloader,devDep jest @jest/globals,以及内嵌 jest 配置You write it: type: module, the start / test scripts (test needs NODE_OPTIONS=--experimental-vm-modules), the dependencies @apollo/server @apollo/subgraph graphql graphql-tag dataloader, the devDependencies jest @jest/globals, and an inline jest config
src/schema.graphql★ 抄源项目的(这是题目):User entity + Order/OrderItem/ShippingInfo + enum + Query/Mutation + input★ Copy it from the source project (this is the question): the User entity + Order/OrderItem/ShippingInfo + enum + Query/Mutation + input
src/dataSources/orderDataSource.js★ 抄源项目的(这是题目):三个 mock 数据源类。注意 OrderDataSource 只有 getOrder / getOrdersByUserId / createOrder★ Copy it from the source project (this is the question): three mock data source classes. Note that OrderDataSource has only getOrder / getOrdersByUserId / createOrder
src/index.js★ 自己写:读 schema、buildSubgraphSchema、ApolloServer + formatError、startStandaloneServer、每请求造 context★ You write it: read the schema, buildSubgraphSchema, ApolloServer + formatError, startStandaloneServer, and build the context per request
src/resolvers/orderResolvers.js★★ 自己写:两个 loader 工厂 + resolvers(User / Order / Query / Mutation)+ ErrorCodes★★ You write it: two loader factories + the resolvers (User / Order / Query / Mutation) + ErrorCodes
__tests__/resolvers.test.js★ 抄源项目的(这是判卷器):10 个测试,beforeEach 里重建 dataSources 与 loaders★ Copy it from the source project (this is what grades you): 10 tests, with dataSources and loaders rebuilt in beforeEach
verify-schema.mjs★ 自己写:进程内查 _service、普通查询、_entities、mutation★ You write it: query _service in process, then a normal query, then _entities, then the mutation
写完后在本机这样验证Verify it locally like this
npm install
依赖装好,出现 node_modules 与 package-lock.jsonThe dependencies install, and node_modules and package-lock.json appear
npm start
打印 Subgraph ready at http://0.0.0.0:4000/It prints Subgraph ready at http://0.0.0.0:4000/
npm test
Tests: 10 passed, 10 total
node verify-schema.mjs
SDL 出得来且含 @key;orders + shippingInfo 有值;order-999 返回 ORDER_NOT_FOUND;_entities 能拿到 orders;createOrder 的 items[0].price 有值且 totalAmount > 0;空 items 返回 INVALID_INPUTThe SDL comes out and contains @key; orders + shippingInfo have values; order-999 returns ORDER_NOT_FOUND; _entities can read orders; items[0].price from createOrder has a value and totalAmount > 0; empty items returns INVALID_INPUT
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。The whole point of this level is writing it with no answer in front of you. Create the files on your machine and run the verification commands first, then come back and compare.

Tick this once you have written it locally and the verification commands give the expected output.
L4从零重写Rebuild from scratch从零重建 Task 2 · Spring Boot 控制器Rebuild Task 2 · the Spring Boot controller

给你 OrderService 的方法签名和五个测试。 自己搭一个 Spring Boot 项目,写出六个端点。不要打开源项目的 OrderController.java。

You are given the method signatures of OrderService and five tests. Set up a Spring Boot project yourself and write six endpoints. Do not open OrderController.java from the source project.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • Spring Boot 3.3 + Java 17,依赖 web / validation / actuator / testSpring Boot 3.3 + Java 17, with the web / validation / actuator / test dependencies
  • 一个 @RestController,构造器注入 OrderServiceOne @RestController, with OrderService injected through the constructor
  • GET /api/orders:?userId= 传了就按用户过滤,没传返回全部;200GET /api/orders: filter by user when ?userId= is given, return everything when it is not; 200
  • GET /api/orders/{id}:200;找不到时由全局异常处理器给出 404(控制器不要 catch)GET /api/orders/{id}: 200; when nothing is found, the global exception handler answers 404 (do not catch it in the controller)
  • GET /api/orders/user/{userId}:200GET /api/orders/user/{userId}: 200
  • POST /api/orders:@Valid 校验请求体;成功返回 201 CreatedPOST /api/orders: validate the request body with @Valid; on success return 201 Created
  • PATCH /api/orders/{id}/status:body 是 {"status":"..."};转成 OrderStatus;缺失或非法值返回 400;成功 200PATCH /api/orders/{id}/status: the body is {"status":"..."}; convert it to OrderStatus; a missing or invalid value returns 400; on success 200
  • DELETE /api/orders/{id}:204 No ContentDELETE /api/orders/{id}: 204 No Content
  • 六个端点都用 SLF4J 打日志,并带上 MDC 里的 correlationIdAll six endpoints log through SLF4J and carry the correlationId from MDC
  • 自己写一个 CorrelationIdFilter:读 X-Correlation-ID 头,没有就生成 UUID,放进 MDC,finally 里清理Write your own CorrelationIdFilter: read the X-Correlation-ID header, generate a UUID when it is absent, put it in MDC, and clear it in finally
  • 自己写 GlobalExceptionHandler:EntityNotFoundException → 404,MethodArgumentNotValidException → 400Write your own GlobalExceptionHandler: EntityNotFoundException → 404, MethodArgumentNotValidException → 400
你需要自己建的文件Files you create yourself
文件清单File list
pom.xmlparent 用 spring-boot-starter-parent 3.3.2,java.version 17,四个依赖 + spring-boot-maven-pluginThe parent is spring-boot-starter-parent 3.3.2, java.version is 17, four dependencies + spring-boot-maven-plugin
src/main/resources/application.propertiesserver.port=8080 就够(顺便按书面题的结论收紧 actuator)server.port=8080 is enough (and tighten actuator while you are here, following the written question)
src/main/java/.../OrderServiceApplication.java@SpringBootApplication + main
src/main/java/.../model/Order.java、OrderItem.java、OrderStatus.java★ 抄源项目的(这是题目)★ Copy it from the source project (this is the question)
src/main/java/.../dto/CreateOrderRequest.java、OrderItemRequest.java★ 抄源项目的:带 @NotBlank / @NotEmpty / @Min / @Valid★ Copy it from the source project: it carries @NotBlank / @NotEmpty / @Min / @Valid
src/main/java/.../repository/OrderRepository.java、InMemoryOrderRepository.java★ 抄源项目的:接口 + 内存实现(含一条种子数据)★ Copy it from the source project: the interface + an in-memory implementation (with one seed record)
src/main/java/.../service/OrderService.java★ 抄源项目的(这是题目):六个方法,三个会抛 EntityNotFoundException★ Copy it from the source project (this is the question): six methods, three of which throw EntityNotFoundException
src/main/java/.../exception/EntityNotFoundException.java、GlobalExceptionHandler.java★ 自己写:两个 @ExceptionHandler★ You write it: two @ExceptionHandler methods
src/main/java/.../config/CorrelationIdFilter.java★ 自己写:OncePerRequestFilter + MDC★ You write it: OncePerRequestFilter + MDC
src/main/java/.../controller/OrderController.java★★ 自己写:六个端点★★ You write it: six endpoints
src/test/java/.../OrderControllerTest.java★ 抄源项目的(这是判卷器):@WebMvcTest + @MockBean + 五个测试★ Copy it from the source project (this is what grades you): @WebMvcTest + @MockBean + five tests
写完后在本机这样验证Verify it locally like this
mvn test
Tests run: 5, Failures: 0, Errors: 0 — BUILD SUCCESS
mvn spring-boot:run
服务起在 8080,日志里能看到 Started OrderServiceApplicationThe service starts on 8080, and the log shows Started OrderServiceApplication
curl -i -s localhost:8080/api/orders/999
404 + {"timestamp":...,"status":404,"message":"Order not found with id: 999"}
curl -i -s -X POST localhost:8080/api/orders -H 'Content-Type: application/json' -d '{"userId":"123","items":[{"productId":"prod-789","quantity":2}]}'
201 Created + 订单 JSON(totalAmount 应为 299.98)201 Created + the order JSON (totalAmount should be 299.98)
curl -i -s -X POST localhost:8080/api/orders -H 'Content-Type: application/json' -d '{"userId":"","items":[]}'
400 Bad Request(Bean Validation 生效)400 Bad Request (Bean Validation is working)
curl -i -s -X PATCH localhost:8080/api/orders/1/status -H 'Content-Type: application/json' -d '{"status":"FLYING"}'
400 Bad Request(不是 500)400 Bad Request (not 500)
curl -i -s -X DELETE localhost:8080/api/orders/1
204 No Content,body 为空204 No Content, with an empty body
curl -i -s -H 'X-Correlation-ID: my-trace-1' localhost:8080/api/orders
响应头里有同一个 X-Correlation-ID;服务端日志里也是它The response header carries the same X-Correlation-ID, and so does the server log
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。The whole point of this level is writing it with no answer in front of you. Create the files on your machine and run the verification commands first, then come back and compare.

Tick this once you have written it locally and the verification commands give the expected output.
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

拿到空目录You are handed an empty directory
先让空服务能起来,再写业务Get an empty service to start first, then write the logic
有测试文件A test file is provided
先抄进来当判卷器,一条一条攻Copy it in and let it grade you; fix one test at a time
跨模块调用A call that crosses module boundaries
先抄一张方法名 + 签名对照表Write down a table of method names and signatures before you start
写完了You think the code is finished
测试 + verify 脚本 + curl,三层都过才算完Tests, the verify script, and curl: it is done only when all three pass
这节的要点What to take away
  1. 起手式:先让空服务器能起来(能看到 ready 日志),再写业务逻辑。First step: get an empty server to start, so you can see the ready log. Only then write the logic.
  2. schema、数据源、测试是「题目」,抄进来等于搭好考场;resolver 和 index.js 是「答案」,自己写。The schema, the data source and the tests are the question, so copying them in just sets up the exam. The resolvers and index.js are the answer, so write those yourself.
  3. 写跨模块调用之前先抄方法名与签名表 —— 这能挡掉 starter 里那两处埋雷同类的错误。Before you write a call across modules, write down the method names and signatures. That stops the same kind of error as the two hidden problems in the starter code.
  4. 10 个测试全绿只是及格线,还要用 verify 脚本验 _service 和 _entities。All 10 tests passing is only the minimum. You still need the verify script to check _service and _entities.
  5. Java 那半和 subgraph 无代码关联,可以完全独立做。The Java half shares no code with the subgraph, so you can do it completely on its own.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 这一门读完了 —— 去验收Course finished — go get checked考场:空文件夹、计时、没有提示按钮The arena: an empty folder, a clock, no hint button
    去考场To the arena
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: Debug Lab · Federation 十种典型故障Debug Lab · ten common Federation failures