原创

Spring Boot与Spring MVC核心注解

一、先把注解体系分清楚

Spring Boot 和 Spring MVC 的注解经常被放在一起讲,但它们解决的问题并不一样:

  • Spring Boot 注解:负责应用启动、自动配置、组件装配。
  • Spring MVC 注解:负责请求映射、参数绑定、响应返回、异常处理。

很多项目里看起来只是“几个注解组合起来能跑”,但真正影响代码可维护性的,不是会不会写注解,而是是否知道:

  1. 这个注解由谁解析;
  2. 解析发生在启动期还是请求期;
  3. 它和哪些默认机制绑定;
  4. 出问题时应该怀疑哪一层。

可以先建立一个最小认知模型:

  • @SpringBootApplication 决定应用怎么启动。
  • @Controller / @RestController 决定类是不是 Web 处理器。
  • @RequestMapping 体系决定请求能不能进来。
  • @RequestParam@PathVariable@RequestBody 决定参数怎么绑定。
  • @ResponseBody 决定返回值是视图还是 HTTP 响应体。
  • @ControllerAdvice@ExceptionHandler 决定异常怎么统一收口。

把这条链路吃透,Spring MVC 的主体就掌握得差不多了。


二、Spring Boot 启动入口的核心注解

1. @SpringBootApplication

这是 Spring Boot 最常见、也最容易被“用会了但没理解”的注解。

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

它本质上是一个组合注解,等价于:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

作用拆解

@SpringBootConfiguration

它本质上又是 @Configuration,表示这是一个配置类,会向 Spring 容器注册 Bean。

@EnableAutoConfiguration

这是 Spring Boot 的关键能力,表示启用自动配置。 Spring Boot 会根据:

  • 当前 classpath 中是否存在某个类;
  • 容器中是否已经存在某个 Bean;
  • 配置文件里是否开启或关闭某个功能;

来决定是否自动装配对应组件。

例如引入 spring-boot-starter-web 后,Spring Boot 会根据条件自动配置:

  • DispatcherServlet
  • Jackson JSON 转换器
  • 内嵌 Tomcat
  • Spring MVC 基础设施
@ComponentScan

默认扫描启动类所在包及其子包,把带有组件语义的类注册到容器中,例如:

  • @Component
  • @Service
  • @Repository
  • @Controller
  • @RestController
  • @Configuration

常见问题

启动类放错包

如果启动类放在过深的子包中,可能导致扫描范围不完整,出现:

  • Controller 扫描不到
  • Service 注入失败
  • Configuration 不生效

最佳实践是把启动类放在项目根包下。

误以为自动配置是“无条件生效”

自动配置从来不是“看见依赖就一定注入”,而是条件装配。 很多问题的根源都在于条件不满足,比如:

  • 缺少类依赖;
  • 自己手动定义了同类型 Bean,覆盖了默认配置;
  • 属性开关关闭了自动配置。

三、Bean 注册与装配层的核心注解

虽然主题重点是 Spring MVC,但不理解容器装配层,很多 MVC 问题也定位不了。

1. @Configuration

表示当前类是配置类,通常用于显式声明 Bean。

@Configuration
public class WebConfig {

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}

核心作用

  • 替代早期 XML 配置;
  • 让一个类承担“Bean 定义入口”的角色;
  • 通常配合 @Bean 使用。

2. @Bean

用于把方法返回值注册到 Spring 容器。

@Configuration
public class AppConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

使用场景

当第三方类你无法修改源码,不能直接加 @Component 时,用 @Bean 最合适。

@Component 的区别

  • @Component:作用在类上,偏向“让 Spring 去扫描发现”。
  • @Bean:作用在方法上,偏向“我显式告诉 Spring 注册这个对象”。

3. @Component@Service@Repository

这几个注解本质上都属于组件标记注解,都会被组件扫描识别为 Bean。

@Service
public class UserService {
}

区别不在“能不能注入”,而在语义

  • @Component:通用组件;
  • @Service:业务层;
  • @Repository:持久层;
  • @Controller:表现层控制器。

@Repository 还有一个额外价值:它在某些场景下会参与数据访问异常转换,把底层异常转换为 Spring 统一的数据访问异常体系。


4. @Autowired

用于自动注入 Bean。

@Service
public class OrderService {

    private final UserService userService;

    public OrderService(UserService userService) {
        this.userService = userService;
    }
}

当前项目更推荐使用构造器注入,而不是字段注入。

