Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported

記一次Java Bug的解決過程

Q:Bug描述

前端form表單數據提交時,後端出現Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported]這樣的提示,也沒有觸發Controller的響應方法。前端

A: 第一種解決辦法

ajax請求的時候,把Content-Type,設置爲application/json; charset=utf-8java

A: 第二種解決辦法

自定義參數解析器:web

  1. 首先定義註解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface P {
    // ...
}
複製代碼
  1. 自定義參數解析器的實現
public class Form2PojoArgumentResolver implements HandlerMethodArgumentResolver
{
	@Override
	public boolean supportsParameter(MethodParameter parameter)
	{
		return parameter.hasParameterAnnotation(P.class);
	}

	@Nullable
	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception
	{
        // 返回對應的參數類型的數據    
	}
}
複製代碼
  1. 配置參數解析器
@Configuration
public class ArgumentResolversConfig implements WebMvcConfigurer
{
    @Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)
	{
		resolvers.add(new Form2PojoArgumentResolver());
	}
}
複製代碼
  1. 相應的控制器的方法中
public class TestController 
{
    @RequestMapping("xxx")
    public testArgumentResolver(@P TestEntity entity) 
    {
        // ...    
    }
}
複製代碼

一般狀況下,前端form表單的數據到Controller中時就轉換成的對象。可是我這裏出現了特殊情形。 因爲個人TestEntity實現了Map接口,而Spring Boot在啓動時會自動加載一系列的解析器,而這些解析其中有一個叫MapMethodProcessor的處理器,它的supportsParameter方法是這樣實現的:ajax

@Override
public boolean supportsParameter(MethodParameter parameter) {
	return Map.class.isAssignableFrom(parameter.getParameterType());
}
複製代碼

這就致使了,在調用獲取方法參數的方法時(即調用InvocableHandlerMethod.java中的getMethodArgumentValues時)直接使用了MapMethodProcessor參數解析器(處理器),而自定義的參數解析器並無被調用。spring

翻了一下HandlerMethodArgumentResolverComposite.java的源碼,參數解析器的選取是順序遍歷的,源碼以下:json

/**
	 * Find a registered {@link HandlerMethodArgumentResolver} that supports the given method parameter.
	 */
	@Nullable
	private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {
	        // 遍歷參數解析器,尋找符合要求的
			for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
				if (logger.isTraceEnabled()) {
					logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
							parameter.getGenericParameterType() + "]");
				}
				if (methodArgumentResolver.supportsParameter(parameter)) {
					result = methodArgumentResolver;
					this.argumentResolverCache.put(parameter, result);
					break;
				}
			}
		}
		return result;
	}
複製代碼

又翻了一下RequestMappingHandlerAdapter.java的源碼,參數解析器的加載順序是這個樣子的:後端

/**
	 * Return the list of argument resolvers to use including built-in resolvers
	 * and custom resolvers provided via {@link #setCustomArgumentResolvers}.
	 */
	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;
	}
複製代碼

好吧,怪不得輪不到自定義的參數解析器來處理。既然如此,那我改變一下自定義參數解析器的加載順序吧————渣渣我要騎到大家頭上去!bash

@Configuration
public class ArgumentResolversConfig
{
	@Autowired
	private RequestMappingHandlerAdapter adapter;

	@PostConstruct
	public void injectSelfMethodArgumentResolver() 
	{
		List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
		argumentResolvers.add(new Form2PojoArgumentResolver());
		argumentResolvers.addAll(adapter.getArgumentResolvers());
		adapter.setArgumentResolvers(argumentResolvers);
	}
}
複製代碼

好了,如此,完美的解決了上述問題。app

這個Bug就聊到這兒吧。 ide

相關文章
相關標籤/搜索