DrillLab
第 13 / 17 节LESSON 13 / 17约 16 分钟~16 min

先看懂给你的东西:Spring 的几个注解和一条请求链路Understand what you are given: a few Spring annotations and the path one request takes

没写过 Java 也能看懂 —— 这一节只讲这道题真正需要的那几个概念。You do not need Java experience. This lesson covers only the few ideas this task actually needs.

2 个练习2 exercisesFederation · 第 4 部分Federation · Part 4
这一页有什么On this page9
学完这节你会After this lesson you can
  • 认得 @RestController / @GetMapping / @PathVariable / @RequestBody 等注解Recognise the annotations @RestController / @GetMapping / @PathVariable / @RequestBody
  • 说清构造器注入是什么、OrderService 是怎么进到控制器里的Explain what constructor injection is, and how OrderService gets into the controller
  • 读懂 OrderService 提供了哪些方法、抛什么异常Read which methods OrderService gives you and which exceptions it throws
  • 说清 GlobalExceptionHandler 和 CorrelationIdFilter 各自在做什么Explain what GlobalExceptionHandler and CorrelationIdFilter each do
这在考试里考什么What the exam does with this

业务逻辑全部 PROVIDED。你要写的只是「调用 + 选状态码 + 记日志」。所以读懂已给的部分,这道题就做完一半了。All the business logic is marked PROVIDED. All you write is: call a method, pick a status code, write a log line. So reading the given code is already half of this task.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
graphql-federation-practice/java-service/src/main/java/com/techflow/orders/service/OrderService.java业务逻辑全在这里(PROVIDED)All the business logic is here (PROVIDED)
JavaOrderService.java源项目From source
1package com.techflow.orders.service;
2
3import com.techflow.orders.dto.CreateOrderRequest;
4import com.techflow.orders.exception.EntityNotFoundException;
5import com.techflow.orders.model.Order;
6import com.techflow.orders.model.OrderItem;
7import com.techflow.orders.model.OrderStatus;
8import com.techflow.orders.repository.OrderRepository;
9import io.micrometer.core.instrument.Counter;
10import io.micrometer.core.instrument.MeterRegistry;
11import io.micrometer.core.instrument.Timer;
12import java.time.Instant;
13import java.util.List;
14import org.slf4j.Logger;
15import org.slf4j.LoggerFactory;
16import org.slf4j.MDC;
17import org.springframework.stereotype.Service;
18
19@Service
20public class OrderService {
21 private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
22
23 private final OrderRepository orderRepository;
24 private final Counter orderCreatedCounter;
25 private final Counter orderRetrievedCounter;
26 private final Timer orderCreationTimer;
27
28 public OrderService(OrderRepository orderRepository, MeterRegistry meterRegistry) {
29 this.orderRepository = orderRepository;
30 this.orderCreatedCounter = Counter.builder("orders.created")
31 .description("Total number of orders created")
32 .register(meterRegistry);
33 this.orderRetrievedCounter = Counter.builder("orders.retrieved")
34 .description("Total number of orders retrieved")
35 .register(meterRegistry);
36 this.orderCreationTimer = Timer.builder("orders.creation.time")
37 .description("Time taken to create an order")
38 .register(meterRegistry);
39 }
40
41 public List<Order> getAllOrders() {
42 String correlationId = MDC.get("correlationId");
43 logger.info("Fetching all orders. correlationId={}", correlationId);
44 orderRetrievedCounter.increment();
45 return orderRepository.findAll();
46 }
47
48 public Order getOrderById(Long id) {
49 String correlationId = MDC.get("correlationId");
50 logger.info("Fetching order by id. orderId={}, correlationId={}", id, correlationId);
51 orderRetrievedCounter.increment();
52 return orderRepository.findById(id)
53 .orElseThrow(() -> new EntityNotFoundException("Order not found with id: " + id));
54 }
55
56 public List<Order> getOrdersByUserId(String userId) {
57 String correlationId = MDC.get("correlationId");
58 logger.info("Fetching orders by userId. userId={}, correlationId={}", userId, correlationId);
59 orderRetrievedCounter.increment();
60 return orderRepository.findByUserId(userId);
61 }
62
63 public Order createOrder(CreateOrderRequest request) {
64 String correlationId = MDC.get("correlationId");
65
66 return orderCreationTimer.record(() -> {
67 logger.info("Creating new order. userId={}, correlationId={}", request.getUserId(), correlationId);
68
69 Order order = new Order();
70 order.setUserId(request.getUserId());
71 order.setStatus(OrderStatus.PENDING);
72 order.setCreatedAt(Instant.now());
73
74 double totalAmount = 0.0;
75 for (var itemRequest : request.getItems()) {
76 OrderItem item = new OrderItem();
77 item.setProductId(itemRequest.getProductId());
78 item.setQuantity(itemRequest.getQuantity());
79 item.setPrice(getMockPrice(itemRequest.getProductId()));
80 order.getItems().add(item);
81 totalAmount += item.getPrice() * item.getQuantity();
82 }
83
84 order.setTotalAmount(totalAmount);
85 Order savedOrder = orderRepository.save(order);
86 orderCreatedCounter.increment();
87 return savedOrder;
88 });
89 }
90
91 public Order updateOrderStatus(Long id, OrderStatus status) {
92 Order order = orderRepository.findById(id)
93 .orElseThrow(() -> new EntityNotFoundException("Order not found with id: " + id));
94 order.setStatus(status);
95 return orderRepository.save(order);
96 }
97
98 public void deleteOrder(Long id) {
99 if (!orderRepository.existsById(id)) {
100 throw new EntityNotFoundException("Order not found with id: " + id);
101 }
102 orderRepository.deleteById(id);
103 }
104
105 private double getMockPrice(String productId) {
106 return switch (productId) {
107 case "prod-789" -> 149.99;
108 case "prod-101" -> 89.99;
109 case "prod-202" -> 199.99;
110 default -> 99.99;
111 };
112 }
113}
Source: graphql-federation-practice/java-service/src/main/java/com/techflow/orders/service/OrderService.java
graphql-federation-practice/java-service/src/main/java/com/techflow/orders/exception/GlobalExceptionHandler.java404 与 400 的统一出口(PROVIDED)The single place 404 and 400 come out of (PROVIDED)
JavaGlobalExceptionHandler.java源项目From source
1package com.techflow.orders.exception;
2
3import java.time.Instant;
4import java.util.Map;
5import org.springframework.http.HttpStatus;
6import org.springframework.http.ResponseEntity;
7import org.springframework.web.bind.MethodArgumentNotValidException;
8import org.springframework.web.bind.annotation.ExceptionHandler;
9import org.springframework.web.bind.annotation.RestControllerAdvice;
10
11@RestControllerAdvice
12public class GlobalExceptionHandler {
13 @ExceptionHandler(EntityNotFoundException.class)
14 public ResponseEntity<Map<String, Object>> handleNotFound(EntityNotFoundException ex) {
15 return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of(
16 "timestamp", Instant.now().toString(),
17 "status", 404,
18 "message", ex.getMessage()
19 ));
20 }
21
22 @ExceptionHandler(MethodArgumentNotValidException.class)
23 public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
24 return ResponseEntity.badRequest().body(Map.of(
25 "timestamp", Instant.now().toString(),
26 "status", 400,
27 "message", "Invalid request"
28 ));
29 }
30}
Source: graphql-federation-practice/java-service/src/main/java/com/techflow/orders/exception/GlobalExceptionHandler.java
graphql-federation-practice/java-service/src/main/java/com/techflow/orders/config/CorrelationIdFilter.java把 correlation id 放进 MDC(PROVIDED)Puts the correlation id into the MDC (PROVIDED)
JavaCorrelationIdFilter.java源项目From source
1package com.techflow.orders.config;
2
3import jakarta.servlet.FilterChain;
4import jakarta.servlet.ServletException;
5import jakarta.servlet.http.HttpServletRequest;
6import jakarta.servlet.http.HttpServletResponse;
7import java.io.IOException;
8import java.util.UUID;
9import org.slf4j.MDC;
10import org.springframework.stereotype.Component;
11import org.springframework.web.filter.OncePerRequestFilter;
12
13@Component
14public class CorrelationIdFilter extends OncePerRequestFilter {
15 private static final String HEADER = "X-Correlation-ID";
16
17 @Override
18 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
19 FilterChain filterChain) throws ServletException, IOException {
20 String correlationId = request.getHeader(HEADER);
21 if (correlationId == null || correlationId.isBlank()) {
22 correlationId = UUID.randomUUID().toString();
23 }
24
25 MDC.put("correlationId", correlationId);
26 response.setHeader(HEADER, correlationId);
27
28 try {
29 filterChain.doFilter(request, response);
30 } finally {
31 MDC.remove("correlationId");
32 }
33 }
34}
Source: graphql-federation-practice/java-service/src/main/java/com/techflow/orders/config/CorrelationIdFilter.java
graphql-federation-practice/java-service/src/main/java/com/techflow/orders/controller/OrderController.java六个 TODOSix TODOs

