对于使用过SpringMVC的开发人员来说,应该都有过这样的疑问——似乎不论在被
@RequestMapping
注解的方法参数中填入怎样的类型,SpringMVC总能非常智能地把相应的参数传入进来, 本文正是为了探究这类疑问。
1. 概述
本文主要关注SpringMVC是:
- 如何支持种类繁多的传入参数——例如
HttpServletRequest
类型,HttpServletResponse
类型,Locale
类型,HttpSession
类型,被@RequestParam
注解的参数等等。 - 如何支持传出各类参数——例如
ModelAndView
,Model
,View
,ResponseEntity
(SpringMVC下载文件常用到它,其实另外一个相似的类型是StreamingResponseBody
),Callable
等等。
2. 传入参数的处理
通过观察请求处理时的堆栈:
我们最终可以定位到SpringMVC是将参数解析的工作委托给了HandlerMethodArgumentResolver
接口的实现类,并且以组合模式HandlerMethodArgumentResolverComposite
的形式将API暴露给外界。以下是HandlerMethodArgumentResolver
接口的继承链:
更多细节请参见笔者之前的博客——SpringMVC源码研究之注解mvc:argument-resolvers
3. 返回值的处理
依然是观察请求处理时的堆栈:
我们发现SpringMVC依然没有将返回值处理的逻辑直接写死在主逻辑之中,而是采用委托的方式将返回值的处理工作全权委托给了接口 HandlerMethodReturnValueHandler
的实现类,并且以组合模式HandlerMethodReturnValueHandlerComposite
的形式将API暴露给外界。以下是HandlerMethodReturnValueHandler
接口的继承链:
我们来看看HandlerMethodReturnValueHandler
接口的定义
public interface HandlerMethodReturnValueHandler {
// 非常经典的控制权反转,子类通过实现这个接口来告知调度者自身是否支持这种类型的处理
// 在spring中可以经常看到类似的处理方式,眼前的另外一个例子就是上面的HandlerMethodArgumentResolver接口的supportsParameter()方法
boolean supportsReturnType(MethodParameter returnType);
// 这个就不必多说, 核心逻辑肯定是在这里面实现
void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
看完了定义,我们再来看看SpringMVC默认会载入哪些HandlerMethodReturnValueHandler
实现类:
// HandlerMethodReturnValueHandlerComposite中的字段returnValueHandlers中存储了如下实现类(4.3.18版本)
[org.springframework.web.servlet.mvc.method.annotation.ModelAndViewMethodReturnValueHandler@27ad3814,
org.springframework.web.method.annotation.ModelMethodProcessor@4fce62fd,
org.springframework.web.servlet.mvc.method.annotation.ViewMethodReturnValueHandler@44d0ba16,
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler@bf5187,
org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler@4e1ca41c,
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor@7279eea8,
org.springframework.web.servlet.mvc.method.annotation.HttpHeadersReturnValueHandler@33b8eafb,
org.springframework.web.servlet.mvc.method.annotation.CallableMethodReturnValueHandler@4971a662, // 返回类型支持Callable
org.springframework.web.servlet.mvc.method.annotation.DeferredResultMethodReturnValueHandler@6266d714,
org.springframework.web.servlet.mvc.method.annotation.AsyncTaskMethodReturnValueHandler@5fd7795e,
org.springframework.web.method.annotation.ModelAttributeMethodProcessor@2b4cbf6d,
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor@6fca5ed0,
org.springframework.web.servlet.mvc.method.annotation.ViewNameMethodReturnValueHandler@3b40fa4a,
org.springframework.web.method.annotation.MapMethodProcessor@5f3969f5,
org.springframework.web.method.annotation.ModelAttributeMethodProcessor@114002a8]
以上,我们至少可以得出两个注意点:
- Spring4.0新增的
@RestController
注解, 就是集合了@Controller
,@ResponseBody
的功能。正好对应了上面的RequestResponseBodyMethodProcessor
类。 - 而且
@ResponseBody
注解的处理类RequestResponseBodyMethodProcessor
优先于处理viewName的ViewNameMethodReturnValueHandler
, 所以如果在Controller层面注解了@RestController
,就不能简单地返回viewName了,此时需要返回ModelAndView
实例。
最后让我们来看看关于HandlerMethodReturnValueHandler
相关的一个应用:
以下是将SpringMVC推荐的JSON处理库Jackson替换为阿里的Fastjson的操作,可以看到其中的核心正是FastJsonHttpMessageConverter
类。
<mvc:annotation-driven>
<!-- 利用FastJson接口返回json数据相关配置; 取代默认的Jackson2 -->
<!-- 关于原理,参见博客: http://blog.csdn.net/lqzkcx3/article/details/78584358 -->
<!-- offcie site: https://github.com/alibaba/fastjson/wiki/%E5%9C%A8-Spring-%E4%B8%AD%E9%9B%86%E6%88%90-Fastjson -->
<mvc:message-converters register-defaults="true">
<!-- http://blog.csdn.net/kernel_32/article/details/50792139 下载的文件内容乱码 -->
<bean
class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
<!-- 配置Fastjson支持 -->
<bean
class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>application/json</value>
</list>
</property>
<property name="features">
<!--
关于features属性,不是serializerFeature,而是features,详见 FastJsonHttpMessageConverter.java
它是用来控制json序列化输出时的一些额外属性,比如说该字段是否输出、输出时key使用单引号还是双引号、key不使用任何引号等等
QuoteFieldNames 输出key时是否使用双引号,默认为true
WriteMapNullValue 是否输出值为null的字段,默认为false
WriteNullNumberAsZero 数值字段如果为null,输出为0,而非null
WriteNullListAsEmpty List字段如果为null,输出为[],而非null
WriteNullStringAsEmpty 字符类型字段如果为null,输出为"",而非null
WriteNullBooleanAsFalse Boolean字段如果为null,输出为false,而非null
6)通常在网上搜到的fastjson和springMVC整合的例子中都像下面注释的代码那样给了两个属性WriteMapNullValue和QuoteFieldNames
这就表示为json解析器设置QuoteFieldNames和WriteMapNullValue的值为true,即输出时key使用双引号,同时也输出值为null的字段
7)输出时某字段为String类型,且值为null,此时若需要其输出,且输出值为空字符串,则需同时赋值WriteMapNullValue和WriteNullStringAsEmpty
经测试,若只赋值WriteNullStringAsEmpty,则不会输出该字段..加上WriteMapNullValue属性后,便输出了,且输出值不是null,而是预期的空字符串
-->
<list>
<value>WriteMapNullValue</value>
<value>QuoteFieldNames</value>
<value>DisableCircularReferenceDetect</value>
</list>
</property>
</bean>
</mvc:message-converters>
而我们注册的FastJSON转换器,最终是在RequestResponseBodyMethodProcessor
类中选举出来的。确切来说是其基类AbstractMessageConverterMethodProcessor
中选举出来的,所以经过以上配置,只需要在方法上注解@ResponseBody
,最终返回的Bean实例将被Fastjson转换为相应的JSON字符串发送回前端。
而且查看RequestResponseBodyMethodProcessor
类实现的supportsReturnType
方法,可以不在每个方法上加ResponseBody
,仅在类级别上修饰@ResponseBody也是可以的。
4. 总结
由以上两个接口,我们可以感受到SpringMVC里精妙的设计,初学者经常犯的一个问题就是将所有的逻辑都当作主逻辑来处理,最终下来的结果是代码耦合度非常高,外界依赖严重,难以拆分;相比较之下SpringMVC将职责进行了精细地划分,主逻辑只是负责流程的判断与扭转,然后在合适的时候回调相应职责的实现者即可,各个组件像积木一样优美地组装在一起,结构清晰,灵活,可扩展性强。