SpringMVC重点功能底层源码解析
课堂疑问1:
当我们使用@RequestParam,并且没有注册StringToUserEditor时,但是User中提供了一个String类型参数的构造方法时:
@RequestMapping(method = RequestMethod.GET, path = "/test")
@ResponseBody
public String test(@RequestParam("name") User user) {
return user.getName();
}
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
SpringMVC在进行把String转成User对象时,会先判断有没有User类型对应的StringToUserEditor,如果有就会利用它来把String转成User对象,如果没有则会找User类中有没有String类型参数的构造方法,如果有则用该构造方法来构造出User对象。
对应的源码方法为:org.springframework.beans.TypeConverterDelegate#convertIfNecessary(java.lang.String, java.lang.Object, java.lang.Object, java.lang.Class, org.springframework.core.convert.TypeDescriptor)
课堂疑问2:
如果方法返回的是byte[]:
@RequestMapping(method = RequestMethod.GET, path = "/test")
@ResponseBody
public byte[] test() {
byte[] bytes = new byte[1024];
return bytes;
}
这种情况会直接使用ByteArrayHttpMessageConverter来处理,会直接把byte[]写入响应中:
public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<byte[]> {
/**
* Create a new instance of the {@code ByteArrayHttpMessageConverter}.
*/
public ByteArrayHttpMessageConverter() {
super(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL);
}
@Override
public boolean supports(Class<?> clazz) {
return byte[].class == clazz;
}
// ...
@Override
protected void writeInternal(byte[] bytes, HttpOutputMessage outputMessage) throws IOException {
StreamUtils.copy(bytes, outputMessage.getBody());
}
}
SpringMVC父子容器
我们可以在web.xml文件中这么来定义:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
在这个web.xml文件中,我们定义了一个listener和servlet。
父容器的创建
ContextLoaderListener的作用是用来创建一个Spring容器,就是我们说的SpringMVC父子容器中的父容器,执行流程为:
- Tomcat启动,解析web.xml时
- 发现定义了一个ContextLoaderListener,Tomcat就会执行该listener中的contextInitialized()方法,该方法就会去创建要给Spring容器
- 从ServletContext中获取contextClass参数值,该参数表示所要创建的Spring容器的类型,可以在web.xml中通过来进行配置
- 如果没有配置该参数,那么则会从ContextLoader.properties文件中读取org.springframework.web.context.WebApplicationContext配置项的值,SpringMVC默认提供了一个ContextLoader.properties文件,内容为org.springframework.web.context.support.XmlWebApplicationContext
- 所以XmlWebApplicationContext就是要创建的Spring容器类型
- 确定好类型后,就用反射调用无参构造方法创建出来一个XmlWebApplicationContext对象
- 然后继续从ServletContext中获取contextConfigLocation参数的值,也就是一个spring配置文件的路径
- 把spring配置文件路径设置给Spring容器,然后调用refresh(),从而启动Spring容器,从而解析spring配置文件,从而扫描生成Bean对象等
- 这样Spring容器就创建出来了
- 有了Spring容器后,就会把XmlWebApplicationContext对象作为attribute设置到ServletContext中去,key为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
- 把Spring容器存到ServletContext中的原因,是为了给Servlet创建出来的子容器来作为父容器的
子容器的创建
Tomcat启动过程中,执行完ContextLoaderListener的contextInitialized()之后,就会创建DispatcherServlet了,web.xml中定义DispatcherServlet时,load-on-startup为1,表示在Tomcat启动过程中要把这个DispatcherServlet创建并初始化出来,而这个过程是比较费时间的,所以要把load-on-startup设置为1,如果不为1,会在servlet接收到请求时才来创建和初始化,这样会导致请求处理比较慢。
- Tomcat启动,解析web.xml时
- 创建DispatcherServlet对象
- 调用DispatcherServlet的init()
- 从而调用initServletBean()
- 从而调用initWebApplicationContext(),这个方法也会去创建一个Spring容器(就是子容器)
- initWebApplicationContext()执行过程中,会先从ServletContext拿出ContextLoaderListener所创建的Spring容器(父容器),记为rootContext
- 然后读取contextClass参数值,可以在servlet中的标签来定义想要创建的Spring容器类型,默认为XmlWebApplicationContext
- 然后创建一个Spring容器对象,也就是子容器
- 将rootContext作为parent设置给子容器(父子关系的绑定)
- 然后读取contextConfigLocation参数值,得到所配置的Spring配置文件路径
- 然后就是调用Spring容器的refresh()方法
- 从而完成了子容器的创建
SpringMVC初始化
子容器创建完后,还会调用一个DispatcherServlet的onRefresh()方法,这个方法会从Spring容器中获取一些特殊类型的Bean对象,并设置给DispatcherServlet对象中对应的属性,比如HandlerMapping、HandlerAdapter。
流程为:
- 会先从Spring容器中获取HandlerMapping类型的Bean对象,如果不为空,那么就获取出来的Bean对象赋值给DispatcherServlet的handlerMappings属性
- 如果没有获取到,则会从DispatcherServlet.properties文件中读取配置,从而得到SpringMVC默认给我们配置的HandlerMapping
DispatcherServlet.properties文件内容为:
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
org.springframework.web.ser