提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.

JavaOrderController.java源项目From source
1package com.techflow.orders.controller;
2
3import com.techflow.orders.dto.CreateOrderRequest;
4import com.techflow.orders.model.Order;
5import com.techflow.orders.service.OrderService;
6import jakarta.validation.Valid;
7import java.util.List;
8import java.util.Map;
9import org.springframework.http.ResponseEntity;
10import org.springframework.web.bind.annotation.DeleteMapping;
11import org.springframework.web.bind.annotation.GetMapping;
12import org.springframework.web.bind.annotation.PatchMapping;
13import org.springframework.web.bind.annotation.PathVariable;
14import org.springframework.web.bind.annotation.PostMapping;
15import org.springframework.web.bind.annotation.RequestBody;
16import org.springframework.web.bind.annotation.RequestParam;
17import org.springframework.web.bind.annotation.RestController;
18
19@RestController
20public class OrderController {
21 private final OrderService orderService;
22
23 public OrderController(OrderService orderService) {
24 this.orderService = orderService;
25 }
26
27 @GetMapping("/")
28 public ResponseEntity<Map<String, String>> getRoot() {
29 return ResponseEntity.ok(Map.of(
30 "message", "Welcome to TechFlow Order Service API",
31 "status", "running"
32 ));
33 }
34
35 @GetMapping("/api/orders")
36 public ResponseEntity<List<Order>> getAllOrders(
37 @RequestParam(required = false) String userId) {
38 // TODO: Implement REST endpoint with structured logging and request validation
39 return null;
40 }
41
42 @GetMapping("/api/orders/{id}")
43 public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
44 // TODO: Implement GET endpoint for order by ID
45 return null;
46 }
47
48 @GetMapping("/api/orders/user/{userId}")
49 public ResponseEntity<List<Order>> getOrdersByUserId(@PathVariable String userId) {
50 // TODO: Implement GET endpoint for orders by user ID
51 return null;
52 }
53
54 @PostMapping("/api/orders")
55 public ResponseEntity<Order> createOrder(
56 @Valid @RequestBody CreateOrderRequest request) {
57 // TODO: Implement POST endpoint with Bean Validation and proper HTTP status
58 return null;
59 }
60
61 @PatchMapping("/api/orders/{id}/status")
62 public ResponseEntity<Order> updateOrderStatus(
63 @PathVariable Long id,
64 @RequestBody Map<String, String> statusUpdate) {
65 // TODO: Implement PATCH endpoint for order status updates
66 return null;
67 }
68
69 @DeleteMapping("/api/orders/{id}")
70 public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
71 // TODO: Implement DELETE endpoint
72 return null;
73 }
74}
Source: graphql-federation-practice/java-service/src/main/java/com/techflow/orders/controller/OrderController.java
§01

