记一次HandlerMethodArgumentResolver失效的问题

起因:
对代码进行重构,把解析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
发现在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,逻辑细节排查。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值