我的写法
看起来没错啊,启动工程使用postman也没问题,但是一旦使用MockMvc进行验证,就遇到如下两个问题。
- application/json 被判定为unsupported mediaType。
- 请求无法触发@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时注入的。其中
- configureMessageConverters 用于完全抛弃默认converters,全用自己的。
- addDefaultHttpMessgeConverters 用于注入默认converter。(MappingJackson2HttpMessageConverter就在这步注入)
- 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的
- spring有个御用的默认validator,OptionalValidatorFactoryBean。
- 会在MvcConfigurationSupport,这里会注入。
- 这个validator会一路跟随到RequestMappingHandlerAdapter中。
- 然后在RequestResponseBodyMethodProcessor中发挥校验作用。(参照为何会Unsupported MediaType章节)
- OptionalValidatorFactoryBean会调用其父类LocalValidatorFactoryBean的afterPropertiesSet。
- 里面有个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在捣鬼。