起因:
对代码进行重构,把解析token的方法从Controller迁移到自定义的参数解析器中,实现代码复用。
问题:
按照正常流程搭建自定义参数解析器,发现最终请求未进入该自定义参数解析器。
Controller
@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自定义参数解析器
@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
@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方法
@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中的解析器。
发现在argumentResolvers集合中,RequestHeaderMethodArgumentResolver类的index比我们自定义解析器的index小,从而优先加载到RequestHeaderMethodArgumentResolver,跳过了我们的自定义解析器。
然后查看查看RequestHeaderMethodArgumentResolver中的逻辑。
@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中添加一个自定义参数,即解决走不到自定义解析器的问题。
@PostMapping
@NeedDistinct
public Result<Void> livenessDetect(@RequestBody LivenessParam livenessParam, @RequestBody LivenessParam livenessParam, String needDistinct) {
if (uid == null){
throw new UserNotExistException();
}
//todo 代码逻辑
}
总结:
排查mvc中解析器,拦截器失效,报错思路
1.首先排查是否出现漏写,如是否有在实现WebMvcConfigurer的配置类中添加自定义的拦截器或解析器。
2.对栈的上游进行排查,查看栈的执行逻辑是否存在直接跳过你预期的方法等问题。
3.从DispatcherServlet#doDispatch进行debug,逻辑细节排查。