这道题会用到的注解,一张表说完One table covers every annotation this task uses

Java 的注解就是「贴在代码上的标签」,框架读这些标签决定怎么处理。A Java annotation is a label attached to code. The framework reads the label and decides how to handle that code.

注解贴在哪作用
@RestController类上「这个类的方法返回值直接当 HTTP 响应体(JSON)」, 不是返回视图名
@GetMapping("/api/orders")方法上GET /api/orders 路由到这个方法
@PostMapping / @PatchMapping / @DeleteMapping方法上同上,对应各自的 HTTP 方法
@PathVariable Long id参数上从路径里取值:/api/orders/1 id = 1L
@RequestParam(required = false) String userId参数上从查询串里取值:?userId=123required = false 表示可以不传(此时是 null)
@RequestBody参数上把请求体的 JSON 反序列化成这个 Java 对象
@Valid参数上触发 Bean Validation(校验 DTO 上的@NotBlank 等约束)
@Service / @Repository类上「把这个类交给 Spring 管理」,于是它能被注入到别处
@RestControllerAdvice类上全局异常处理器 —— 所有控制器抛的异常都会经过它

只有这些。这道题不需要你懂 Spring 的 bean 生命周期、AOP、事务传播。

AnnotationGoes onWhat it does
@RestControllerthe class“Whatever the methods of this class return is the HTTP response body (JSON)” — not a view name
@GetMapping("/api/orders")a methodRoutes GET /api/orders to this method
@PostMapping / @PatchMapping / @DeleteMappinga methodSame thing for their own HTTP methods
@PathVariable Long ida parameterTakes a value out of the path: /api/orders/1 id = 1L
@RequestParam(required = false) String userIda parameterTakes a value out of the query string: ?userId=123. required = false means it may be absent, and then it is null
@RequestBodya parameterDeserializes the JSON request body into this Java object
@Valida parameterFires Bean Validation, which checks the @NotBlank and friends declared on the DTO
@Service / @Repositorythe class“Let Spring manage this class”, which is what makes it injectable elsewhere
@RestControllerAdvicethe classGlobal exception handler — every exception thrown by any controller passes through it

