MockMvc触发Hibernate Validator踩坑

39 篇文章 1 订阅
16 篇文章 0 订阅

我的写法

看起来没错啊,启动工程使用postman也没问题,但是一旦使用MockMvc进行验证,就遇到如下两个问题。

  1. application/json 被判定为unsupported mediaType。
  2. 请求无法触发@Valid的校验。
@RestController
@RequestMapping("test")
public class TestContrller {

	@PostMapping("say")
	public String say(@Valid @RequestBody Orange orange) {
		return "success";
	}
}


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestHibernateValidator.InnerConfig.class)
@WebAppConfiguration
public class TestHibernateValidator {

	MockMvc mockMvc;

	@Autowired
	WebApplicationContext wac;

	@Before
	public void build() {
		mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
	}

	@Test
	public void testSay() throws Exception {
		Orange orange = new Orange();
		orange.setName("123");
		mockMvc.perform(MockMvcRequestBuilders.post("/test/say").
				contentType(MediaType.APPLICATION_JSON).
				content(JSONObject.toJSONString(orange)).
				accept(MediaType.APPLICATION_JSON))
				.andDo(print());
	}

	@Configuration
	public static class InnerConfig {

		@Bean
		public TestContrller getTestController() {
			return new TestContrller();
		}

	}
}

Unsupported MediaType

为何会Unsupported MediaType

这是HttpMessgeConverter在搞鬼。请求必须通过httpMessageConverter的转义,才能被识别或封装成对象。

首先我们来到

RequestResponseBodyMethodProcessor ,它是负责处理@RequestBody、@ResponseBody 以及 @Valid或@Validated注解的类

并且从其父类,能看出它与HttpMessgeConverter相关。

看注解,处理@RequestBody @ResponseBody @Valid ,并在校验失败时,抛出MethodArgumentNotValidException。配置了某个resolver会返回400状态码。

/**
 * Resolves method arguments annotated with {@code @RequestBody} and handles
 * return values from methods annotated with {@code @ResponseBody} by reading
 * and writing to the body of the request or response with an
 * {@link HttpMessageConverter}.
 *
 * <p>An {@code @RequestBody} method argument is also validated if it is
 * annotated with {@code @javax.validation.Valid}. In case of validation
 * failure, {@link MethodArgumentNotValidException} is raised and results
 * in a 400 response status code if {@link DefaultHandlerExceptionResolver}
 * is configured.
 *
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
 * @since 3.1
 */
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor

跟踪其resolveArgument代码。这段代码

  • 既包含使用哪些converter读取HttpInputMessage
  • 又包含调用validate进行参数校验。
parameter = parameter.nestedIfOptional();
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		String name = Conventions.getVariableNameForParameter(parameter);

		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
				}
			}
			if (mavContainer != null) {
				mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
			}
		}

		return adaptArgumentIfNecessary(arg, parameter);

在使用mockMvc启动时,到readWithMessageConverters中一看,可以看见当前spring容器中有哪些converter。好家伙,竟然只有4个没啥用的converter,一般我们常用的

MappingJackson2HttpMessageConverter 

根本没在里面,这四个converter是默认converter,虽然有的application/json格式,但不支持处理目标为对象(@RequestBody中的Orange是对象,像StringHttpMessageConverter仅支持处理转string的),所以会报Unsupported Media Type。

在使用工程启动时是有MappingJackson2HttpMessageConverter 的。

为解决整个问题,请不要跳过每个章节。

Spring在何时注入HttpMessageConverter的

在WebMvcConfigurationSupport中,注入RequestMappingHandlerAdapter时注入的。其中

  1. configureMessageConverters        用于完全抛弃默认converters,全用自己的。
  2. addDefaultHttpMessgeConverters 用于注入默认converter。(MappingJackson2HttpMessageConverter就在这步注入
  3. extendMessageConverters             用于在默认converters的基础上增加自定义converter。
	@Bean
	public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
			@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
			@Qualifier("mvcConversionService") FormattingConversionService conversionService,
			@Qualifier("mvcValidator") Validator validator) {

		RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
		adapter.setContentNegotiationManager(contentNegotiationManager);
        就在这里
		adapter.setMessageConverters(getMessageConverters());
		adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
		adapter.setCustomArgumentResolvers(getArgumentResolvers());
		adapter.setCustomReturnValueHandlers(getReturnValueHandlers());

		if (jackson2Present) {
			adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
			adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
		}


protected final List<HttpMessageConverter<?>> getMessageConverters() {
		if (this.messageConverters == null) {
			this.messageConverters = new ArrayList<>();
			configureMessageConverters(this.messageConverters);
			if (this.messageConverters.isEmpty()) {
				addDefaultHttpMessageConverters(this.messageConverters);
			}
			extendMessageConverters(this.messageConverters);
		}
		return this.messageConverters;
	}

		AsyncSupportConfigurer configurer = getAsyncSupportConfigurer();
		if (configurer.getTaskExecutor() != null) {
			adapter.setTaskExecutor(configurer.getTaskExecutor());
		}
		if (configurer.getTimeout() != null) {
			adapter.setAsyncRequestTimeout(configurer.getTimeout());
		}
		adapter.setCallableInterceptors(configurer.getCallableInterceptors());
		adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());

		return adapter;
	}