为什么更推荐构造器注入

  • 依赖关系更明确;
  • 便于单元测试;
  • 可以保证对象创建后依赖完整;
  • 更适合 final 字段,提升不可变性。

常见问题

当出现注入失败时,优先检查:

  1. 这个类有没有被 Spring 管理;
  2. 扫描路径是否覆盖;
  3. 是否存在多个同类型 Bean;
  4. 是否存在循环依赖。

四、Spring MVC 入口:控制器相关注解

1. @Controller

表示当前类是一个 MVC 控制器。

@Controller
@RequestMapping("/page")
public class PageController {

    @GetMapping("/home")
    public String home() {
        return "home";
    }
}

它的默认语义

如果方法返回 String,默认会被当作视图名处理,而不是响应体。

这意味着:

  • 返回 "home",框架会尝试找名为 home 的视图;
  • 不会直接把 "home" 输出到 HTTP 响应中。

2. @RestController

这是 @Controller + @ResponseBody 的组合注解。

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public UserVO getById(@PathVariable Long id) {
        return new UserVO(id, "Tom");
    }
}

它的核心语义

类中所有方法的返回值,默认都写入 HTTP 响应体,通常序列化为 JSON。

常见误区

@RestController 不是“专门给前后端分离用的”,而是明确告诉 Spring:

这个控制器返回的不是视图,而是响应数据。


五、请求映射:决定请求能否进入方法

1. @RequestMapping

这是最基础的请求映射注解,可标在类上和方法上。

@RestController
@RequestMapping("/orders")
public class OrderController {

    @RequestMapping(value = "/detail", method = RequestMethod.GET)
    public String detail() {
        return "ok";
    }
}

类级别与方法级别组合规则

  • 类上定义公共路径前缀;
  • 方法上定义具体子路径;
  • 最终路径为两者拼接。

例如:

  • 类上:/orders
  • 方法上:/detail
  • 最终:/orders/detail

2. 派生注解:@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping

这些注解本质上是 @RequestMapping 的语义化快捷写法。

@GetMapping("/{id}")
public OrderVO get(@PathVariable Long id) {
    return orderService.get(id);
}

为什么更推荐用派生注解

  • 可读性更强;
  • HTTP 方法一眼可见;
  • 不容易把 method 写错。

3. @PathVariable

用于绑定 URL 路径中的模板变量。

@GetMapping("/users/{id}")
public UserVO getUser(@PathVariable Long id) {
    return userService.getById(id);
}

如果参数名和路径变量名不一致,要显式指定:

@GetMapping("/users/{userId}")
public UserVO getUser(@PathVariable("userId") Long id) {
    return userService.getById(id);
}

适用场景

适合表示资源定位信息,例如:

  • /users/1
  • /orders/1001/items/3

常见问题

路径参数是 URL 结构的一部分,不适合承载复杂筛选条件。 筛选条件更适合放在查询参数里。


4. @RequestParam

用于绑定查询参数或表单参数。

@GetMapping("/search")
public List<UserVO> search(@RequestParam String keyword,
                           @RequestParam(defaultValue = "1") Integer page,
                           @RequestParam(defaultValue = "10") Integer size) {
    return userService.search(keyword, page, size);
}

请求示例:

GET /search?keyword=spring&page=1&size=10

常用属性

  • required:是否必须;
  • defaultValue:默认值。

使用建议

  • 分页参数、排序参数、筛选参数,优先用 @RequestParam
  • 简单单值参数适合直接写;
  • 参数一多,建议封装成对象,而不是在方法参数里堆十几个字段。

5. @RequestBody

用于把请求体中的 JSON/XML 等内容反序列化为 Java 对象。

@PostMapping("/users")
public Long create(@RequestBody UserCreateRequest request) {
    return userService.create(request);
}
public class UserCreateRequest {
    private String username;
    private Integer age;

    // getter/setter
}

底层机制

Spring MVC 会通过 HttpMessageConverter 读取请求体,再结合 Jackson 等转换器把 JSON 转成 Java 对象。

关键特点

  • 一个请求通常只能有一个 @RequestBody
  • 它读取的是 HTTP Body,不是 URL 参数;
  • 常用于 POSTPUTPATCH 请求。

常见问题

问题一:GET 请求配 @RequestBody

技术上不是绝对禁止,但绝大多数场景都不推荐,而且很多客户端、代理、中间件也不按这个模式工作。

问题二:参数绑定失败

常见原因包括:

  • 请求头 Content-Type 不正确;
  • 字段名对不上;
  • 没有默认构造器或 setter;
  • JSON 格式错误;
  • 没有对应消息转换器。

