基于XML配置DispatcherServlet并整合Spring IoC容器
Spring MVC的核心就是前端控制器DispatcherServlet
,它显然是一个Servlet,需要进行配置以便由Servlet容器(比如Tomcat)来加载并管理。
那么如何配置DispatcherServlet
让Servlet容器加载呢?显然不能在其源码中加上注解,因此只能使用部署描述符web.xml了。
不过,Spring MVC也提供了一种基于Java来配置DispatcherServlet
的方式,实际上底层是基于Servlet API中的动态注册Servlet的接口。
部署描述符web.xml
然后,我们需要为我们的Web应用编写部署描述符web.xml(也可以在建立web工程时自动生成),然后在web.xml中配置Servlet了,DispatcherServlet
也是一个Servlet而已。
同样,我先给出完整的web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>appA</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/appA-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appA</servlet-name>
<url-pattern>/appA/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>appB</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/appB-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appB</servlet-name>
<url-pattern>/appB/*</url-pattern>
</servlet-mapping>
</web-app>
可以看到,web.xml跟之前学过的Spring IoC容器的基于XML的配置元数据很像,废话,都是遵循XML规范嘛!
不过这里的根标记是,它的命名空间是Java EE的。关于部署描述符我们以后再详细讨论,我们先关注DispatcherServlet
的配置。
大家可能会发现,web.xml中context这个单词出现很多次,除了<context-param>
标记里面的context是指Servlet范畴的context,剩下的显然都是指Spring范畴的context。
部署描述符中配置DispatcherServlet
部署描述符中配置DispatcherServlet
与配置普通的Servlet没什么区别,主要使用<servlet>
和<servlet-mapping>
这两个标记,下面是配置了DispatcherServlet
的web.xml:
<servlet>
标记:声明一个Servlet,其作用类似于@WebServlet
注解,它有两个子标记:
<servlet-name>
:指定该Servlet的名字,必须是唯一的,我这指定的是appA
,显然希望这个DispatcherServlet
是处理appA
这个应用的请求,也就是说你还可以再声明一个DispatcherServlet
,其名字可能叫做appB
,用来处理appB
这个应用的请求。<servlet-class>
:指定Servlet的具体类型,当然这里就必须是Spring MVC中的DispatcherServlet
了。
<servlet-mapping>
标记:用来配置一个Servlet与请求的映射模式,它也有两个子标记:
<servlet-name>
:指定要配置映射的哪个Servlet,使用其名字。<url-pattern>
:请求的URL映射模式,可以有通配符,比如星号*
。
嗯,配置很简单,一目了然,只要是属于应用appA即URL是/appA/*
模式的请求,就Servlet容器就会交给名字是appA的DispatcherServlet
实例来处理。
同理,如果再配置一个应用appB的DispatcherServlet
,也是类似,我就配置了两个DispatcherServlet
独立处理appA和appB这两个web应用的请求。
部署描述符中整合Spring IoC容器
DispatcherServlet
配置好之后,就应该 整合 Spring IoC容器了。整合这个词听起来有点太大了,其实就是要生成一个实现ApplicationContext
接口(更底层的是BeanFactory接口)的具体类的实例而已,该实例会读取配置元数据并生成和管理Bean。不过,在Web应用(部署到Servlet容器中运行)中,Spring框架为我们提供的是WebApplicationContext
接口。
之前standalone应用(可以独立部署并运行的),当然可以在应用的运行入口main
方法中生成一个Spring IoC容器;而Web应用是由Servlet容器加载并执行的,当然就应该在Servlet容器加载Web应用的时候生成Spring IoC容器了。
不过Spring MVC不需要我们显式的像standalone应用那样生成Spring IoC容器,我们只需要指定配置元数据的位置即可,当然也提供了显式的基于Java的方式。
我们首先来看看Spring MVC中的Spring IoC容器的层次问题。
Spring IoC容器的层次
可能会有也应该是这么一种场景,我们肯定尽量把相关的多个应用部署到同一个Servlet容器中,这样,多个应用可以共享各个层次(模型层、控制器层、视图层)某些代码,而其他组件可能是不一样的。
于是,每个应用需要配置自己的非共享的控制层、视图层以及业务模型组件;而那些共享的业务模型组件只需要配置一次,每个应用就都可以使用。
因此,每个应用都需要一个独立的Spring IoC容器(我们把它叫做Servlet IoC)来配置和管理自己的组件,还需要一个整个Servlet容器级别的Spring IoC容器(我们把它叫做Root IoC)来配置和管理所有应用共享的组件。
很自然的,如果一个DispatcherServlet
在自己的Servlet IoC容器中没有找到需要的组件,那么它就应该去Root IoC容器找需要的组件;如果找到了,就不需要再去Root IoC容器找了。
也就是说,一个DispatcherServlet
应该指向两个Spring IoC容器,比如上面的web.xml中就配置了两个DispatcherServlet
,每个DispatcherServlet
都指向了一个自己的Servlet IoC容器以及Root IoC容器:
配置Servlet IoC容器
自己的IoC容器当然是配置在自己里面了,于是只需要在标记下添加一个子标记:
<init-param>
标记:用来向Servlet传送初始化参数的,由此可以推测,Servlet IoC容器是在初始化Servlet的阶段生成的。它也有两个子标记:
<param-name>
:这是参数名字,这里就必须是contextConfigLocation
,意味着是IoC容器的配置文件的位置,当然这是Spring MVC规定的;<param-value>
:这是参数的值,当然就是IoC配置文件的位置了,通常都是放在WEB-INF目录下的。不过,这里可以填写多个位置以加载多个IoC配置文件,以逗号分开即可。
<load-on-startup>
这个标记其实跟配置IoC容器没有关系,主要是用来表示是否在Servlet容器启动时就加载该Servlet,非零即表示启动时就加载。
实际上,可以完全不配置Servlet IoC容器,或者<param-value>
标记的内容为空。
我们的web.xml中appA和appB都配置了自己的Servlet IoC容器。
配置Root IoC容器
Root IoC容器的配置要稍微麻烦一点,首先要配置一个监听器(属于Servlet规范):
<listener>
标记:声明一个监听器对象,再配置一个子标记:
<listener-class>
标记:指定监听器类,其值为Spring MVC为我们提供的org.springframework.web.context.ContextLoaderListener
从名字即可看出使用来加载IoC容器的。
然后要配置Root IoC容器的配置元数据的位置:
<context-param>
标记:类似<init-param>
标记,用来传递参数的,子标记也相同,不再赘述,不过这里应该传Root IoC容器的配置文件路径,我们的web.xml配置的是/WEB-INF/root-context.xml
。
IoC容器的配置元数据
现在,我们要为IoC容器提供配置元数据,我们在web.xml中配置了三个IoC容器,所以需要提供三份配置元数据,默认是使用基于XML的方式。
先来看root-context.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="test.common"/>
</beans>
123456789101112
可以看到,里面开启了组件扫描,故采用的是基于注解的Bean生成和自动装配,而Root IoC容器扫描的范围是test.common
这个包。
appA-context.xml和appB-context.xml也类似,不过appA应用的Servlet IoC容器扫描的范围是test.appa
这个包,appB应用的是test.appb
这个包。
总结
- Spring MVC的核心配置是在Servlet容器中配置DispatcherServlet;
- 可以配置多个DispatcherServlet;
- Spring MVC中的Spring IoC容器分为两层:每个DispatcherServlet可以配置自己的IoC容器,然后它们都有一个针对整个Web应用的Root IoC容器;
- Servlet IoC容器在DispatcherServlet的内部使用
<init-param>
标记传入参数contextConfigLocation
来配置; - Root IoC容器使用Spring MVC的
ContextLoaderListener
监听器和<context-param>
标记来配置。
基于Java配置DispatcherServlet并整合Spring IoC容器
介绍
使用基于Java的方式进行配置的基本步骤就是要实现 Spring MVC提供的某个接口或扩展某个抽象类,往往扩展抽象类会更简单更清晰一些;然后覆盖某些方法即可。
这些接口和抽象类的名字都是以Initializer结尾的,我们就叫它们为初始化器吧。
Spring MVC会自动扫描是否有初始化器的具体类,如果有,就会自动实例化它们并执行相应的方法来配置DispatcherServlet并整合Spring IoC容器。
而且,基于XML和基于Java这两种方式可以同时使用。
使用WebApplicationInitializer接口
我们先使用实现WebApplicationInitializer
接口的方式来配置DispatcherServlet并整合Spring IoC容器。
首先,假设我们要为spring-mvc-test工程的appA应用使用基于Java的方式来配置DispatcherServlet并整合Spring IoC容器。
先把web.xml中的关于appA的相关配置注释掉或删掉,我这里选择注释(使用<!--
和-->
):
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<!-- 注释掉appA的相关配置
<servlet>
<servlet-name>appA</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/appA-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appA</servlet-name>
<url-pattern>/appA/*</url-pattern>
</servlet-mapping>
-->
<servlet>
<servlet-name>appB</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/appB-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appB</servlet-name>
<url-pattern>/appB/*</url-pattern>
</servlet-mapping>
</web-app>
新建类AppAInitializer
我把新建的这个初始化器类放到test.config
包下,你也可以放在自己定义的包下,AppAInitializer.java的代码如下:
package test.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
public class AppAInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) throws ServletException {
XmlWebApplicationContext appAContext = new XmlWebApplicationContext();
appAContext.setConfigLocation("/WEB-INF/appA-context.xml");
ServletRegistration.Dynamic registrationA = container.addServlet("appA", new DispatcherServlet(appAContext));
registrationA.setLoadOnStartup(1);
registrationA.addMapping("/appA/*");
}
}
WebApplicationInitializer
接口有唯一的方法onStartup
需要我们实现。那怎么实现呢?
实例化Spring IoC容器
还记得我们在standalone应用中是如何在main方法中实例化Spring IoC容器的吗?没错,就是使用ClassPathXmlApplicationContext
来实例化一个ApplicationContext
对象,参考这篇文章。
同样,在Web应用中也是类似,只不过Spring MVC为我们提供的是WebApplicationContext
这个接口,如果配置元数据是基于XML的,那么我们就可以使用XmlWebApplicationContext
这个具体类,我们实例化一个它的对象,然后设置设置它的配置元数据的位置即可(这个位置默认是从资源的根目录开始)。
XmlWebApplicationContext appAContext = new XmlWebApplicationContext();
appAContext.setConfigLocation("/WEB-INF/appA-context.xml");
动态注册DispatcherServlet
我们直接实例化一个DispatcherServlet
对象,别忘了把我们前面生成的IoC容器传给它,这个IoC容器就是它的Servlet IoC容器了:
new DispatcherServlet(appAContext)
接下来,就应该配置DispatcherServlet
了,这里,主要采用的是Servlet规范中的动态注册Servlet的API,即接口ServletContext
的addServlet
方法,ServletContext
对象container
是onStartup
方法的参数:
ServletRegistration.Dynamic registrationA = container.addServlet("appA", new DispatcherServlet(appAContext));
最后,使用得到的注册对象设置Servlet的其他属性,比如是否容器启动时加载、URL映射模式等:
registrationA.setLoadOnStartup(1);
registrationA.addMapping("/appA/*");
验证
同上篇文章,不再赘述。
结论是整个Web应用可以由Tomcat正常加载并运行,包括Java配置的appA应用和原来web.xml中配置的appB应用。
如果完全不用web.xml配置呢?
那就把web.xml中的内容都注释或删除。
- 然后,可以仍然在
AppAInitializer
的onStartup
方法中添加appB应用以及Root IoC容器的配置代码,不过这样的话代码会显得有些臃肿且不明确,记住,一个类只干一件事。 - 当然,也可以为appB应用创建独立的初始化器
AppBInitializer
,然后实现onStartup
方法;为Root IoC容器的配置创建独立的初始化器MyWebApplicationInitializer
,然后实现onStartup
方法。这里的类名都可以自由命名的。
AppBInitializer
跟AppAInitializer
类似,读者可以自行实现。
Root IoC容器则因为使用的是监听器而非Servlet,而有所不同。我们可以根据上面的动态注册Servlet的代码举一反三,既然有addServlet
方法,是否也会有addListener
方法呢?答案是肯定的,我们可以使用Eclipse的代码补全功能可以很快的找到这个方法(其他IDE都应该有此功能)。于是,我们应该先new
一个Spring MVC提供的ContextLoaderListener
,将Root IoC容器的配置元数据的位置传给这个监听器即可:
XmlWebApplicationContext rootContext = new XmlWebApplicationContext();
rootContext.setConfigLocation("/WEB-INF/root-context.xml");
container.addListener(new ContextLoaderListener(rootContext));
使用AbstractDispatcherServletInitializer抽象类
上面覆盖WebApplicationInitializer
接口的onStartup
方法的方式感觉不够简练,所以Spring MVC为我们提供了更方便的AbstractDispatcherServletInitializer
抽象类来配置DispatcherServlet并整合Spring IoC容器。
实际上,这个抽象类里面也可以配置Root IoC容器,不过这个抽象类也是通过继承AbstractContextLoaderInitializer
抽象类的,因此我们也可以直接使用AbstractContextLoaderInitializer
抽象类来配置Root IoC容器,见下一节。
我为appB应用的DispatcherServlet
创建了一个独立的初始化器,AppBInitializer.java:
package test.config;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
public class AppBInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext appBContext = new XmlWebApplicationContext();
appBContext.setConfigLocation("/WEB-INF/appB-context.xml");
return appBContext;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/appB/*" };
}
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected String getServletName() {
return "appB";
}
}
我们必须要覆盖三个方法:
createServletApplicationContext
:创建Servlet IoC容器的实例并传入配置元数据的位置;getServletMappings
:返回Servlet的URL映射模式;createRootApplicationContext
:创建Root IoC容器的实例并传入配置元数据的位置,我这里直接返回null,因为我下一节直接使用AbstractContextLoaderInitializer
抽象类来配置Root IoC容器。
我还覆盖了一个方法:
getServletName
:返回Servlet的名字即可,如果不覆盖此方法,会使用AbstractDispatcherServletInitializer
抽象类默认的名字DEFAULT_SERVLET_NAME = "dispatcher"
。大家可以自行看它的源码(如何查看源码参考这篇文章)。
一开始我并没有覆盖此方法,但是验证的时候我发现Console视图中打印的日志信息是初始化dispatcher这个DispatcherServlet,于是就想该如何使用AbstractDispatcherServletInitializer
抽象类配置Servlet的名字,于是就查看了一下其源码。
使用AbstractContextLoaderInitializer抽象类
AbstractContextLoaderInitializer
抽象类与上一节的AbstractDispatcherServletInitializer
抽象类的配置模式是一样的,都是覆盖某些方法而已,不再赘述,直接上代码。
MyWebApplicationInitializer.java:
package test.config;
import org.springframework.web.context.AbstractContextLoaderInitializer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;
public class MyWebApplicationInitializer extends AbstractContextLoaderInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
XmlWebApplicationContext rootContext = new XmlWebApplicationContext();
rootContext.setConfigLocation("/WEB-INF/root-context.xml");
return rootContext;
}
}
总结
- 基于Java和基于XML在一个Web应用中可以混合使用;
- 基于Java的方式又有两种:
WebApplicationInitializer
接口和AbstractDispatcherServletInitializer
抽象类; - 基于Java的整合Spring IoC的思想是一致的:都是先new一个容器对象,设置元数据的所在位置,然后把容器对象传给
DispatcherServlet
或ContextLoaderListener
对象。 - 我们要善于举一反三,触类旁通,推此及彼。
- 我们要多看源代码和Javadoc。