天天看點

項目自定義HandlerMethodArgumentResolver不生效原因與解法

作者:linyb極客之路

前言

本文素材的來源自業務部門技術負責人一次代碼走查引發的故事,技術負責人在某次走查成員的代碼時,發現他們的業務控制層大量充斥着如下的代碼

@PostMapping("add")
    public User add(@RequestBody User user, HttpServletRequest request){
        String tenantId = request.getHeader("x-tenantid");
        String appId = request.getHeader("x-appid");
        user.setAppId(appId);
        user.setTenantId(tenantId);
        return user;
    }
           

他們的tenantId和appId是作為中繼資料放在請求頭,而業務model又需要tenantId和appId,于是他們團隊的成員就寫出了形如上的代碼,雖然這樣的代碼是能滿足業務要求,但是大面積如上的寫法,都是重複性的代碼,很不優雅。後面這個技術負責人項通過自定義HandlerMethodArgumentResolver的方式來優雅解決這問題,他的代碼形如下

@Data
public class MetaInfo {


    private String tenantId;
    
    private String appId;
    
}
           
public class MetaInfoHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    private RequestResponseBodyMethodProcessor handlerMethodArgumentResolver;

    public MetaInfoHandlerMethodArgumentResolver(RequestResponseBodyMethodProcessor handlerMethodArgumentResolver) {
        this.handlerMethodArgumentResolver = handlerMethodArgumentResolver;
    }


    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBody.class) && MetaInfo.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        MetaInfo metaInfo = (MetaInfo) handlerMethodArgumentResolver.resolveArgument(parameter,mavContainer,webRequest,binderFactory);
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        metaInfo.setAppId(request.getHeader("x-appid"));
        metaInfo.setTenantId(request.getHeader("x-tenantid"));

        return metaInfo;
    }
}

           
@Configuration
public class WebConfig implements WebMvcConfigurer {


    @Autowired
    private MetaInfoHandlerMethodArgumentResolver metaInfoHandlerMethodArgumentResolver;



    @Bean
    @ConditionalOnMissingBean
    public MetaInfoHandlerMethodArgumentResolver metaInfoHandlerMethodArgumentResolver(List<HttpMessageConverter<?>> httpMessageConverters){
        RequestResponseBodyMethodProcessor handlerMethodArgumentResolver = new RequestResponseBodyMethodProcessor(httpMessageConverters);
        return new MetaInfoHandlerMethodArgumentResolver(handlerMethodArgumentResolver);
    }


    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(metaInfoHandlerMethodArgumentResolver);
    }
}

           

當他寫下如下代碼時,按他的想法應該是沒問題才對,但是事實上這個HandlerMethodArgumentResolver卻無法生效,他排查了很久,沒啥頭緒,于是就找我探讨了一下。本文就來聊一下該自定義HandlerMethodArgumentResolver不生效原因

為何自定義的HandlerMethodArgumentResolver不生效

看過springmvc的源碼或者背過springmvc相關八股文的朋友,可能會知道springmvc執行HandlerMethodArgumentResolver,主要是通過HandlerMethodArgumentResolverComposite這個聚合器來進行執行。而HandlerMethodArgumentResolverComposite這個聚合器是如何擷取要執行的HandlerMethodArgumentResolver呢?我們可以直接檢視源碼

@Nullable
    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
        if (result == null) {
            Iterator var3 = this.argumentResolvers.iterator();

            while(var3.hasNext()) {
                HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
                if (resolver.supportsParameter(parameter)) {
                    result = resolver;
                    this.argumentResolverCache.put(parameter, resolver);
                    break;
                }
            }
        }

        return result;
    }
           

看到這個源碼,我想老司機應該會有點頭緒,HandlerMethodArgumentResolverComposite内部是會維護一個key為MethodParameter,值為HandlerMethodArgumentResolver的本地緩存,是以要取HandlerMethodArgumentResolver,就會通過MethodParameter來取。

接着我們在來思考一個問題,源碼中的this.argumentResolvers的是什麼時候放進去的,我們繼續跟蹤源碼會發現,他是通過

public HandlerMethodArgumentResolverComposite addResolvers(@Nullable List<? extends HandlerMethodArgumentResolver> resolvers) {
        if (resolvers != null) {
            this.argumentResolvers.addAll(resolvers);
        }

        return this;
    }
           

這個方法進行添加。而addResolvers又是什麼時候被調用的,我們繼續跟蹤源碼,會發現addResolvers,他是會RequestMappingHandlerAdapter的afterPropertiesSet方法中的被調用

@Override
	public void afterPropertiesSet() {
	
			if (this.argumentResolvers == null) {
			List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
		if (this.initBinderArgumentResolvers == null) {
			List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
			this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
	
           

從這個代碼片段,我們可以看到HandlerMethodArgumentResolverComposite初始會添加一些預設的HandlerMethodArgumentResolver

List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
           

而getDefaultArgumentResolvers這方法點開

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,有經驗的老司機看到這裡,應該就知道為啥自定義HandlerMethodArgumentResolver會失效了吧。

自定義HandlerMethodArgumentResolver會失效的原因是當我們方法中有引入@RequestBody時,他的用到的HandlerMethodArgumentResolver是RequestResponseBodyMethodProcessor,而我們自定義的

HandlerMethodArgumentResolver是通過setCustomArgumentResolvers塞進去,而從源碼我們可以看出,我們自定義的HandlerMethodArgumentResolver是放在預設的HandlerMethodArgumentResolver之後

當我們方法中同時存在@RequestBody和自定義HandlerMethodArgumentResolver,因為他們的Method相同,即MethodParameter一樣,是以argumentResolverCache的key是一樣的,從一開始的源碼我們就可以得知,當key已經找到值時,它就直接傳回了,是以當它找到@RequestBody的HandlerMethodArgumentResolver,它就不會再找自定義的HandlerMethodArgumentResolver,這就會導緻我們自定義的HandlerMethodArgumentResolver不生效

HandlerMethodArgumentResolver不生效的解法

1、方法一:直接去掉方法中的@RequestBody

去掉方法中的@RequestBody,此時方法就不存在解析@RequestBody的HandlerMethodArgumentResolver,是以就隻剩我們自定義的HandlerMethodArgumentResolver必然會執行

2、方法二:提高我們自定義HandlerMethodArgumentResolver的執行順序

具體做法如下

@Configuration
public class HandlerMethodArgumentResolverAutoConfiguration implements InitializingBean{

    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
    

    @Override
    public void afterPropertiesSet() throws Exception {
        List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
        List<HandlerMethodArgumentResolver> customArgumentResolvers = new ArrayList<>();

        for (HandlerMethodArgumentResolver argumentResolver : argumentResolvers) {
            if(argumentResolver instanceof RequestResponseBodyMethodProcessor){
                 customArgumentResolvers.add(new MetaInfoHandlerMethodArgumentResolver(argumentResolver));
            }
            customArgumentResolvers.add(argumentResolver);
        }

        requestMappingHandlerAdapter.setArgumentResolvers(customArgumentResolvers);

    }
}
           

将自定義的HandlerMethodArgumentResolver放在解析@RequestBody的HandlerMethodArgumentResolver之前。調整後,我們測試一下

項目自定義HandlerMethodArgumentResolver不生效原因與解法

此時會發現已經有值填充進去了

總結

本文主要講解自定義HandlerMethodArgumentResolver不生效原因與解法,我們可以思考一個問題修改或者填充請求參數,除了利用HandlerMethodArgumentResolver之外,還有沒有其他實作方式?下篇文章揭曉答案

繼續閱讀