6. @ResponseBody

用于把返回值直接写入响应体。

@Controller
@RequestMapping("/api")
public class ApiController {

    @GetMapping("/ping")
    @ResponseBody
    public String ping() {
        return "pong";
    }
}

如果类上已经使用 @RestController,通常不需要再写 @ResponseBody


六、参数绑定进阶:Spring MVC 真正高频的注解

1. @ModelAttribute

@ModelAttribute 容易被忽略,但它在 Spring MVC 里非常重要。

用法一:绑定请求参数到对象

@GetMapping("/users")
public List<UserVO> list(@ModelAttribute UserQuery query) {
    return userService.list(query);
}
public class UserQuery {
    private String username;
    private Integer page = 1;
    private Integer size = 10;

    // getter/setter
}

请求示例:

GET /users?username=tom&page=1&size=10

Spring 会把查询参数按字段名绑定到 UserQuery 对象。

用法二:在请求处理前向模型中放数据

@ModelAttribute("module")
public String module() {
    return "user";
}

这在传统服务端页面渲染里更常见。

什么时候优先用它

当查询参数较多,且这些参数天然组成一个“查询对象”时,优先用 @ModelAttribute,而不是堆多个 @RequestParam


2. @RequestHeader

用于读取请求头。

@GetMapping("/headers")
public String header(@RequestHeader("User-Agent") String userAgent) {
    return userAgent;
}

适用场景

  • 读取 token 之外的自定义头;
  • 读取版本号、渠道号、追踪 ID;
  • 调试网关转发字段。

3. @CookieValue

用于读取 Cookie。

@GetMapping("/cookie")
public String cookie(@CookieValue("SESSION") String sessionId) {
    return sessionId;
}

在现代前后端分离项目中,它不如 Header 常见,但在会话系统、老项目或某些网关场景下仍然会用到。


4. @RequestPart

用于处理 multipart/form-data 请求中的某个部分,常见于文件上传加结构化参数混合提交。

@PostMapping("/upload")
public String upload(@RequestPart("file") MultipartFile file,
                     @RequestPart("meta") FileMeta meta) {
    return "success";
}

@RequestParam 的区别

  • @RequestParam 更偏简单字段;
  • @RequestPart 更明确面向 multipart 中的独立 part,并可触发消息转换。

七、校验注解:让参数错误在入口处拦住

1. @Valid@Validated

这两个注解经常一起出现,但不完全相同。

@PostMapping("/users")
public Long create(@Validated @RequestBody UserCreateRequest request) {
    return userService.create(request);
}
public class UserCreateRequest {

    @NotBlank(message = "用户名不能为空")
    private String username;

    @Min(value = 1, message = "年龄必须大于0")
    private Integer age;

    // getter/setter
}

@Valid

来自 Jakarta Validation 标准,主要用于触发基础校验。

@Validated

来自 Spring,功能更强,支持分组校验

常见结论

  • 不需要分组时,@Valid@Validated 都能完成大多数工作;
  • 需要分组校验时,用 @Validated

容易踩坑的点

校验注解写了,不代表一定生效。还要满足:

  • 控制器参数上加了 @Valid@Validated
  • 项目引入了校验相关依赖;
  • 全局异常处理能正确接住校验异常。

八、响应状态与异常处理注解

1. @ResponseStatus

可以直接指定响应状态码。

@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/users")
public Long create(@RequestBody UserCreateRequest request) {
    return userService.create(request);
}

也可以用于自定义异常类:

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

适用场景

适合语义非常固定的状态输出,比如资源不存在、创建成功。


2. @ExceptionHandler

用于在控制器内部或全局处理特定异常。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
        return ResponseEntity.status(404).body(ex.getMessage());
    }
}

它解决的核心问题

把异常处理逻辑从业务代码中剥离出来,避免每个方法都写重复的 try-catch


3. @ControllerAdvice@RestControllerAdvice

这是全局增强注解。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidException(MethodArgumentNotValidException ex) {
        Map<String, Object> body = new HashMap<>();
        body.put("code", 400);
        body.put("message", ex.getBindingResult().getFieldError().getDefaultMessage());
        return ResponseEntity.badRequest().body(body);
    }
}

区别

  • @ControllerAdvice:返回值默认仍按 MVC 规则处理,可能走视图解析;
  • @RestControllerAdvice:等价于 @ControllerAdvice + @ResponseBody,更适合 REST API。

常见职责

  • 统一异常响应格式;
  • 统一处理参数校验异常;
  • 统一处理业务异常;
  • 统一处理系统异常。