That is the whole list. This paper does not need you to understand Spring bean lifecycles, AOP, or transaction propagation.

§02

OrderService 是怎么进到控制器里的How OrderService gets into the controller

构造器注入 —— 一行代码就能理解。Constructor injection. One line of code is enough to understand it.

看 starter 里已经给好的这几行:

这是构造器注入(constructor injection)。 流程是:

  1. OrderService 类上有 @Service, 所以 Spring 启动时会创建它的实例并管起来。
  2. OrderController 的构造器需要一个OrderService
  3. Spring 创建控制器时,自动把它管着的那个实例传进来

所以你在方法里可以直接用 orderService.xxx()不需要 new OrderService()

这部分已经给好了,别动。你只需要知道 orderService 这个字段随时可用。

Look at these lines, already written for you in the starter:

This is constructor injection. Here is the sequence:

  1. OrderService carries @Service, so Spring builds one instance at startup and holds on to it.
  2. The constructor of OrderController asks for an OrderService.
  3. When Spring builds the controller it hands over the instance it is holding.

So inside your methods you can call orderService.xxx() straight away. No new OrderService() anywhere.

This part is already done. Leave it alone. All you need to know is that the orderService field is there whenever you want it.

JavaOrderController.java(已给好)OrderController.java (given to you)源项目From source
1private final OrderService orderService;
2
3public OrderController(OrderService orderService) {
4 this.orderService = orderService;
5}
Source: graphql-federation-practice/java-service/src/main/java/com/techflow/orders/controller/OrderController.java
§03

