Controller 自定义参数类型解析

背景

在 Spring MVC 中,很多地方我们都会直接或者间接的接触到 Controller 参数注入

例如:

  • @RequestParam 注入请求的 QueryString 参数
  • @PathVariable 注入请求的路径参数变量
  • @RequestBody 注入请求体
  • @CookieValue 注入 Cookie

众所周知,Spring 有良好的拓展性,所以我们是不是也可以注入自定义的对象到 Controller 方法里面呢?

实践

通过追踪源代码,可以看到刚刚那些注解实际上是通过 HandlerMethodArgumentResolver 接口的实现类进行注入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());

// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}

// Catch-all
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));

return resolvers;
}

这里面有个很重要的接口 HandlerMethodArgumentResolver, 用于在给定请求的上下文中将方法参数解析为参数值。简单的理解为:它负责处理你 Handler 方法里的所有入参:包括自动封装、自动赋值、校验等等。有了它才能会让 Spring MVC 处理入参显得那么高级、那么自动化。上面源代码就展示了 Spring MVC 内置的实现。

HandlerMethodArgumentResolver 接口就两个方法,一个用来判断是否支持参数类型,一个是解析的具体实现

1
2
3
4
5
6
7
8
9
10
public interface HandlerMethodArgumentResolver {

// 返回是否支持该参数
boolean supportsParameter(MethodParameter parameter);

// 返回解析后的参数值
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

使用场景

这个接口主要的使用场景就是来实现自定义的参数注入。例如在很多前后端分离的项目中,我们不会去使用 Session,而是自己维护一个 token 来实现状态管理

这种时候,如果需要取得用户数据,正常操作,我们可能需要手动去取得 token,然后去查询用户数据。

例如以下这个例子,我们从 Cookie 中取得 token 数据,然后从 SessionHolder 中查询当前登陆的用户。

1
2
3
4
5
6
7
8
@GetMapping("/currentUser")
public UserInfo getUserInfo(@CookieValue(required = false) String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
Long userId = SessionHolder.get(token).getUcId();
return userDao.getById(userId);
}

这样写没什么问题,不过在实际的项目中,我们可能很多地方都需要用到用户的一些基本信息,每次都这样去手动编码去取,就显得得很繁琐了。所以我们可以自定义 HandlerMethodArgumentResolver 来注入我们当前登录用户的 UserInfo 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Component
public class LoginHandler implements HandlerMethodArgumentResolver {

@Resource
private HttpServletRequest request;

@Override
public boolean supportsParameter(@Nonnull MethodParameter parameter) {
Set<Class<?>> supports = new HashSet<>();
supports.add(UserInfo.class);
supports.add(Long.class);
if (!parameter.hasParameterAnnotation(LoginUser.class)) {
// 参数重带注解 @LoginUser 才进行注入
return false;
}
return supports.stream().anyMatch(cls -> parameter.getParameterType().isAssignableFrom(cls));
}

@Override
public Object resolveArgument(@Nonnull MethodParameter parameter, ModelAndViewContainer mavContainer, @Nonnull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Class<?> type = parameter.getParameterType();

if (type.isAssignableFrom(Long.class)) {
// 登陆用户的 id
String ticket = Cookies.getTokenFromRequest(request);
if (StringUtils.isEmpty(ticket)){
throw new NoLoginException();
}
return userMoudle.getUserInfo(ticket).getUcId;
}

if (type.isAssignableFrom(UserInfo.class)) {
// 登陆用户的 userInfo 对象
String ticket = Cookies.getTokenFromRequest(request);
if (StringUtils.isEmpty(ticket)){
throw new NoLoginException();
}
UserInfo userInfo = userMoudle.getUserInfo(ticket);
if (null == userInfo){
throw new NoLoginException();
}
return userInfo;
}

return null;
}
}

然后实现 WebMvcConfigurer

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Resource
private LoginHandler loginHandler;

@Override
public void addArgumentResolvers(@Nonnull List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginHandler);
}
}

这样我们就能自定义 Controller 的注入对象了。

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/excel")
@ApiOperation("导出excel")
public void exportExcel(ResignJoinListParam param, @LoginUser Long userId) {
// 方法省略...
}

@PostMapping("/excel")
@ApiOperation("导入excel")
public RespBody<Boolean> importExcel(@LoginUser UserInfo user, @RequestBody MultipartFile file) {
// 方法省略...
}