tomcat + spring mvc原理(七):spring mvc的Servlet和九大标准组件的静态结构与初始化
前言
前六讲基本已经将tomcat中比较重要的部分讲清楚了,如果有关键内容遗漏或者大家有哪些感兴趣的内容,可以留言给我,我会看情况和大家讨论一下或者发补充的文章。当然如果有错误的地方,也请大家留言给我,我会及时更正。
本文开始介绍spring mvc的基本原理。按照惯例,会先介绍tomcat中spring mvc的Servlet是如何加载的,然后到spring mvc中各种父Servlet的作用,最后是spring mvc中的标准Servlet实现DispatcherServlet和在DispatcherServlet中初始化和使用的各种组件的基本作用。
tomcat中Servlet的加载
原理(六)有讲到,部署最后,当context的实例设置完成后,会调用host的addChild(context)。接下来addChild方法内部调用context的start()方法,实际调用的是父类ContainerBase的start()方法,在这个方法中依次执行context的initInternal()方法和startInternal()方法。在Context的startInternal()中有很多代码逻辑,加载Servlet的代码在:
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
loadOnStartup(Container children[])入参是Context子容器的数组,即Wrapper的数组。
public boolean loadOnStartup(Container children[]) {
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
//获取加载顺序
for (int i = 0; i < children.length; i++) {
Wrapper wrapper = (Wrapper) children[i];
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0)
continue;
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}
//加载Servlet
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
} catch (ServletException e) {
getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
if(getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;
}
第一个循环获取所有Servlet的加载顺序值的大小,第二个根据获取的顺序值进行Wrapper加载。wrapper.load()中调用了Wrapper的loadServlet(),使用InstanceManager工具根据配置的servletName(如果是标准的servlet,这里servlet名就是org.springframework.web.servlet.DispatcherServlet)创建了一个实例,赋值给了Wrapper中持有的Servlet instance引用。
public synchronized Servlet loadServlet() throws ServletException {
......
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
......
servlet = (Servlet)instanceManager.newInstance(servletClass);
......
}
//在方法load中将loadServlet返回的Servlet实例赋值给instance
instance = loadServlet();
然后在load方法中调用了initServlet(instance)。
private synchronized void initServlet(Servlet servlet)
throws ServletException {
......
servlet.init(facade);
instanceInitialized = true;
......
}
initServlet中调用了servlet.init(facade),其中facade为StandardWrapperFacade,继承自ServletConfig,用来存储Servlet配置。
//StandardWrapper.java
protected final StandardWrapperFacade facade
= new StandardWrapperFacade(this);
这样整个servlet的初始化就传递到了Servlet的init()方法中了。
Servlet的实现
查看DispatcherServlet代码时,发现没有实现init()方法,第一反应是父类中实现了init()。
继承树显示DispatcherServlet主要继承了两部分,Servlet系(蓝色箭头追溯)和环境系(接口)。环境系的几个接口,类名以Aware和Capable结尾,还是挺有意思的。这和ApplicationListener中的几个Event可以凑一块,单独开个环境变量获取的party,这里不深究。
Servlet接口
标准的Servlet接口就是一个规范。
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
public ServletConfig getServletConfig();
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletInfo();
public void destroy();
}
init()用来初始化Servlet。service()方法在原理(五)也出现了,用来处理传入的请求和应答体,等讲spring mvc部分处理请求的流程时,这个方法的介绍会是整个流程的开始。getServletInfo可以获取Servlet作者、版权等信息,需要Servlet的作者自己实现。
getServletConfig()和配置有关,这就要说到原理(一)跳过没讲的配置文件web.xml。在tomcat的安装目录下的conf文件夹下和普通spring mvc(非spring boot)项目的WEB-INF文件夹下一般都有web.xml配置文件。tomcat目录下web.xml主要是系统默认Servlet配置、filter配置和mapping配置(项目暴露的接口路由),配置的格式和项目中配置的web.xml格式一致。这里以spring mvc 项目WEB-INF目录下配置的web.xml为例:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/myservlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
......
</web-app>
contextConfigLocation标签定义了项目自己的servlet.xml配置文件的位置。ServletConfig包含了servlet标签和contextConfigLocation标签设定的配置文件的大部分配置内容。从ServletConfig中能获取servlet配置,甚至上一级Context的配置内容。对于ServletConfig类对象的初始化,可以看到在init(ServletConfig config)方法中有从Wrapper中传入这个参数。servlet.xml中可以配置的内容就比较多了,主要是一些bean,比如视图解析器的配置。这里贴一个idea自动生成的servlet.xml配置。
<?xml version="1.0" encoding="UTF-8"?>
<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.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd">
<!--视图解析-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!--扫描-->
<context:component-scan base-package="com.demo.controller"/>
</beans>
spring boot的伟大功绩之一就是让我们从这种繁琐的配置中解放了出来。
GenericServlet和HttpServlet
GenericServlet是抽象类,继承Servlet, ServletConfig,包含了 ServletConfig类引用config,重载了Servlet和ServletConfig的几个方法,可以作为获取环境ServletConfig的默认实现。值得一提的是GenericServlet对init方法进行了封装,重载了带参数的init(ServletConfig config),对类中封装的ServletConfig引用进行了赋值,然后调用了不带参数的init方法,后者是的代码逻辑由子类实现。上文说到的Servlet的初始化servlet.init(facade),调用的就是GenericServlet的init(ServletConfig config)的方法。
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
// NOOP by default
}
HttpServlet继承GenericServlet,这个Servlet和协议相关,主要包括了一些用来处理http请求的方法,包括doHead、doGet、doPost、doPut、doDelete、doOptions、doTrace等,当然还有service方法。service()作为请求的通用入口,然后根据Http请求的类型不同分发给doXxx()方法。由于除了doHead方法,其他方法包括service()方法都被后续的子类FrameworkServlet重载了,HttpServlet相当于只是定义了一个标准,这里也就不再贴代码了。
HttpServletBean和FrameworkServlet
HttpServletBean最重要的作用是重载了不带参数的init()方法。
@Override
public final void init() throws ServletException {
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
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);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
initServletBean();
}
其中,BeanWrapper是一个获取类对象,然后直接修改对象属性的工具。BeanWrapper bw获取了Servlet对象(实际就代表DispatcherServlet对象),然后直接设置ServletConfig相关的配置(“bw.setPropertyValues(pvs, true)”)到Servlet对象中。在init()方法中还提供了initBeanWrapper(bw)和initServletBean()两个模板方法供子类实现。initBeanWrapper直接传入了Servlet的BeanWrapper,提供了直接修改Servlet对象属性的能力。initServletBean()让子类可以初始化任何servlet bean。
FrameworkServlet类中重载了initServletBean方法,主要只做了一件事:
......
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
......
initWebApplicationContext()给FrameworkServlet类的应用环境配置的引用赋值。initFrameworkServlet方法和上面的initServletBean方法一样,都是模板方法供子类实现,但是这个方法子类(DispatcherServlet)并没有重载。所以重点还是initWebApplicationContext()方法。
所谓的webApplicationContext包含了很多重要的东西,比如类加载的ClassLoader、Context配置的ServletContext、Servlet配置的ServletConfig、tomcat的webServer实例、注解相关的AnnotatedBeanDefinitionReader、BeanFactory实例、资源加载的resourceLoader等等。所以initWebApplicationContext()中使用了多重机制,确保能够将这个环境的配置获取。
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
//1.构造方法中已经配置了webApplicationContext,直接获取
if (this.webApplicationContext != null) {
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
//2.ServletContext中已经有WebApplicationContext,
//从ServletContext获取
if (wac == null) {
wac = findWebApplicationContext();
}
//3.以上都没有获取,直接创建一个然后配置内容
if (wac == null) {
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
if (this.publishContext) {
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
不管是3种方式的哪一种,onRefresh()会有且只调用一次。而onRefresh方式正是由DispatcherServlet实现的。
DispatcherServlet
public class DispatcherServlet extends FrameworkServlet {
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
......
}
DispatcherServlet的onFresh方法调用initStrategies方法初始化了spring mvc的九大组件,分别是HandlerMapping(处理器映射,寻找对应Interceptor和Controller)、HandlerAdapter(处理器适配,使用处理器干活)、HandlerExceptionResolver(处理器错误解析,请求处理过程报错,设置ModelAndView)、ViewResolver(视图解析器,寻找对应视图)、RequestToViewNameTranslator(从请求获取view名,view名供视图解析器寻找对应视图)、LocaleResolver(从请求获取Locale,Locale供视图解析器使用)、ThemeResolver(主题解析器)、MultipartResolver(处理上传请求)、FlashMapManager(管理重定向相关的FlashMap)。这些组件后续有时间可以拆开详细介绍。
组件初始化基本步骤是,先在根据名称或者类型在注册的容器bean里查找,自行定制的组件可以在context中注册,这样就能加载成功。如果在context中找不到,就需要加载默认的组件,默认组件的配置是在org.springframework.web.DispatcherServlet.properties文件中,通过调用ClassUtils.forName就能够通过className加载类。需要注意的是,spring boot的配置方法和标准spring mvc不一样,需要另行讨论。
本系列文章:
tomcat + spring mvc原理(一):tomcat原理综述和静态架构
tomcat + spring mvc原理(二):tomcat容器初始化加载和启动
tomcat + spring mvc原理(三):tomcat网络请求的监控与处理1
tomcat + spring mvc原理(四):tomcat网络请求的监控与处理2
tomcat + spring mvc原理(五):tomcat Filter组件实现原理
tomcat + spring mvc原理(六):tomcat WAR包的部署与加载