OrderService 给了你什么What OrderService gives you

这张表就是你的工具箱。写代码前抄一遍。This table is your toolbox. Copy it out before you write any code.

方法返回找不到时
getAllOrders()List<Order>返回空列表
getOrderById(Long id)OrderEntityNotFoundException
getOrdersByUserId(String userId)List<Order>返回空列表
createOrder(CreateOrderRequest request)Order—(内部会查价格、算总价、计数)
updateOrderStatus(Long id, OrderStatus status)OrderEntityNotFoundException
deleteOrder(Long id)voidEntityNotFoundException

注意第二列的 void 和第三列那三个 「抛异常」。它们直接决定了你的端点该怎么写:

  • deleteOrder 返回 void → 没有内容可返回 → 204 No Content
  • 三个会抛 EntityNotFoundException 的方法 →你不要 try/catch 它, 让它冒到全局处理器去(下一段说为什么)。

OrderService 里还有一些你不需要管的东西: Micrometer 的计数器(orders.createdorders.retrieved)和 Timer、 以及从 MDC 里读 correlationId 打日志。这些说明这个项目在演示「可观测性」这个主题—— 所以你的控制器里也该打日志、也该带 correlationId。

MethodReturnsWhen nothing is found
getAllOrders()List<Order>an empty list
getOrderById(Long id)Orderthrows EntityNotFoundException
getOrdersByUserId(String userId)List<Order>an empty list
createOrder(CreateOrderRequest request)Order— (it looks up prices, totals them, bumps the counter)
updateOrderStatus(Long id, OrderStatus status)Orderthrows EntityNotFoundException
deleteOrder(Long id)voidthrows EntityNotFoundException

Look hard at the void in column two and the three “throws” cells in column three. They decide how your endpoints have to be written:

  • deleteOrder returns void → there is nothing to send back → 204 No Content.
  • The three methods that throw EntityNotFoundException do not try/catch them. Let the exception bubble up to the global handler. The next section explains why.

OrderService also holds things you can ignore: the Micrometer counters (orders.created, orders.retrieved), a Timer, and reading the correlationId out of MDC for logging. All of that says the project is demonstrating observability — so your controller should log too, and it should carry the correlationId.

JavaOrderService.java(节选,PROVIDED)OrderService.java (extract, PROVIDED)源项目From source
1public Order getOrderById(Long id) {
2 String correlationId = MDC.get("correlationId");
3 logger.info("Fetching order by id. orderId={}, correlationId={}", id, correlationId);
4 orderRetrievedCounter.increment();
5 return orderRepository.findById(id)
6 .orElseThrow(() -> new EntityNotFoundException("Order not found with id: " + id));
7}
8
9public void deleteOrder(Long id) {
10 if (!orderRepository.existsById(id)) {
11 throw new EntityNotFoundException("Order not found with id: " + id);
12 }
13 orderRepository.deleteById(id);
14}
Source: graphql-federation-practice/java-service/src/main/java/com/techflow/orders/service/OrderService.java
orElseThrow 的意思是「Optional 里有值就取出来,没有就抛这个异常」。注意 service 已经自己打了日志、也自己读了 MDC —— 这是在给你示范控制器里该怎么做。orElseThrow means "take the value out of the Optional if there is one, otherwise throw this exception". Note the service already logs and already reads the MDC itself. That is a demonstration of what your controller should do.
§04

GlobalExceptionHandler:为什么你不该 try/catchGlobalExceptionHandler: why you should not write try/catch

这是这道题最容易做反的一处设计。This is the design decision people most often get backwards in this task.

@RestControllerAdvice 标记的类是全局异常处理器。任何控制器方法抛出的异常, 如果这里有对应的 @ExceptionHandler, 就由它来转成 HTTP 响应。

项目里给好了两个:

  • EntityNotFoundException404, 响应体是 { timestamp, status, message }
  • MethodArgumentNotValidException(Bean Validation 失败时 Spring 自动抛的)→400

