ModelAndViewResolverMethodReturnValueHandler
这个就很厉害了,它是Spring MVC
交给我们自定义返回值处理器的一个非常重要的渠道。从官方的javadoc里也能看出来:
* This return value handler is intended to be ordered after all others as it * attempts to handle _any_ return value type (i.e. returns {@code true} for * all return types).
简单的说它是放在所有的其它的处理器最后一位的,所以它的supportsReturnType()是永远return true。 但它默认并没有给我们配置进来(而是我们根据需要自己选装~),装配的源码如下:
... // Catch-all if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) { handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers())); } else { handlers.add(new ModelAttributeMethodProcessor(true)); } ...
由此可见默认情况下它添加进来的是ModelAttributeMethodProcessor,但凡你RequestMappingHandlerAdapter#setModelAndViewResolvers()自己往里set了个ModelAndViewResolver,它就会被添加,进而让ModelAndViewResolver生效~。
ModelAndViewResolver它是一个接口,Spring并没有默认的实现类。Spring对它的定位很清楚:SPI for resolving custom return values from a specific handler method,它就是给我们自己来自定义处理返回值的一个处理器。通常用于检测特殊的返回类型,解析它们的已知结果值,下面我们自己玩一把试试~~~
public class MyModelAndViewResolver implements ModelAndViewResolver { @Override public ModelAndView resolveModelAndView(Method handlerMethod, Class<?> handlerType, Object returnValue, ExtendedModelMap implicitModel, NativeWebRequest webRequest) { System.out.println("...MyModelAndViewResolver..."); if (returnValue instanceof Person) { ModelAndView modelAndView = new ModelAndView(); Person person = (Person) returnValue; // 把属性值放进Model里 implicitModel.addAttribute("name", person.name).addAttribute("age", person.age); modelAndView.setViewName("person"); modelAndView.setStatus(HttpStatus.CREATED); //返回201的状态码 return modelAndView; } else { return UNRESOLVED; } } }
读源码发现我们重点就是要在RequestMappingHandlerAdapter这个Bean初始化,也就是执行afterPropertiesSet()方法的时候把ModelAndViewResolver给放进去,这样子就会生效了。
通读之后,我们发现WebMvcConfigurationSupport它的createRequestMappingHandlerAdapter()方法是受保护的。因此我们可以通过重新注册一个它来达到效果:
至于扩展Spring MVC采用WebMvcConfigurer接口还是继承WebMvcConfigurationSupport,建议参见:
WebMvcConfigurationSupport与WebMvcConfigurer的关系
因此我们只需要这么来定义即可:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { // 通过继承WebMvcConfigurationSupport 的方式去覆盖,前提是你对原理比较熟悉~ @Configuration public static class MyWebMvcConfigurationSupport extends WebMvcConfigurationSupport { @Override protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() { RequestMappingHandlerAdapter requestMappingHandlerAdapter = super.createRequestMappingHandlerAdapter(); requestMappingHandlerAdapter.setModelAndViewResolvers(Arrays.asList(new MyModelAndViewResolver())); return requestMappingHandlerAdapter; } } }
这样我们controller返回值类型如下
@RequestMapping(value = "/hello", method = RequestMethod.GET) public Person helloGet() { Person person = new Person(); person.name = "fsx"; person.age = 18; return person; }
本来我们是不能够解析Person类型的,现在我们也能够正常解析了~~~~~ 这就是Spring MVC留给我们处理自定义类型的一个钩子,可以这么来用~~~
备注:好几个小伙伴问这个核心原理是什么,其实核心原理就是Bean定义的覆盖,希望可以举一反三,它是扩展Spring的一个较为常用的方式~
备注:ModelAndViewResolver从setModelAndViewResolvers()的javadoc里可以看出,它一般用于来做向下兼容。如果你要自定义,一般需要重写HandlerMethodReturnValueHandler和ModelAndViewResolver
ModelAndViewResolverMethodReturnValueHandler它的解释如下:
public class ModelAndViewResolverMethodReturnValueHandler implements HandlerMethodReturnValueHandler { @Nullable private final List<ModelAndViewResolver> mavResolvers; // 持有modelAttributeProcessor 的引用,所以是对它的一个加强~~~~ private final ModelAttributeMethodProcessor modelAttributeProcessor = new ModelAttributeMethodProcessor(true); public ModelAndViewResolverMethodReturnValueHandler(@Nullable List<ModelAndViewResolver> mavResolvers) { this.mavResolvers = mavResolvers; } @Override public boolean supportsReturnType(MethodParameter returnType) { return true; } @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { // 若我们配置了处理器,那就一个一个的处理吧~~~~~ // 当然,最终真正处理的可能只有一个,这里也是责任链的形式~~~~一般会用if判断 if (this.mavResolvers != null) { for (ModelAndViewResolver mavResolver : this.mavResolvers) { Class<?> handlerType = returnType.getContainingClass(); Method method = returnType.getMethod(); Assert.state(method != null, "No handler method"); ExtendedModelMap model = (ExtendedModelMap) mavContainer.getModel(); // 处理ModelAndView,若返回的不是ModelAndViewResolver.UNRESOLVED // 那就说明它处理了,那就return掉~~~~ 逻辑还是很简单的~~~ ModelAndView mav = mavResolver.resolveModelAndView(method, handlerType, returnValue, model, webRequest); // 这一步相当于如果我们自定义了model,会把它的属性合并进来~~~ // 大多数情况下,我们外部直接操作ExtendedModelMap model这个对象即可 // 当然你也可以不指定view,自己写成同@ResponseBody一样的效果也是阔仪的 if (mav != ModelAndViewResolver.UNRESOLVED) { mavContainer.addAllAttributes(mav.getModel()); mavContainer.setViewName(mav.getViewName()); if (!mav.isReference()) { mavContainer.setView(mav.getView()); } return; } } } // No suitable ModelAndViewResolver... if (this.modelAttributeProcessor.supportsReturnType(returnType)) { this.modelAttributeProcessor.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } else { throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); } } }
ModelAttributeMethodProcessor
它是一个Processor
,既处理入参封装,也处理返回值,本文只处理返回值部分。
@ModelAttribute
能标注在入参处来处理入参,能标在方法处来处理方法返回值。源码部分也只展示处理返回值部分:
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { // 默认值是false // true: private final boolean annotationNotRequired; public ModelAttributeMethodProcessor(boolean annotationNotRequired) { this.annotationNotRequired = annotationNotRequired; } // 方法上标注有@ModelAttribute这个注解 // 或者annotationNotRequired为true并且是简单类型isSimpleProperty() = true // 简单类型释义:8大基本类型+包装类型+Enum+CharSequence+Number+Date+URI+Local+Class // 数组类型并且是简单的数组(上面那些类型的数组类型)类型 也算作简单类型 @Override public boolean supportsReturnType(MethodParameter returnType) { return (returnType.hasMethodAnnotation(ModelAttribute.class) || (this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType()))); } // 做法相当简单,就是吧返回值放在model里面~~~~ @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue != null) { // 这个方法:@ModelAttribute指明了name,那就以它的为准 // 否则就是一个复杂的获取过程:string... String name = ModelFactory.getNameForReturnValue(returnValue, returnType); mavContainer.addAttribute(name, returnValue); } } }
这个时候因为没有指定viewName,so~~~ 不解释
ServletModelAttributeMethodProcessor
它继承自ModelAttributeMethodProcessor。主要是对入参数据绑定方面做了一些方法的复写,支持到了Servlet等,它主要是对入参做了更多支持,因此本文先不做讨论。
Spring MVC内部实际应用中,ServletModelAttributeMethodProcessor仅仅被用于getDefaultArgumentResolvers()方法内。而ModelAttributeMethodProcessor都用于getDefaultReturnValueHandlers()内。当然它还用于ExceptionHandlerExceptionResolver~~~
AbstractMessageConverterMethodProcessor
一看它以Processor命名结尾,所以它既能处理入参,又能处理返回值。因此一样的,本文只关注返回值处理部分代码:
因为它都没有对supportsReturnType和handleReturnValue进行实现,此抽象类暂时飘过~~~
其实它有一个非常重要的方法:writeWithMessageConverters(),下面详述
RequestResponseBodyMethodProcessor
它继承自AbstractMessageConverterMethodProcessor。从名字或许就能看出来,这个处理器及其重要,因为它处理着我们最为重要的一个注解@ResponseBody(其实它还处理@RequestBody,只是我们这部分不讲请求参数~~~) 并且它在读、写的时候和HttpMessageConverter还有深度结合~~
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { // 显然可以发现,方法上或者类上标注有@ResponseBody都是可以的~~~~ // 这也就是为什么现在@RestController可以代替我们的的@Controller + @ResponseBody生效了 @Override public boolean supportsReturnType(MethodParameter returnType) { return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class)); } @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { // 首先就标记:此请求已经被处理了~~~ mavContainer.setRequestHandled(true); ServletServerHttpRequest inputMessage = createInputMessage(webRequest); ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); // Try even with null return value. ResponseBodyAdvice could get involved. // 这个方法是核心,也会处理null值~~~ 这里面一些Advice会生效~~~~ // 会选择到合适的HttpMessageConverter,然后进行消息转换~~~~(这里只指写~~~) 这个方法在父类上,是非常核心关键自然也是非常复杂的~~~ writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); } }
HttpEntityMethodProcessor
显然它是处理返回值为HttpEntity
类型的。
public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor { // 看这个判断。绝大多数情况下我们使用的返回值是ResponseEntity<T> // 当然你也可以直接使用HttpEntity<T> 作为返回值也是可以的 @Override public boolean supportsReturnType(MethodParameter returnType) { return (HttpEntity.class.isAssignableFrom(returnType.getParameterType()) && !RequestEntity.class.isAssignableFrom(returnType.getParameterType())); } // 它的handleReturnValue方法就不详细说了,在RequestResponseBodyMethodProcessor的基础上主要做了如下增强: // 1、对请求中有Vary的进行特殊处理 // 2、Http状态码是200的话。如果是get请求或者Head请求,并且内容没有改变isResourceNotModified 那就直接outputMessage.flush() 然后return掉 // 3、做Http状态码是3打头(returnStatus / 100 == 3),如果有location的key,就特殊处理 // 最终,最终。正常情况下:依然同上调用父类writeWithMessageConverters()方法~~~ }
显然,若我们想自己设置管理Http状态码,可以使用ResponseEntity。但显然绝大多数情况下,我们使用@ResponseBody更加的便捷~~~~
因为这块特别重要,所以这里逃不开的要深入了解。毕竟它还和非常重要消息转换器也有非常重要的联系。所以要对父类方法writeWithMessageConverters()进行深入的解释:
你会发现其它的返回值处理器都是不会调用消息转换器的,而只有AbstractMessageConverterMethodProcessor它的两个子类才会这么做。而刚巧,这种方式(@ResponseBody方式)是我们当下最为流行的处理方式,因此非常有必要进行深入的了解~~~
AbstractMessageConverterMethodProcessor#writeWithMessageConverters详解
为了方便讲解,此处我们采用解析此处理器结合讲解:
@ResponseBody @RequestMapping(value = "/hello", method = RequestMethod.GET) public Person helloGet() { Person person = new Person(); person.name = "fsx"; person.age = 18; return person; }
很显然它标注了@ResponseBody,所以最终会调用ResponseBodyEmitterReturnValueHandler进行转换、解析~~~~
// @since 3.1 会发现它也处理请求,但是不是本文讨论的重点 //return values by writing to the response with {@link HttpMessageConverter HttpMessageConverters} public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... // 此处我们只关注它处理返回值的和信访方法 // Writes the given return type to the given output message // 从JavaDoc解释可以看出,它的作用很“单一“:就是把返回值写进output message~~~ @SuppressWarnings({"rawtypes", "unchecked"}) protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class<?> valueType; Type targetType; // 注意此处的特殊处理,相当于把所有的CharSequence类型的,都最终当作String类型处理的~ if (value instanceof CharSequence) { body = value.toString(); valueType = String.class; targetType = String.class; } // 我们本例;body为返回值对象 Person@5229 // valueType为:class com.fsx.bean.Person // targetType:class com.fsx.bean.Person else { body = value; valueType = getReturnValueType(body, returnType); // 此处相当于兼容了泛型类型的处理 targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); } // 若返回值是个org.springframework.core.io.Resource 就走这里 此处忽略~~ if (isResourceType(value, returnType)) { outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes"); if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null && outputMessage.getServletResponse().getStatus() == 200) { Resource resource = (Resource) value; try { List<HttpRange> httpRanges = inputMessage.getHeaders().getRange(); outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value()); body = HttpRange.toResourceRegions(httpRanges, resource); valueType = body.getClass(); targetType = RESOURCE_REGION_LIST_TYPE; } catch (IllegalArgumentException ex) { outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength()); outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value()); } } } // selectedMediaType表示最终被选中的MediaType,毕竟请求放可能是接受N多种MediaType的~~~ MediaType selectedMediaType = null; // 一般情况下 请求方很少会指定contentType的~~~ // 如果请求方法指定了,就以它的为准,就相当于selectedMediaType 里面就被找打了 // 否则就靠系统自己去寻找到一个最为合适的~~~ MediaType contentType = outputMessage.getHeaders().getContentType(); if (contentType != null && contentType.isConcrete()) { if (logger.isDebugEnabled()) { logger.debug("Found 'Content-Type:" + contentType + "' in response"); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); // 前面我们说了 若是谷歌浏览器 默认它的accept为:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/s // 所以此处数组解析出来有7对 List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); // 这个方法就是从所有已经注册的转换器里面去找,看看哪些转换器.canWrite,然后把他们所支持的MediaType都加入进来~~~ // 比如此例只能匹配到MappingJackson2HttpMessageConverter,所以匹配上的有application/json、application/*+json两个 List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); // 这个异常应该我们经常碰到:有body体,但是并没有能够支持的转换器,就是这额原因~~~ if (body != null && producibleTypes.isEmpty()) { throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType); } // 下面相当于从浏览器可议接受的MediaType里面,最终抉择出N个来 // 原理也非常简单:你能接受的isCompatibleWith上了我能处理的,那咱们就好说,处理就完了 List<MediaType> mediaTypesToUse = new ArrayList<>(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { // 从两个中选择一个最匹配的 主要是根据q值来比较 排序 // 比如此例,最终匹配上的有两个:application/json;q=0.8和application/*+json;q=0.8 mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } } // 这个异常也不少见,比如此处如果没有导入Jackson相关依赖包 // 就会抛出这个异常了:HttpMediaTypeNotAcceptableException:Could not find acceptable representation if (mediaTypesToUse.isEmpty()) { if (body != null) { throw new HttpMediaTypeNotAcceptableException(producibleTypes); } if (logger.isDebugEnabled()) { logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes); } return; } // 根据Q值进行排序: MediaType.sortBySpecificityAndQuality(mediaTypesToUse); // 因为已经排过 for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } if (logger.isDebugEnabled()) { logger.debug("Using '" + selectedMediaType + "', given " + acceptableTypes + " and supported " + producibleTypes); } } // 最终的最终 都会找到一个决定write的类型,必粗此处为:application/json;q=0.8 // 因为最终会决策出来一个MediaType,所以此处就是要根据此MediaType找到一个合适的消息转换器,把body向outputstream写进去~~~ // 注意此处:是RequestResponseBodyAdviceChain执行之处~~~~ if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> converter : this.messageConverters) { // 从这个判断可以看出 ,处理body里面内容,GenericHttpMessageConverter类型的转换器是优先级更高,优先去处理的 GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { // 在写body之前执行~~~~ 会调用我们注册的所有的合适的ResponseBodyAdvice#beforeBodyWrite方法 // 相当于在写之前,我们可以介入对body体进行处理 body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); // 给响应Response设置一个Content-Disposition的请求头(若需要的话) 若之前已经设置过了,此处将什么都不做 // 比如我们常见的:response.setHeader("Content-Disposition", "attachment; filename=" + java.net.URLEncoder.encode(fileName, "UTF-8")); //Content-disposition 是 MIME 协议的扩展,MIME 协议指示 MIME 用户代理如何显示附加的文件。 // 当 Internet Explorer 接收到头时,它会激活文件下载对话框,它的文件名框自动填充了头中指定的文件名 addContentDispositionHeader(inputMessage, outputMessage); if (genericConverter != null) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } // 如果return null,body里面是null 那就啥都不写,输出一个debug日志即可~~~~ else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body"); } } // 这一句表示:只要一个一个消息转换器处理了,就立马停止~~~~ return; } } } if (body != null) { throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); } } ... }
从上分析可以看出,这里面也提供了ResponseBodyAdvice钩子,我们可以通过实现此接口,来对接口的返回值进行干预、修改。相关注解为:@ControllerAdvice、@RestControllerAdvice
比如我下面这个可以让所有的@ResponseBody的处理器都返回固定值"hello,world":
@ControllerAdvice public class MyResponseBodyAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { return "hello,world"; } }
这样,访问任何这种rest请求,返回的都是:
这里面还是有个问题的:我们发现返回的hello world没错,,但是却都是带双引号的,显然这不是我想要的呀。怎么回事?怎么办呢?
原因,其实了解了上面的原理就能知道了。因为执行我们的MyResponseBodyAdvice#beforeBodyWrite此时候消息转换器已经选好了:MappingJackson2HttpMessageConverter
它最后调用writer方法其实底层其实就是调用objectMapper.writeValueAsString()进行写入,而为何会有双引号,看下面这个ObjectMapper的例子就一目了然了:
public static void main(String[] args) throws JsonProcessingException { ObjectMapper objectMapper = new ObjectMapper(); System.out.println(objectMapper.writeValueAsString("hello world")); // "hello world" 两边有分号 Person person = new Person(); person.name = "fsx"; person.age = 18; System.out.println(objectMapper.writeValueAsString(person)); // {"name":"fsx","age":18} 非常正规的json数据 }
解决办法:参考StringHttpMessageConverter写字符串的方法,然后自己进一步替换默认操作~~(自定义消息转换器)