九、数据绑定细节:为什么“参数明明传了却拿不到”

Spring MVC 参数绑定不是“看起来字段名一样就一定能成功”,它背后有一套清晰规则。

1. 常见绑定来源

Spring MVC 会从不同位置取值:

  • URL 路径:@PathVariable
  • 查询字符串:@RequestParam
  • 请求头:@RequestHeader
  • Cookie:@CookieValue
  • 请求体:@RequestBody
  • 表单/查询参数聚合对象:@ModelAttribute

2. 常见失败原因

参数名不匹配

例如前端传的是 user_name,后端对象字段是 username,没有额外映射规则时就绑定不上。

Content-Type 不匹配

JSON 请求必须保证请求头正确,例如:

Content-Type: application/json

否则 @RequestBody 可能无法解析。

时间类型格式不兼容

例如前端传 2026-04-14 10:20:30,后端是 LocalDateTime,没有统一格式转换时可能失败。

复杂对象混用错误

例如把查询参数场景写成 @RequestBody,或者把 JSON 体场景写成多个 @RequestParam,都会增加绑定失败概率。


十、最容易混淆的几组注解

1. @Controller@RestController

@Controller

  • 偏传统 MVC;
  • 返回视图名;
  • 适合页面渲染。

@RestController

  • 偏 REST API;
  • 返回响应体;
  • 通常用于 JSON 接口。

2. @RequestParam@RequestBody

@RequestParam

  • 取 URL 参数或表单参数;
  • 适合简单字段。

@RequestBody

  • 取请求体;
  • 适合 JSON 对象。

一个典型判断标准是: 参数在 URL 上,就优先想 @RequestParam;参数在 Body 里,就优先想 @RequestBody


3. @PathVariable@RequestParam

@PathVariable

表达“这是路径的一部分,是资源标识”。

@RequestParam

表达“这是筛选条件、辅助参数、控制参数”。

示例:

GET /users/100
GET /users?name=tom&page=1

前者适合 @PathVariable,后者适合 @RequestParam


4. @Component@Bean

@Component

类本身可控,交给 Spring 自动扫描。

@Bean

类本身不可控,或者需要精细化实例化逻辑。


十一、版本差异必须明确的地方

以下内容以 Spring Boot 3.x / Spring Framework 6.x 为主。

1. javax.* 迁移为 jakarta.*

这是 Boot 3.x 非常关键的变化。

例如校验相关注解、Servlet API、JPA API 等,包名大量从:

javax.validation.Valid

迁移为:

jakarta.validation.Valid

如果项目从 Boot 2.x 升级到 Boot 3.x,这类导包问题是最常见的编译错误来源之一。


2. 路径匹配与底层兼容细节

Spring Boot 2.x 与 3.x 在部分默认配置、底层依赖、路径匹配相关行为上存在差异。 升级时不要只看代码能不能编译,更要关注:

  • 路径匹配是否与旧规则一致;
  • 第三方 starter 是否兼容 Spring Framework 6;
  • Servlet 容器版本是否兼容;
  • 校验、序列化、拦截器等扩展点是否受影响。

3. Web 技术栈要区分 MVC 和 WebFlux

Spring MVC 注解体系主要对应 Servlet 模型。 虽然有些注解在 WebFlux 中也出现,但底层执行模型完全不同:

  • Spring MVC:同步阻塞模型为主;
  • WebFlux:响应式非阻塞模型。

讨论控制器注解时,不能把两套运行机制混为一谈。


十二、一个更接近实际项目的控制器示例