所以控制器里正确的做法是:什么都不做。直接 return ResponseEntity.ok(orderService.getOrderById(id))。 找不到时 service 抛异常,异常冒出控制器, 被全局处理器接住转成 404。

如果你自己 try/catch 会怎样?异常被你吞掉了,全局处理器永远收不到, 于是 404 变成 200(返回 null body)或 500。你把项目已经做好的事情弄坏了。

这是一条通用原则:有全局异常处理器的项目, 控制器里不要写 try/catch—— 除非你要把某个异常转成不同的状态码。 (下一节 PATCH 端点里那个 ResponseStatusException就属于这种例外情况。)

A class marked @RestControllerAdvice is a global exception handler. Any exception thrown by a controller method gets turned into an HTTP response here, as long as there is a matching @ExceptionHandler.

The project gives you two of them:

  • EntityNotFoundException404, with a body of { timestamp, status, message }.
  • MethodArgumentNotValidException (which Spring throws by itself when Bean Validation fails) → 400.

So the right move inside the controller is: do nothing. Just return ResponseEntity.ok(orderService.getOrderById(id)). When the order is missing the service throws, the exception leaves your controller, the global handler catches it and turns it into a 404.

What happens if you try/catch it yourself? You swallowed the exception, the global handler never hears about it, and your 404 becomes a 200 with a null body — or a 500. You broke something the project had already done.

This is a general rule: when a project has a global exception handler, do not write try/catch in controllers — unless you need to turn one exception into a different status code. (The ResponseStatusException in the PATCH endpoint in the next lesson is exactly that exception to the rule.)

JavaGlobalExceptionHandler.java(全文,PROVIDED)GlobalExceptionHandler.java (full file, PROVIDED)源项目From source
1@RestControllerAdvice
2public class GlobalExceptionHandler {
3 @ExceptionHandler(EntityNotFoundException.class)
4 public ResponseEntity<Map<String, Object>> handleNotFound(EntityNotFoundException ex) {
5 return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of(
6 "timestamp", Instant.now().toString(),
7 "status", 404,
8 "message", ex.getMessage()
9 ));
10 }
11
12 @ExceptionHandler(MethodArgumentNotValidException.class)
13 public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
14 return ResponseEntity.badRequest().body(Map.of(
15 "timestamp", Instant.now().toString(),
16 "status", 400,
17 "message", "Invalid request"
18 ));
19 }
20}
Source: graphql-federation-practice/java-service/src/main/java/com/techflow/orders/exception/GlobalExceptionHandler.java
§05

@Valid 与 DTO 上的约束@Valid and the constraints on the DTO

CreateOrderRequest 上已经贴好了校验注解:

@NotBlank(不能是 null / 空 / 全空格)、@NotEmpty(列表不能空)、@Min(1)(数量至少 1)、@Valid贴在列表上时表示「连列表里每个元素也要校验」)。

但这些约束只有在方法参数上写了 @Valid才会生效。starter 已经写了:createOrder(@Valid @RequestBody CreateOrderRequest request)别删掉那个 @Valid —— 删了之后非法请求会带着空 userId 进到 service, 400 变成 500。

这也是一条设计分工:格式校验交给 Bean Validation, 业务校验(比如「这个用户被冻结了」)才写在代码里。

CreateOrderRequest already carries its validation annotations:

@NotBlank (not null, not empty, not all whitespace), @NotEmpty (the list cannot be empty), @Min(1) (quantity is at least 1), and @Valid (on a list it means “validate every element inside too”).

But none of those constraints fire unless the method parameter is marked @Valid. The starter already did it: createOrder(@Valid @RequestBody CreateOrderRequest request). Do not delete that @Valid — without it a bad request walks into the service with an empty userId, and your 400 turns into a 500.

This is also a division of labour: shape checks belong to Bean Validation, business checks (say “this user is suspended”) belong in your code.

