记一次HandlerMethodArgumentResolver失效的问题

起因:
对代码进行重构,把解析token的方法从Controller迁移到自定义的参数解析器中,实现代码复用。

问题:
按照正常流程搭建自定义参数解析器,发现最终请求未进入该自定义参数解析器。

Controller

1
2
3
4
5
6
7
8
9
10
@PostMapping
@NeedDistinct
public Result<Void> livenessDetect(@RequestHeader Map<String, String> headers, @RequestBody LivenessParam livenessParam) {
//重构前的token解析方法
//Long uid = userService.getUidByToken(getToken(headers));
if (uid == null){
throw new UserNotExistException();
}
//todo 代码逻辑
}

UserInfoArgumentResolver自定义参数解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class UserInfoArgumentResolver extends BaseArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
//若方法存在@NeedDistinct,则返回true执行下面的resolveArgument方法
return parameter.hasMethodAnnotation(NeedDistinct.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//对token进行解析
return null;
}

配置类实现WebMvcConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableAsync
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private UserInfoArgumentResolver userInfoArgumentResolver;

//把自定义解析器添加到解析器集合当中
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userInfoArgumentResolver);
}
}

原因:因为在获得参数解析器的过程中,Controller的入参存在@RequestHeader注解的Map参数,mvc优先解析到其他的参数解析器,从而跳过了自定义参数解析器的判断与执行。

解决思路:
通过idea的栈信息,往上层找到调用参数解析器中 supportsParameter 与 resolveArgument的方法,然后定位到HandlerMethodArgumentResolverComposite中的getArgumentResolver方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}

此时对该方法进行debug,发现在遍历argumentResolvers的过程中,优先加载到一个RequestHeaderMethodArgumentResolver的类执行成功,并且放到缓存中。下一次每次发送该Controller的请求,会直接从缓存中获得。

从上面的方法可以看出,mvc在获得参数解析器时, 会先从缓存中获得参数解析器,若不存在,则遍历argumentResolvers,若有一个解析器的supportsParameter结果为true,则对该解析器进行缓存,并退出遍历。
意味着只要有一个其他的参数解析器比我们自定义的UserInfoArgumentResolver优先加载成功,则不会执行我们自定义的解析器的逻辑。

此时再查看argumentResolvers中的解析器。

img

发现在argumentResolvers集合中,RequestHeaderMethodArgumentResolver类的index比我们自定义解析器的index小,从而优先加载到RequestHeaderMethodArgumentResolver,跳过了我们的自定义解析器。

然后查看查看RequestHeaderMethodArgumentResolver中的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(RequestHeader.class) &&
!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType()));
}

@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
RequestHeader ann = parameter.getParameterAnnotation(RequestHeader.class);
Assert.state(ann != null, "No RequestHeader annotation");
return new RequestHeaderNamedValueInfo(ann);
}

发现当传入参数包含注解@RequestHeader,并且参数为Map时,该参数解析器则返回为true。

所以,看到这里就可以得知,我们只需要把原来Controller中的headers去掉,RequestHeaderMethodArgumentResolver则会返回false,argumentResolvers会继续遍历,直到遍历到我们自定义的UserInfoArgumentResolver为止。或者我们可以在Controller中添加一个自定义参数,即解决走不到自定义解析器的问题。

1
2
3
4
5
6
7
8
@PostMapping
@NeedDistinct
public Result<Void> livenessDetect(@RequestBody LivenessParam livenessParam, @RequestBody LivenessParam livenessParam, String needDistinct) {
if (uid == null){
throw new UserNotExistException();
}
//todo 代码逻辑
}

转载自:
https://dongguabai.blog.csdn.net/article/details/135724410?spm=1001.2014.3001.5502