背景 在 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 private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers () { List<HandlerMethodArgumentResolver> resolvers = new ArrayList <>(); 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 ()); 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 ()); if (getCustomArgumentResolvers() != null ) { resolvers.addAll(getCustomArgumentResolvers()); } 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)) { 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)) { String ticket = Cookies.getTokenFromRequest(request); if (StringUtils.isEmpty(ticket)){ throw new NoLoginException (); } return userMoudle.getUserInfo(ticket).getUcId; } if (type.isAssignableFrom(UserInfo.class)) { 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) { }