spring MVC是严格遵守java servlet规范的一种web框架,可以打包成war包交由web容器(Tomcat或者jetty)运行。接下来就来学习下spring mvc的运行过程以及其中的细节,如何和Tomcat无缝合作,如何和spring 本身的核心功能IOC、AOP合作。
MVC 实例
接下来就搭建一个基于maven的spring mvc的实例。
项目结构
StudentController 类
@Controller
@RequestMapping
public class StudentController {
@RequestMapping(value = "/h")
@ResponseBody
public String getStudentInfo() {
return "hello world";
}
}
web.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/applicationContext.xml</param-value>
</context-param>
<servlet>
<servlet-name>demo</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>demo</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.demo.web" />
</beans>
pom.xml
// 插件部分
<build>
<finalName>demo</finalName>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<uriEncoding>${file_encoding}</uriEncoding>
<port>9912</port>
<server>tomcat7</server>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.5</version>
</plugin>
<!-- Java Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>${java_source_version}</source>
<target>${java_target_version}</target>
<encoding>${file_encoding}</encoding>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
然后通过mvn tomcat7:run
就可以正常启动了
Tomcat 基础
在介绍spring mvc的工作原理之前,有必要介绍下web容器的一种Tomcat。Tomcat是一个开源的web容器,严格准守servlet规范,在Tomcat中包含了各种各样的组件,层层嵌套依赖,如下图所示。
catalina是最核心的组件,也可以认为Tomcat是从它开始启动的,他持有1个server容器,server容器可以包含了多个service容器,每个service容器都持有了一个connector连接器以及一个engine,engine有层层包含了host、context、wrapper等。
其中engine、host、context、wrapper又分别存在各自的一个管道pipeline以及至少一个阀门valve,阀门可以为request和response添加任何外置的功能。
Tomcat启动是由各自容器的监听器调用启动的,按照上面所说的顺序依次执行启动的。
那我们常用的servlet是在哪里的么?他是被wrapper包装的,每一个wrapper持有一个servlet,所以在xml中配置了几个servlet,则就会存在多少个wrapper。并且servlet是通过ServletContext传递上下文的。在具体的URL映射的时候,会先根据各自的servlet的URL配置在Tomcat的mappingdata中体现,经过host、context、再到选择不同类型的wrapper(包含了wildcardWrappers、extensionWrappers、defaultWrapper、exactWrappers四种wrapper类型)最后才具体到某一个servlet请求上。
DispatcherServlet init 过程
DispatcherServlet类是spring mvc中实现的了servlet规范的实体类,实现了HttpServlet类,如下图
首先需要明确一点的是,DispatcherServlet也只是一个HttpServlet类,他是被wrapper调用的init(ServletConfig config)方法进入的,设置好config之后,进入到init()方法。
PS:在当前环境中,ServletConfig是StandardWrapperFacade类,可以从中获取到在xml配置的例如contextConfigLocation数据
HttpServletBean 类
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
// 获取xml配置的属性,在下面贴的图片中可以看到,当DispatcherServlet没有任何配置的时候,就会抛出异常,说缺少必备的配置属性
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
// 这一步就是设置DispatcherServlet的属性值的操作
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// 开始真正的初始化DispatcherServlet类了
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}
FrameworkServlet 类
protected final void initServletBean() throws ServletException {
try {
this.webApplicationContext = initWebApplicationContext();
// 这个是初始化当前dispatchservlet的webApplicationContext参数
// 然后该参数中会持有spring的IOC容器等信息,通过这个参数可以获取bean数据
// 具体细节可看下面的代码
initFrameworkServlet();
// 未做任何事情,由子类实现,不过暂时为空
}
catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
}
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
// 从当前的上下文中查找是否存在跟上下文信息(通过ContextLoaderListener加载的都会存在的)
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
// 重新再刷新一次WebApplicationContext持有的内容信息
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = findWebApplicationContext();
}
if (wac == null) {
// 创建一个新的WebApplicationContext信息,并设置好其parent
// 这里需要好好看看,同样的拆分为两部分执行
// 1、创建WebApplicationContext类,也就是XmlWebApplicationContext实体类,并设置好其环境、配置属性、父类等信息
// 2、调用configureAndRefreshWebApplicationContext,同样的设置好其关于servlet的上下文的属性信息,最后调用wac.refresh()
// PS: refresh()就开始了spring IOC的解析存储操作了
wac = createWebApplicationContext(rootContext);
// 不过这里有一步需要注意到,在refresh()中,最后会有onApplicationEvent()的操作,他会调用在DispatcherServlet类的initStrategies方法,完成URL映射、Template等操作
// 并且设置this.refreshEventReceived为true
}
if (!this.refreshEventReceived) {
// 完成了上面的刷新操作就不要再刷新了
// 这里的onRefresh还是会调用initStrategies方法
// 殊途同归罢了
onRefresh(wac);
}
if (this.publishContext) {
// 把当前的上下文信息也保存到ServletContext中
// 如果注意到的函数开头的rootContext的获取方法会发现也是通过这样的方式获取的
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
这样就完成了DispatcherServlet的初始化操作了,接下来就可以具体处理http请求了,当前这其中遗漏了很多重要的的点,例如
- rootContext 这是怎么一回事,是必须的么,和applicationContext.xml又有什么关系呢?
- xml配置的context-param和servlet的init-param有什么区别?
这几个点会在后续的学习笔记中再了解其原理,当前主要是介绍DispatcherServlet以及相关的东西。
DispatcherServlet URL映射以及请求处理 过程
Tomcat会通过的DispatcherServlet的servlet-mapping的属性匹配到合适的wrapper,再关联到具体的DispatcherServlet,也就意味着在web.xml确实可以配置多个servlet,只是在spring mvc中常用的就这一个而已。
HTTP请求,最后都会打到DispatcherServlet类的doService方法中,设置一些属性之后,又来到了doDispatch方法中,这个方法是核心也是最重要的http请求处理的方法,通过URL选择合适的controller,选择具体的modelandview,再渲染生成数据回调等等操作
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
mappedHandler = getHandler(processedRequest);
// 通过URL找到合适的controller,并存储在HandlerExecutionChain对着中
// 还会检测是否进行cors跨域操作,如果存在跨域就会按照跨域的要求去处理
// @CrossOrigin(origins="http://test.com") 可以直接放在controller的注解上
if (mappedHandler == null || mappedHandler.getHandler() == null) {
// 没有找到合适的处理handle,就是404了
// 这就可以自定义配置404页面以及跳转等信息
// response.sendError(HttpServletResponse.SC_NOT_FOUND)
// 这里又可以引出一个问题了,如何配置404页面
noHandlerFound(processedRequest, response);
return;
}
// 检测当前获取的controller是否合适,并且得到合适的HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 如果符合可以使用缓存机制,减少不必要的请求
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
// 使用spring本身的拦截器前置处理
return;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 真正的处理请求,在本demo中会调到AnnotationMethodHandlerAdapter类中执行handle方法,返回的mv是ModelAndView
// 解析当前handler中包含了所有的方法,匹配其中合适的方法之后,invoke调用
// 不过这里需要注意到,类似于返回json的请求,是不需要模板渲染的,此时mv返回的是null,不过具体的json数据已经填入到了responseBody中
applyDefaultViewName(processedRequest, mv);
// 如果mv
mappedHandler.applyPostHandle(processedRequest, response, mv);
// spring 拦截器的后置处理
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 处理结果了,如果存在异常会把exception带上,例如500错误等,按照异常处理
// 如果存在了模板,需要经过render处理
// 否则就直接把得到的数据当做body返回
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
到这里就完成了对一个普通的http请求的处理全过程,不过还是存在诸多问题没有去分析,如图
- cors跨域是什么,如何使用
- URL映射规则是如何完成的,以及和Tomcat的URL映射有什么关联么?
- 模板是如何被渲染的,在xml中如何设置不同的模板的?
还有个问题一直疏忽了,在spring mvc中存在大量的if(***.debug)这种操作,那么该如何配置使用日志系统呢?
接下来会再写笔记去分析上述提到的各种问题~