Java两个 DTO(PROVIDED)The two DTOs (PROVIDED)源项目From source
1public class CreateOrderRequest {
2 @NotBlank
3 private String userId;
4 @Valid
5 @NotEmpty
6 private List<OrderItemRequest> items;
7 // getter / setter ...
8}
9
10public class OrderItemRequest {
11 @NotBlank
12 private String productId;
13 @Min(1)
14 private int quantity;
15 // getter / setter ...
16}
Source: graphql-federation-practice/java-service/src/main/java/com/techflow/orders/dto/
§06

CorrelationIdFilter:Java 版的 correlation idCorrelationIdFilter: the Java version of a correlation id

和 Node 那边一个思路:优先用请求头里的X-Correlation-ID,没有就生成一个 UUID

区别在于存放位置。Node 那边放进 context 手动往下传; Java 这边放进 MDC(Mapped Diagnostic Context)—— 一个和当前线程绑定的键值存储。 这样同一个线程里任何地方都能MDC.get("correlationId") 取到, 不需要一层层传参。

注意 finally 里的 MDC.remove —— 线程是复用的,不清理会导致下一个请求读到上一个的 id。这个细节要记住 ——它是「线程局部存储必须清理」这个通用原则的实例。

你要做的:在六个端点里logger.info(...) 时带上MDC.get("correlationId")。 第一个 TODO 明确写了with structured logging

Same idea as on the Node side: use the X-Correlation-ID header if the caller sent one, otherwise generate a UUID.

The difference is where it gets parked. Node put it in context and passed it down by hand. Java puts it in MDC (Mapped Diagnostic Context) — a key-value store bound to the current thread. That way anywhere on that same thread can call MDC.get("correlationId") without threading a parameter through every layer.

Look at the MDC.remove in the finally block. Threads get reused, so skipping the cleanup means the next request reads the previous request’s id. That detail earns your attention because it is one instance of a general rule: thread-local storage must be cleaned up.

Your job: in all six endpoints, pass MDC.get("correlationId") into your logger.info(...) calls. The very first TODO spells it out: with structured logging.

JavaCorrelationIdFilter.java(全文,PROVIDED)CorrelationIdFilter.java (full file, PROVIDED)源项目From source
1@Component
2public class CorrelationIdFilter extends OncePerRequestFilter {
3 private static final String HEADER = "X-Correlation-ID";
4
5 @Override
6 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
7 FilterChain filterChain) throws ServletException, IOException {
8 String correlationId = request.getHeader(HEADER);
9 if (correlationId == null || correlationId.isBlank()) {
10 correlationId = UUID.randomUUID().toString();
11 }
12
13 MDC.put("correlationId", correlationId);
14 response.setHeader(HEADER, correlationId);
15
16 try {
17 filterChain.doFilter(request, response);
18 } finally {
19 MDC.remove("correlationId"); // 线程复用,必须清理
20 }
21 }
22}
1@Component
2public class CorrelationIdFilter extends OncePerRequestFilter {
3 private static final String HEADER = "X-Correlation-ID";
4
5 @Override
6 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
7 FilterChain filterChain) throws ServletException, IOException {
8 String correlationId = request.getHeader(HEADER);
9 if (correlationId == null || correlationId.isBlank()) {
10 correlationId = UUID.randomUUID().toString();
11 }
12
13 MDC.put("correlationId", correlationId);
14 response.setHeader(HEADER, correlationId);
15
16 try {
17 filterChain.doFilter(request, response);
18 } finally {
19 MDC.remove("correlationId"); // threads are reused, so clean up
20 }
21 }
22}
Source: graphql-federation-practice/java-service/src/main/java/com/techflow/orders/config/CorrelationIdFilter.java
§07

