第 3 章 · Spring MVC 与 REST Controller
本章目标:掌握 @RestController、@GetMapping / @PostMapping 与路径变量、请求参数;设计 /api/v1 统一前缀与包结构;理解 DispatcherServlet 请求映射流程;能对照 shop-demo(Django) 的 urls.py + views.py 说明路由、视图、响应的差异;在 shop-spring-demo 中完成商品 REST 只读 API 骨架。
学时建议:4~5 小时(含 2 小时跟练)
前置:完成 spring-boot-web ch02 与 java-dev(集合与泛型、HTTP 基础)。
3.1 场景说明:统一 API 路径
虚构网关 https://api.example.com 约定:Java 侧 REST 一律挂在 /api/v1 下,与 Django shop-demo 的 HTML 页面路径分离。
| 类型 | shop-demo 示例 | shop-spring-demo 示例 |
|---|---|---|
| HTML 列表 | GET /catalog/ | ch04 Thymeleaf /catalog |
| JSON API | DRF /api/v1/products/(规划) | GET /api/v1/products |
| 健康检查 | /accounts/health/ | /health(ch01) |
| Admin | /admin/ | 无内置,ch06 自建登录 |
本章只做 JSON 只读 API,写操作与校验在 ch07 扩展。
3.2 Spring MVC 请求处理流程
客户端 HTTP 请求
│
▼
① DispatcherServlet(Front Controller)
│
▼
② HandlerMapping 查找 @GetMapping 等方法
│
▼
③ HandlerAdapter 调用 Controller 方法
│
▼
④ 返回值 → HttpMessageConverter(JSON 用 Jackson)
│
▼
⑤ HTTP 响应(状态码 + Body)
| 阶段 | Django 对照 | |
|---|---|---|
| URL 路由 | urlpatterns + path() | |
| 视图 | views.product_list / CBV | |
| 响应 | JsonResponse / DRF Response | |
| 内容协商 | DRF renderer | Accept 头 + MessageConverter |
3.3 @Controller 与 @RestController
| 注解 | 返回值 | 典型用途 |
|---|---|---|
@Controller | 视图名 → Thymeleaf | ch04 HTML 页面 |
@RestController | 直接写 Body(=@Controller + @ResponseBody) | REST JSON |
@RestController
@RequestMapping("/api/v1/products")
public class ProductApiController {
// 类级别路径前缀
}
等价 Django:
# urls.py
path("api/v1/products/", include("catalog.api_urls"))
# views.py — DRF ViewSet 或 APIView
3.4 统一路径与包结构
推荐目录:
com.example.shop/
├── web/
│ ├── HealthController.java # 根路径 /health
│ └── api/
│ └── v1/
│ └── ProductApiController.java
├── catalog/
│ ├── ProductCatalogService.java
│ └── ...
└── config/
└── WebMvcConfig.java # 可选:全局 CORS、路径规范
3.4.1 全局 API 前缀(可选两种)
方式 A — 类上 @RequestMapping("/api/v1/...")(本章采用,直观)
方式 B — WebMvcConfig 配置 pathPrefix(Boot 2.2+):
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api/v1",
c -> c.getPackageName().startsWith("com.example.shop.web.api.v1"));
}
}
方式 B 下 Controller 可写 @GetMapping("/products"),实际映射为 /api/v1/products。
| 策略 | 优点 | 缺点 |
|---|---|---|
| 类注解 | 易读、IDE 跳转清晰 | 前缀重复 |
| pathPrefix | DRY | 包结构必须规范 |
3.5 ProductApiController 完整示例
package com.example.shop.web.api.v1;
import com.example.shop.catalog.ProductCatalogService;
import com.example.shop.catalog.ProductDto;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@RestController
@RequestMapping("/api/v1/products")
public class ProductApiController {
private final ProductCatalogService catalogService;
public ProductApiController(ProductCatalogService catalogService) {
this.catalogService = catalogService;
}
@GetMapping
public List<ProductDto> list(
@RequestParam(defaultValue = "20") int limit,
@RequestParam(required = false) String q) {
// ch05 前:内存服务;q 过滤 ch07 实现
return catalogService.listFeatured(limit);
}
@GetMapping("/{id}")
public ProductDto detail(@PathVariable Long id) {
return catalogService.listFeatured(100).stream()
.filter(p -> p.id().equals(id))
.findFirst()
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "商品不存在"));
}
}
3.5.1 注解说明
| 注解 | 作用 | Django/DRF 对照 |
|---|---|---|
@GetMapping | GET 路由 | @api_view(['GET']) / ListAPIView |
@PathVariable | URL 路径段 | path("<int:pk>") |
@RequestParam | Query String | request.GET.get('limit') |
defaultValue | 缺省参数 | request.GET.get('limit', 20) |
ResponseStatusException | 快捷 404 | get_object_or_404 / Http404 |
3.6 HTTP 方法与映射对照
| HTTP | Spring 注解 | 幂等 | shop-spring-demo 本章 |
|---|---|---|---|
| GET | @GetMapping | 是 | list / detail |
| POST | @PostMapping | 否 | ch07 创建 |
| PUT | @PutMapping | 是 | ch07 全量更新 |
| PATCH | @PatchMapping | 否 | ch07 部分更新 |
| DELETE | @DeleteMapping | 是 | ch07 删除 |
预览 POST(ch07 实现 body 校验):
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ProductDto create(@RequestBody ProductCreateRequest req) {
throw new UnsupportedOperationException("ch07 实现");
}