对于使用过SpringMVC的开发人员来说,应该都有过这样的疑问——似乎不论在被@RequestMapping注解的方法参数中填入怎样的类型,SpringMVC总能非常智能地把相应的参数传入进来, 本文正是为了探究这类疑问。


0. 目录

  • 1. 概述
  • 2. 传入参数的处理
  • 3. 返回值的处理
  • 4. 总结
  • 5. 参考


1. 概述

本文主要关注SpringMVC是:

  1. 如何支持种类繁多的传入参数——例如HttpServletRequest类型,HttpServletResponse类型,Locale类型,HttpSession类型,被@RequestParam注解的参数等等。
  2. 如何支持传出各类参数——例如ModelAndViewModelViewResponseEntity(SpringMVC下载文件常用到它,其实另外一个相似的类型是StreamingResponseBody),Callable等等。

2. 传入参数的处理

通过观察请求处理时的堆栈:

springmvc 返回时间戳 springmvc怎么处理返回值_SpringMVC


我们最终可以定位到SpringMVC是将参数解析的工作委托给了HandlerMethodArgumentResolver接口的实现类,并且以组合模式HandlerMethodArgumentResolverComposite的形式将API暴露给外界。以下是HandlerMethodArgumentResolver接口的继承链:

springmvc 返回时间戳 springmvc怎么处理返回值_Spring_02


3. 返回值的处理

依然是观察请求处理时的堆栈:

springmvc 返回时间戳 springmvc怎么处理返回值_源码_03

我们发现SpringMVC依然没有将返回值处理的逻辑直接写死在主逻辑之中,而是采用委托的方式将返回值的处理工作全权委托给了接口 HandlerMethodReturnValueHandler的实现类,并且以组合模式HandlerMethodReturnValueHandlerComposite的形式将API暴露给外界。以下是HandlerMethodReturnValueHandler接口的继承链:

springmvc 返回时间戳 springmvc怎么处理返回值_springmvc 返回时间戳_04


我们来看看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]

以上,我们至少可以得出两个注意点:

  1. Spring4.0新增的 @RestController注解, 就是集合了@Controller@ResponseBody 的功能。正好对应了上面的RequestResponseBodyMethodProcessor类。
  2. 而且@ResponseBody注解的处理类RequestResponseBodyMethodProcessor优先于处理viewName的ViewNameMethodReturnValueHandler, 所以如果在Controller层面注解了@RestController,就不能简单地返回viewName了,此时需要返回ModelAndView实例。

最后让我们来看看关于HandlerMethodReturnValueHandler相关的一个应用:

以下是将SpringMVC推荐的JSON处理库Jackson替换为阿里的Fastjson的操作,可以看到其中的核心正是FastJsonHttpMessageConverter类。

<mvc:annotation-driven>
		<!-- 利用FastJson接口返回json数据相关配置; 取代默认的Jackson2 -->
		<!-- 关于原理,参见博客:  -->
		<!-- 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">
			<!--  下载的文件内容乱码 -->
			<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将职责进行了精细地划分,主逻辑只是负责流程的判断与扭转,然后在合适的时候回调相应职责的实现者即可,各个组件像积木一样优美地组装在一起,结构清晰,灵活,可扩展性强。