@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {

    @GetMapping("/{id}")
    public UserVO getById(@PathVariable Long id) {
        return new UserVO(id, "Tom", 20);
    }

    @GetMapping
    public List<UserVO> list(@ModelAttribute UserQuery query) {
        return List.of(new UserVO(1L, "Tom", 20));
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Long create(@Validated @RequestBody UserCreateRequest request) {
        return 1001L;
    }

    @PutMapping("/{id}")
    public Boolean update(@PathVariable Long id,
                          @Validated @RequestBody UserUpdateRequest request) {
        return true;
    }

    @DeleteMapping("/{id}")
    public Boolean delete(@PathVariable Long id) {
        return true;
    }

    @GetMapping("/header")
    public String header(@RequestHeader("X-Trace-Id") String traceId) {
        return traceId;
    }
}
public class UserQuery {
    private String username;
    private Integer page = 1;
    private Integer size = 10;

    // getter/setter
}
public class UserCreateRequest {

    @NotBlank(message = "用户名不能为空")
    private String username;

    @Min(value = 1, message = "年龄必须大于0")
    private Integer age;

    // getter/setter
}
public class UserUpdateRequest {

    @NotBlank(message = "用户名不能为空")
    private String username;

    @Min(value = 1, message = "年龄必须大于0")
    private Integer age;

    // getter/setter
}

这个例子基本覆盖了 REST 接口里最常见的注解组合:

  • @RestController
  • @RequestMapping
  • @GetMapping / @PostMapping / @PutMapping / @DeleteMapping
  • @PathVariable
  • @ModelAttribute
  • @RequestBody
  • @RequestHeader
  • @Validated
  • @ResponseStatus

十三、统一异常处理示例

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex) {

        String message = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .findFirst()
                .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
                .orElse("参数校验失败");

        Map<String, Object> result = new HashMap<>();
        result.put("code", 400);
        result.put("message", message);

        return ResponseEntity.badRequest().body(result);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleUserNotFound(UserNotFoundException ex) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 404);
        result.put("message", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(result);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(Exception ex) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 500);
        result.put("message", "系统异常");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
    }
}

这类统一异常处理的价值不在于“写法统一”,而在于它能保证:

  • 错误码统一;
  • 响应结构统一;
  • 前端处理逻辑统一;
  • 日志追踪更容易收口。

十四、实际开发中的注解使用建议

1. 不要把一个控制器写成“参数绑定实验场”

控制器方法参数越多,越难维护。 简单参数用 @RequestParam,复杂查询条件封装成 @ModelAttribute 对象,请求体对象用 @RequestBody,保持边界清晰。

2. REST 接口优先用 @RestController

除非你明确在做服务端页面渲染,否则绝大多数接口项目直接使用 @RestController 更自然。

3. 不要滥用 @RequestMapping

能用 @GetMapping@PostMapping 等派生注解时,优先使用派生注解。

4. 参数校验一定要前置

不要等到 Service 层再发现字段为空、数值越界。 入口层就应该用 @Validated + 校验注解拦住非法请求。

5. 异常处理必须全局统一

零散的 try-catch 会让接口风格混乱,也会让错误码体系失控。 业务异常、参数异常、系统异常应该进入统一处理机制。

6. 搞清楚“视图返回”和“响应体返回”是两条路径

很多初学者的问题本质上不是注解不会写,而是没有分清:

  • 返回页面;
  • 返回 JSON;

这会直接影响你该选 @Controller 还是 @RestController,以及方法返回值应该如何设计。


十五、从执行链路理解这些注解,才算真正掌握

一次典型请求进入 Spring MVC,大致会经历这条链路:

  1. 请求进入 Servlet 容器;

  2. DispatcherServlet 统一接收请求;

  3. 根据 @RequestMapping 等映射规则找到处理方法;

  4. 根据参数注解完成绑定:

    • @PathVariable
    • @RequestParam
    • @RequestBody
    • @RequestHeader
  5. 执行参数校验;

  6. 调用控制器方法;

  7. 处理返回值:

    • 视图解析
    • JSON 序列化
  8. 如果中途抛异常,由 @ExceptionHandler / @ControllerAdvice 接管。

真正有经验的开发者看注解,不会只看“表面用途”,而是会直接联想到它在这条链路里的位置。 因为只有理解它在请求生命周期中的职责,才能在出现 404、400、415、500、参数绑定失败、JSON 转换异常、校验异常时快速定位问题。


十六、最后收束

Spring Boot 负责把应用启动起来,把该有的基础设施自动装配好;Spring MVC 负责把一次 HTTP 请求稳定、清晰地落到具体方法上。

核心注解真正重要的不是数量,而是分层:

  • 启动装配层@SpringBootApplication@Configuration@Bean
  • 组件注册层@Component@Service@Repository
  • 控制器层@Controller@RestController
  • 请求映射层@RequestMapping@GetMapping@PostMapping
  • 参数绑定层@PathVariable@RequestParam@RequestBody@ModelAttribute
  • 响应处理层@ResponseBody@ResponseStatus
  • 异常治理层@ExceptionHandler@ControllerAdvice@RestControllerAdvice
  • 校验层@Valid@Validated

把这些注解按职责拆开,再放回一次完整请求链路里理解,Spring Boot / Spring MVC 的核心注解体系就不再是“靠记忆拼出来的写法”,而是一个可解释、可定位、可扩展的 Web 开发模型。

正文到此结束
评论插件初始化中...
Loading...