很多Spring Boot新手都踩过这个坑:在控制器类上写@RestController("/api"),然后发现接口访问路径根本没生效。比如下面这段代码:
java复制@RestController("/user")
public class UserController {
@GetMapping("/list")
public String list() {
return "用户列表";
}
}
开发者原本期望接口路径是/user/list,实际访问时却发现路径变成了/list。这个问题困扰过不少初学者,我自己刚学Spring Boot时也掉进过这个坑。要理解这个现象,我们需要从注解的源码设计说起。
打开@RestController的源码(Spring 5.3.9版本),你会发现它的定义非常简单:
java复制@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(annotation = Controller.class)
String value() default "";
}
关键点在于源码注释中的这句话:"The value may indicate a suggestion for a logical component name"。这个value属性实际上是为自动检测的组件提供一个逻辑名称建议,也就是Spring容器中Bean的名称,与URL路径映射完全无关。这就像你给@Service注解传值一样,只是给Bean起个名字而已。
@RestController是一个组合注解,相当于@Controller+@ResponseBody,它的核心作用是:
而@RequestMapping才是真正用来定义请求映射路径的注解。这种职责分离的设计符合单一职责原则,让每个注解只做一件事。
我做过一个对比实验,用两种方式定义相同的API路径:
java复制// 方式一:错误用法
@RestController("/api")
public class WrongController {
@GetMapping("/users")
public List<User> getUsers() { /*...*/ }
}
// 方式二:正确用法
@RestController
@RequestMapping("/api")
public class CorrectController {
@GetMapping("/users")
public List<User> getUsers() { /*...*/ }
}
测试结果:
/users/api/usersSpring MVC处理请求映射时,RequestMappingHandlerMapping会扫描所有控制器方法上的@RequestMapping及其衍生注解(如@GetMapping)。关键点在于:
@RequestMapping定义@RestController的value属性完全不会参与路径计算这是最标准的做法,适合大多数场景:
java复制@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// 实现细节
}
}
对于大型项目,建议定义路径常量:
java复制public interface ApiPaths {
String USER_API = "/api/v1/users";
}
@RestController
@RequestMapping(ApiPaths.USER_API)
public class UserController {
// 方法实现
}
这种方式的好处是:
对于需要动态路径的场景,可以使用路径变量:
java复制@RestController
@RequestMapping("/{tenant}/api/users")
public class MultiTenantUserController {
@GetMapping
public List<User> list(@PathVariable String tenant) {
// 根据租户过滤数据
}
}
如果项目有特殊规范,可以创建组合注解:
java复制@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@RestController
@RequestMapping("/api/v1")
public @interface V1ApiController {
@AliasFor(annotation = RequestMapping.class, attribute = "value")
String[] value() default {};
}
使用方式:
java复制@V1ApiController("/users")
public class CustomUserController {
// 最终路径:/api/v1/users/...
}
REST API必须考虑版本兼容,推荐两种路径版本化方案:
java复制// 方案一:路径显式版本
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 { /*...*/ }
// 方案二:Accept头版本
@RestController
@RequestMapping(value = "/api/users",
produces = "application/vnd.company.app-v1+json")
public class UserControllerV1 { /*...*/ }
根据RESTful最佳实践,路径命名应:
/users而非/user)/departments/{id}/employees)-而非下划线_根据我踩过的坑,特别提醒:
@RequestMapping/users/可能引发匹配问题)/users/{userId}优于/users/{id})当多个映射匹配同一请求时,Spring按照以下顺序选择:
/users/new比/users/{id}优先)路径中的变量和正则表达式可以组合使用:
java复制@GetMapping("/users/{userId:\\d+}")
public User getUser(@PathVariable Long userId) {
// 只匹配数字ID
}
遇到中文路径时需要注意编码:
java复制// 前端需要encodeURIComponent("/api/用户")
@GetMapping("/users/{name}")
public User getByName(@PathVariable String name) {
// name已经是解码后的中文
}
添加这个Bean可以打印所有映射:
java复制@Bean
public CommandLineRunner printMappings(RequestMappingHandlerMapping mapping) {
return args -> {
mapping.getHandlerMethods().forEach((k, v) -> {
System.out.println(k + " => " + v);
});
};
}
开启management.endpoints.web.exposure.include=mappings后,访问/actuator/mappings可以查看所有路径映射。
推荐用MockMvc测试路径是否正确:
java复制@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldMapCorrectPath() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk());
}
}
理解AbstractHandlerMethodMapping的初始化过程很重要:
@Controller Bean@RequestMappingMappingRegistry中HandlerMapping查找匹配的处理器关键源码片段:
java复制// RequestMappingHandlerMapping.java
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
这个设计解释了为什么@RestController的value不参与路径映射——因为它根本没有被RequestMappingHandlerMapping处理。