两个干扰项Two distractors

  • java-service/orders.db —— 一个数据库文件。但 pom.xml没有任何 JDBC 或 JPA 依赖(只有 web / validation / actuator / test), 代码里也没有一处引用它。 数据实际来自 InMemoryOrderRepository(一个 ConcurrentHashMap)。看到 .db 就去配数据源,纯浪费时间。
  • MetricsConfig.java —— 一个空的 @Configuration 类,里面什么都没有。 计数器实际是在 OrderService 的构造器里建的。

判断方法和 Node 那边一样:顺着依赖和引用找。没被 import、 没被注入、没在配置里出现,就跟这次任务无关。

  • java-service/orders.db — a database file. But pom.xml carries no JDBC and no JPA dependency at all (only web, validation, actuator, test), and not one line of code references it. The data actually comes from InMemoryOrderRepository, which is a ConcurrentHashMap. Seeing a .db file and going off to configure a datasource is pure wasted time.
  • MetricsConfig.java — an empty @Configuration class with nothing inside. The counters are actually created in the OrderService constructor.

Same test as on the Node side: follow the dependencies and the references. Not imported, not injected, not mentioned in config — not part of this task.

练习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.

L1认出来Spot it找不到订单时该怎么处理What to do when the order is not found

getOrderById 端点里,orderService.getOrderById(id) 找不到时会抛EntityNotFoundException。 控制器应该怎么写?

In the getOrderById endpoint, orderService.getOrderById(id) throws EntityNotFoundException when it finds nothing. How should the controller be written?

先选一个选项Pick an option first
L1认出来Spot it这三个参数注解各从哪取值Where each of these parameter annotations reads from

请求是 PATCH /api/orders/7/status, body 是 {"status":"SHIPPED"}
方法签名是 updateOrderStatus(@PathVariable Long id, @RequestBody Map<String,String> statusUpdate)idstatusUpdate 分别是什么?

The request is PATCH /api/orders/7/status with the body {"status":"SHIPPED"}.
The method signature is updateOrderStatus(@PathVariable Long id, @RequestBody Map<String,String> statusUpdate). What are id and statusUpdate?

先选一个选项Pick an option first
迁移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.

项目里有 @RestControllerAdviceThe project has an @RestControllerAdvice
控制器里不要 try/catch,让异常冒出去No try/catch in the controller; let the exception travel up
参数上有 @ValidA parameter is marked @Valid
格式校验交给 Bean Validation,别自己写Leave format checks to Bean Validation; do not write them yourself
service 方法返回 voidThe service method returns void
端点大概该返回 204The endpoint probably returns 204
看到一个可疑的资源文件(.db 之类)You see a suspicious resource file, such as a .db file
查 pom.xml 有没有对应依赖,没有就是干扰项Check pom.xml for a matching dependency; if there is none, it is a distractor
需要 correlation idYou need a correlation id
Java 用 MDC.get(),别自己一层层传参In Java use MDC.get(); do not pass it down through every method
这节的要点What to take away
  1. 构造器注入已经写好,orderService 随时可用,不要 new。Constructor injection is already written. orderService is ready to use; never create it with new.
  2. OrderService 的三个方法会抛 EntityNotFoundException —— 别 try/catch,交给全局处理器转 404。Three OrderService methods throw EntityNotFoundException. Do not try/catch them; the global handler turns them into 404.
  3. deleteOrder 返回 void,暗示端点该返回 204。deleteOrder returns void, which hints the endpoint should return 204.
  4. @Valid 必须保留,格式校验靠它;MDC.get("correlationId") 用来打结构化日志。Keep @Valid; it does the format checks. Use MDC.get("correlationId") for structured log lines.
  5. orders.db 和 MetricsConfig 都是干扰项 —— pom.xml 里没有数据库依赖。orders.db and MetricsConfig are both distractors. pom.xml has no database dependency.

接下来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. 接着看下一节Continue to the next lesson六个端点:状态码就是这道题的全部Six endpoints: the status codes are the whole task
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 三处埋雷:怎么系统地找出来The three planted bugs: how to find them systematically