protected final List<HttpMessageConverter<?>> getMessageConverters() {
		if (this.messageConverters == null) {
			this.messageConverters = new ArrayList<>();
			configureMessageConverters(this.messageConverters);
			if (this.messageConverters.isEmpty()) {
				addDefaultHttpMessageConverters(this.messageConverters);
			}
			extendMessageConverters(this.messageConverters);
		}
		return this.messageConverters;
	}

为何MockMvc启动没MappingJackson2HttpMessageConverter

因为MockMvc的RequestMappingHandlerAdapter不是由WebMvcConfigurationSupport承建的,而是由于没有其参与,dispatcherServlet找不到handlerAdapters,而自己脑补了一个默认的。

就是这个默认的handlerAdapter坏事儿了。

1. 往里跟 在DispatcherServlet中可以跟到initStrategies(),它是dispatcherSerlvet创建上下文的策略
protected void onRefresh(ApplicationContext context) {
		initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
                 2. 此处
		initHandlerAdapters(context); 
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
}

private void initHandlerAdapters(ApplicationContext context) {
		this.handlerAdapters = null;

		if (this.detectAllHandlerAdapters) {
			// Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
            3. 因为没有WebMvcConfigurationSupport参与,容器中没有handlerAdapters
			Map<String, HandlerAdapter> matchingBeans =
					BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
			if (!matchingBeans.isEmpty()) {
				this.handlerAdapters = new ArrayList<>(matchingBeans.values());
				// We keep HandlerAdapters in sorted order.
				AnnotationAwareOrderComparator.sort(this.handlerAdapters);
			}
		}
		else {
			try {
				HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
				this.handlerAdapters = Collections.singletonList(ha);
			}
			catch (NoSuchBeanDefinitionException ex) {
				// Ignore, we'll add a default HandlerAdapter later.
			}
		}

		// Ensure we have at least some HandlerAdapters, by registering
		// default HandlerAdapters if no other adapters are found.
		if (this.handlerAdapters == null) {
            4. 所以只能采用默认的HandlerAdapters生成策略
			this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
			if (logger.isTraceEnabled()) {
				logger.trace("No HandlerAdapters declared for servlet '" + getServletName() +
						"': using default strategies from DispatcherServlet.properties");
			}
		}
	}

解决方案

引入WebMvcConfigurationSupport

  • 使用@EnableWebMvc注解
  • 或配置类继承WebMvcConfigurationSupport
@EnableWebMvc
@Configuration
public static class InnerConfig{

		@Bean
		public TestContrller getTestController() {
			return new TestContrller();
		}

	}

或

@Configuration
public static class InnerConfig extends WebMvcConfigurationSupport{

		@Bean
		public TestContrller getTestController() {
			return new TestContrller();
		}

	}

为何Hibernate Validator会不生效

Hibernate Validator组件是何时注入Spring的

  1. spring有个御用的默认validator,OptionalValidatorFactoryBean
  2. 会在MvcConfigurationSupport,这里会注入。
  3. 这个validator会一路跟随到RequestMappingHandlerAdapter中。
  4. 然后在RequestResponseBodyMethodProcessor中发挥校验作用。(参照为何会Unsupported MediaType章节)
  5. OptionalValidatorFactoryBean会调用其父类LocalValidatorFactoryBean的afterPropertiesSet。
  6. 里面有个bootstrap.cofigure,里面会使用ServiceLoader加载hibernateValidator。

ServiceLoader,它会去找classPath底下,包括jar包内,寻找所提供接口的实现类。ServiceLoader<ValidationProvider> loader = ServiceLoader.load( ValidationProvider.class, classloader );

@Bean
public Validator mvcValidator() {
		Validator validator = getValidator();
		if (validator == null) {
			if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
				Class<?> clazz;
				try {
					String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
					clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
				}
				catch (ClassNotFoundException | LinkageError ex) {
					throw new BeanInitializationException("Failed to resolve default validator class", ex);
				}
				validator = (Validator) BeanUtils.instantiateClass(clazz);
			}
			else {
				validator = new NoOpValidator();
			}
		}
		return validator;
	}


@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
			@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
			@Qualifier("mvcConversionService") FormattingConversionService conversionService,
			@Qualifier("mvcValidator") Validator validator) {
...
具体省略
...
}


public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean {

	@Override
	public void afterPropertiesSet() {
		try {
			super.afterPropertiesSet();
		}
		catch (ValidationException ex) {
			LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
		}
	}

}

public class LocalValidatorFactoryBean extends SpringValidatorAdapter
		implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean {
     public void afterPropertiesSet() {
           ....
           configuration = bootstrap.configure();
           ....
     }
}    

继续往里面跟代码,可以跑到Validation类的GetValidationProviderListAction子类里的run方法,看里面的loadProviders。

loadProviders里就会使用ServiceLoader,它会去找classPath底下,包括jar包内,ValidationProvider接口的实现类,如果此时成功找到HibernateValidator,则成功将hibernate validator注入。

public List<ValidationProvider<?>> run() {
            ...

			List<ValidationProvider<?>> validationProviderList = loadProviders( classloader );          
            ...
		
		}


private List<ValidationProvider<?>> loadProviders(ClassLoader classloader) {
			ServiceLoader<ValidationProvider> loader = ServiceLoader.load( ValidationProvider.class, classloader );
			...
		}

 

相关文章

ResponseBodyAdvice和HttpMessageConverter应用浅析

待整理

请求时,报MediaType not supported,其实是HttpMessageConverter在捣鬼。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值