Skip to content
Go back

Spring MVC 自定义参数解析:按类型注入请求上下文

Published:  at  12:00 AM

需求

在 Spring MVC 中,控制器方法通常需要从 HttpServletRequest、请求头、Token 或 Session 中提取信息。如果每个方法都手动解析,代码会变得重复且难以维护。

更优雅的方式是:让 Spring MVC 自动解析请求,构造一个自定义上下文对象,并直接作为控制器方法参数注入。

例如:

@GetMapping("/profile")
public Map<String, Object> getProfile(UserContext userContext) {
    // 直接使用已经构造好的上下文对象
}

要实现这一点,核心是 HandlerMethodArgumentResolver。

核心接口:HandlerMethodArgumentResolver

HandlerMethodArgumentResolver 只有两个关键方法:

  1. supportsParameter(MethodParameter parameter)
    判断当前解析器是否支持该参数。可以基于注解判断,也可以仅基于参数类型判断。

  2. 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 自定义参数解析的核心是:

  1. 实现 HandlerMethodArgumentResolver;
  2. 在 supportsParameter 中判断是否支持当前参数;
  3. 在 resolveArgument 中构造并返回自定义对象;
  4. 通过 WebMvcConfigurer#addArgumentResolvers 注册解析器。

如果追求可控性,推荐使用自定义注解方式;如果确实需要完全按类型自动解析,也可以去掉注解,但要注意解析器顺序和潜在冲突。


Suggest Changes

Previous Post
Spring MVC 拦截器实战:从 Filter 区别到登录、权限、日志与监控
Next Post
如何在 Feign 客户端中配置超时时间和重试机制?