需求
在 Spring MVC 中,控制器方法通常需要从 HttpServletRequest、请求头、Token 或 Session 中提取信息。如果每个方法都手动解析,代码会变得重复且难以维护。
更优雅的方式是:让 Spring MVC 自动解析请求,构造一个自定义上下文对象,并直接作为控制器方法参数注入。
例如:
@GetMapping("/profile")
public Map<String, Object> getProfile(UserContext userContext) {
// 直接使用已经构造好的上下文对象
}
要实现这一点,核心是 HandlerMethodArgumentResolver。
核心接口:HandlerMethodArgumentResolver
HandlerMethodArgumentResolver 只有两个关键方法:
-
supportsParameter(MethodParameter parameter)
判断当前解析器是否支持该参数。可以基于注解判断,也可以仅基于参数类型判断。 -
resolveArgument(...)
当supportsParameter返回true时调用,负责从请求中提取数据,构造并返回最终参数对象。
方式一:使用自定义注解(推荐)
推荐通过自定义注解精确控制哪些参数需要特殊解析,避免影响其他同类型参数。
1. 定义注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {
}
2. 定义上下文类
public class UserContext {
private String userId;
private String username;
// 构造器、Getter/Setter 省略
}
3. 实现解析器
下面示例从请求头中解析用户信息并构造 UserContext。
@Component
public class UserContextArgumentResolver implements HandlerMethodArgumentResolver {
private final ObjectMapper objectMapper;
public UserContextArgumentResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 仅支持带有 @CurrentUser 注解的 UserContext 类型参数
return parameter.hasParameterAnnotation(CurrentUser.class)
&& UserContext.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
String jsonContext = webRequest.getHeader("X-User-Context");
if (jsonContext == null) {
return null;
}
return objectMapper.readValue(jsonContext, UserContext.class);
}
}
4. 注册解析器
在 Spring Boot 中,实现 WebMvcConfigurer 并重写 addArgumentResolvers。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final UserContextArgumentResolver userContextArgumentResolver;
public WebMvcConfig(UserContextArgumentResolver userContextArgumentResolver) {
this.userContextArgumentResolver = userContextArgumentResolver;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userContextArgumentResolver);
}
}
5. 控制器中使用
@RestController
@RequestMapping("/api")
public class ExampleController {
@GetMapping("/profile")
public Map<String, Object> getProfile(@CurrentUser UserContext userContext) {
Map<String, Object> result = new HashMap<>();
result.put("userId", userContext.getUserId());
result.put("username", userContext.getUsername());
return result;
}
}
方式二:无注解,按类型自动解析
如果希望去掉注解,仅根据参数类型解析,可以修改 supportsParameter 的判断逻辑。
1. 修改解析器
@Component
public class UserContextArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 只判断类型,不再要求注解
return UserContext.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
UserContext context = new UserContext();
context.setUserId(webRequest.getHeader("X-User-Id"));
context.setUsername(webRequest.getHeader("X-Username"));
return context;
}
}
2. 控制器中使用
@RestController
@RequestMapping("/api")
public class ExampleController {
@GetMapping("/profile")
public Map<String, Object> getProfile(UserContext userContext) {
Map<String, Object> result = new HashMap<>();
result.put("userId", userContext.getUserId());
result.put("username", userContext.getUsername());
return result;
}
}
3. 关于解析器顺序
通过 WebMvcConfigurer#addArgumentResolvers 注册的自定义解析器,通常会排在无注解的 ServletModelAttributeMethodProcessor(true) 之前,因此仅按类型解析一般可以直接生效。
如果项目中存在其他自定义解析器,或者出现被内置解析器抢先的情况,可以把自定义解析器移动到解析器列表最前面:
@Configuration
public class WebMvcConfig implements SmartInitializingSingleton {
private final RequestMappingHandlerAdapter adapter;
private final UserContextArgumentResolver userContextArgumentResolver;
public WebMvcConfig(RequestMappingHandlerAdapter adapter,
UserContextArgumentResolver userContextArgumentResolver) {
this.adapter = adapter;
this.userContextArgumentResolver = userContextArgumentResolver;
}
@Override
public void afterSingletonsInstantiated() {
List<HandlerMethodArgumentResolver> resolvers =
new ArrayList<>(adapter.getArgumentResolvers());
resolvers.remove(userContextArgumentResolver);
resolvers.add(0, userContextArgumentResolver);
adapter.setArgumentResolvers(resolvers);
}
}
注意:不要同时通过
WebMvcConfigurer#addArgumentResolvers和SmartInitializingSingleton重复添加同一个解析器。
注意事项
- 优先级冲突:Spring MVC 内置解析器优先于自定义解析器。如果参数上同时存在
@RequestBody、@RequestParam等内置注解,内置解析器可能优先生效。 - 无注解方式容易误伤:所有同类型参数都会被自定义解析器处理,包括原本希望使用
@ModelAttribute做数据绑定的场景。 - 推荐使用注解:通过自定义注解精确控制解析范围,可维护性和安全性更好。
- 异常处理:在
resolveArgument中,如果请求缺少必要信息,应进行适当的异常处理或返回null,并在业务逻辑中做好判空。 - 性能考虑:每个请求都会按顺序遍历解析器列表。如果自定义解析器命中率很高,可以调整顺序,让它尽早被检查。
总结
Spring MVC 自定义参数解析的核心是:
- 实现
HandlerMethodArgumentResolver; - 在
supportsParameter中判断是否支持当前参数; - 在
resolveArgument中构造并返回自定义对象; - 通过
WebMvcConfigurer#addArgumentResolvers注册解析器。
如果追求可控性,推荐使用自定义注解方式;如果确实需要完全按类型自动解析,也可以去掉注解,但要注意解析器顺序和潜在冲突。