掌握了 Spring MVC 的整体运行流程之后,就可以开始编写真正处理 HTTP 请求的控制器了。控制器是 MVC 中承上启下的核心组件:它接收前端请求,调用业务层处理,再决定返回什么内容给客户端。Spring 为此提供了一套丰富且直观的注解,能够将 HTTP 请求的各个部分——URL 路径、查询字符串、请求体——清晰地映射到 Java 方法的参数上。
12.2.1 请求映射:从 @RequestMapping 到快捷注解
所有控制器类都必须被 Spring 管理,标注 @Controller 或 @RestController。两者的区别在于:
@Controller:用于传统的视图渲染,方法通常返回视图名(如"orderList"),配合模板引擎使用。@RestController:等于@Controller+@ResponseBody,每个方法的返回值直接写入 HTTP 响应体,通常序列化为 JSON。开发 RESTful API 时,统一使用@RestController。
在类和方法上使用 @RequestMapping 可以指定该方法处理的请求路径和 HTTP 方法。但是,直接使用 @RequestMapping 需要额外指定 method 属性,略显繁琐。Spring 提供了更语义化的快捷注解:
@GetMapping:处理 GET 请求@PostMapping:处理 POST 请求@PutMapping:处理 PUT 请求@DeleteMapping:处理 DELETE 请求@PatchMapping:处理 PATCH 请求
这些注解其实就是 @RequestMapping 的特定方法版本,阅读代码时一目了然。
下面是一个最简单的控制器示例,展示如何使用 @GetMapping 处理根路径的请求:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, Spring MVC!";
}
}
访问 http://localhost:8080/hello,页面会直接显示字符串 "Hello, Spring MVC!"。如果你的项目引入了 Jackson(通常通过 spring-boot-starter-web 自动引入),返回对象会被自动转换为 JSON:
@GetMapping("/user")
public User getUser() {
return new User("张三", "zhangsan@example.com");
}
当方法返回 User 对象时,响应体会是 {"name":"张三","email":"zhangsan@example.com"},Content-Type 自动设为 application/json。
12.2.2 路径参数:@PathVariable
RESTful 风格的 API 经常会在 URL 路径中携带标识符,例如 /users/123 表示获取 id 为 123 的用户。Spring 使用 @PathVariable 将 URL 模板中的变量绑定到方法参数。
首先,在 @GetMapping 的路径中用 {变量名} 声明占位符,然后在方法参数中使用 @PathVariable 接收:
@GetMapping("/users/{id}")
public User getUserById(@PathVariable("id") Long userId) {
// 根据 userId 查询数据库
return userService.findById(userId);
}
如果方法参数名与路径变量名一致,可以省略 @PathVariable 的 value 属性:
@GetMapping("/users/{id}")
public User getUserById(@PathVariable Long id) {
// ...
}
多个路径参数同样直观:
@GetMapping("/orders/{orderId}/items/{itemId}")
public OrderItem getItem(@PathVariable Long orderId,
@PathVariable Long itemId) {
return orderService.findItem(orderId, itemId);
}
在真实项目中,路径参数常用于查询或删除单个资源。务必在 Service 层做好空值处理(如找不到返回 404),避免抛出不可控的异常。
12.2.3 查询参数:@RequestParam
当客户端通过 URL 的查询字符串传递参数(如 /users?page=1&size=10)时,使用 @RequestParam 将其绑定到控制器方法参数。
@GetMapping("/users")
public List<User> listUsers(@RequestParam int page,
@RequestParam int size) {
// page = 1, size = 10
return userService.list(page, size);
}
@RequestParam 的几个重要属性必须掌握:
value/name:指定查询参数名,如果方法参数名与查询参数名一致可省略。required:是否必传,默认true。如果设置为false,参数缺失时值为null(或基本类型的默认值)。defaultValue:指定默认值,当参数缺失时使用。注意:设定了defaultValue后,required自动变为false。
@GetMapping("/search")
public List<Product> search(@RequestParam String keyword,
@RequestParam(defaultValue = "0") int minPrice,
@RequestParam(required = false) String category) {
// keyword 必填,minPrice 默认0,category 可选
return productService.search(keyword, minPrice, category);
}
常见误区: 不要将查询参数用于传递复杂的 JSON 数据。当参数过多或结构复杂时,应该改用 POST 请求并将数据放入请求体。查询参数适合扁平、少量的筛选条件。
12.2.4 请求体:@RequestBody
对于 POST、PUT、PATCH 等请求,客户端通常会将数据放在 HTTP 请求体中,格式多为 JSON。@RequestBody 注解负责将请求体的内容反序列化为 Java 对象。
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.save(user);
}
当客户端发送如下 JSON:
{
"name": "李四",
"email": "lisi@example.com"
}
Spring 会通过 HttpMessageConverter(默认使用 Jackson)将 JSON 映射为 User 对象,再传入方法。整个过程是自动的,但前提是 Java 对象的属性名与 JSON 的字段名相匹配(或使用 @JsonProperty 指定映射)。
与校验配合使用
真实项目中,绝不会信任客户端传入的裸数据,必须进行校验。将 @Valid 或 @Validated 注解加在 @RequestBody 的参数前即可触发 JSR-303 校验:
@PostMapping("/users")
public User createUser(@Valid @RequestBody User user) {
return userService.save(user);
}
在实体类中声明校验规则:
public class User {
@NotBlank(message = "姓名不能为空")
private String name;
@Email(message = "邮箱格式不正确")
private String email;
// getters & setters
}
当校验失败时,Spring 会抛出 MethodArgumentNotValidException,我们可以通过全局异常处理器(@ControllerAdvice)统一返回友好的错误信息。
注意事项:
- 一个方法只能有一个
@RequestBody参数,因为一个 HTTP 请求只有一个请求体。 - 不要将
@RequestBody和表单相关的@ModelAttribute混用,保持设计的一致性。 - 对于文件上传,请求体的处理会有所不同,应该使用
@RequestParam MultipartFile,而不是@RequestBody。
12.2.5 组合实战:一个典型的 CRUD 控制器
将上述注解组合在一起,基本上就能完成一个标准资源的增删改查操作。下面以“商品管理”为例,展示一个完整的 RESTful 控制器片段:
@RestController
@RequestMapping("/products") // 类级别统一路径前缀
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// 分页查询商品列表,支持查询参数
@GetMapping
public List<Product> list(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
return productService.list(page, size);
}
// 根据 ID 查询单个商品
@GetMapping("/{id}")
public Product getById(@PathVariable Long id) {
return productService.findById(id);
}
// 新增商品,请求体携带 JSON 数据
@PostMapping
public Product create(@Valid @RequestBody Product product) {
return productService.save(product);
}
// 全量更新商品
@PutMapping("/{id}")
public Product update(@PathVariable Long id,
@Valid @RequestBody Product product) {
product.setId(id);
return productService.update(product);
}
// 删除商品
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
productService.delete(id);
}
}
这个控制器类覆盖了日常开发中最常见的场景:路径参数用于资源定位,查询参数用于列表筛选,请求体承载复杂的数据结构。所有方法都专注于 HTTP 层面的协调,真正的业务逻辑下沉到 Service 层,控制器的代码因而干净、可测试。
在实际项目中,为了保持代码的整洁和统一,往往还会封装一个通用的响应对象(如 Result<T>)包含状态码、消息和数据,避免在方法中直接返回裸数据。不过,响应封装的优化更适合在“统一响应结构”一节中深入探讨,这里先掌握核心的参数绑定技能即可。