前言
在 Servlet 3.0 标准之前,配置Spring MVC 要在 web.xml 中配置 前端控制器DispatcherServlet 、Web容器启动监听器ContextLoaderListener 。配置的东西倒是不多,但是由于 XML 文件的特性,即使是简单的配置上述两项也会有一大堆的 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">
<!--配置Spring IOC容器的启动监听器-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/springApplication.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--配置Spring MVC的前端控制器-->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
本来记住几个类名还不太难,可是要记住这么多XML节点,就有点费劲了。
Servlet 3.0 (及以上版本)
自从 Servlet 3.0 问世之后,我们可以通过 Java 的方式配置 Servlet 容器了,摆脱了繁琐的 XML 文件,瞬间起飞~~ 芜湖!!!
使用 Java 配置的原理
所有实现了 Servlet3.0 的 Web 容器,都会在 jar/war 包下面搜索一个文件,这个文件的名字必须叫 javax.servlet.ServletContainerInitializer ,而且必须放在 jar/war 包的类路径下面的 META-INF/services 文件夹里面。这个文件的内容也非常简单,是某个实现了 javax.servlet.ServletContainerInitializer 接口的实现类的全限定类名。
(PS : 在 IDEA 中,蓝色的文件夹就代表项目的类路径。)
现在看一下这个类的实现
@HandlesTypes(Color.class)
public class MyServletContainerInit implements ServletContainerInitializer {
/**
* Servlet 容器启动时调用该方法
* @param set 上面的 @HandlersTypes 注解中 classes 方法指明的接口/抽象类的所有子类
* @param servletContext Application 作用域,可以用来注册 Servlet 、Listener、Filter
* @throws ServletException
*/
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
// 注册 servlet
ServletRegistration.Dynamic userServlet = servletContext.addServlet("userServlet", UserServlet.class);
userServlet.addMapping("/users");
// 注册 filter
FilterRegistration.Dynamic characterFilter = servletContext.addFilter("characterFilter", MyFilter.class);
// 第一个参数是设置过滤器拦截哪种请求
characterFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),true, "/*");
// 添加 Listener
servletContext.addListener(MyListener.class);
}
}
可以看到,onStartup 方法中所做的事情,和 web.xml 文件中做的事情是一样的,但是简化了很多。是不是很方便!而且要配什么,都是通过调用方法的方式进行配置,而不是像 XML 中一样要记住那些标签的名字。
这里要特别的注意一下 @HandlesTypes 注解,这个注解就是后面要说的,Spring MVC 基于 Java 配置的原理。
使用 Java 配置 Spring MVC
讲了 Servlet 3.0 之后,我们再来理解基于 Java 配置的 Spring MVC,就会轻车熟路了。因为说到底,Spring MVC的核心:前端控制器 DispatcherServlet 只是一个普通的 Servlet ,它也需要注入到 Web 容器中才能发挥 Spring MVC 的前端控制器功能。你刀法再好,我不给你刀,你也练不出来。
有了 Servlet 3.0 的基础,我们知道了所有实现了 Servlet 3.0 标准的 Web 容器在启动的时候都会去项目类路径下的 META-INF/services 下面查找 javax.servlet.ServletContainerInitializer 文件,那我们就来看看 Spring MVC 的 jar 包下面有没有这个文件。
果然被我们找到了,只不过这里的实现类,是一个名为 SpringServletContainerInitializer 的类,点进去看看里面写了啥
/**
* 注意这里,非常重要,这就是 Spring-MVC 留给我们的扩展接口
*/
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
// 如果传入的webAppInitializerClasses中某个成员不是WebApplicationInitializer的子接口
// 并且不是抽象实现类,就要创建实例
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
// 将所有 WebApplicationInitializer 实现类收集到 list 中
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
// 循环调用所有实现类的 onStartup 方法
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
主要的实现逻辑如下:
- 通过注解
@HandlesTypes来获取所有WebApplicationInitializer类型的实现类 - 如果实现类 Set 不为空,则遍历所有实现类
- 找到其中的不是子接口的、不是抽象实现类的 实现类
- 创建实例,存到
LinkedList中 - 循环调用每个实现类的
onStartup方法。
既然要加载的是WebApplicationInitializer接口的实现类,那是不是我们创建一个类来实现该接口就可以对Web 容器进行初始化和添加组件了呢?如果真的这么写了,就和上面的普通 Web 工程一样了,我们无法通过这种形式创建 Spring MVC父子容器。
Spring MVC父子容器
先了解下 Spring MVC 父子容器的概念,打开–> Spring 官网<-- 可以看到,Spring 推荐我们使用父子容器的方式创建 Spring MVC上下文环境。其中 Servlet 应用上下文 注入所有与 Web 开发相关的功能,如所有 Controller 、视图解析器和处理器映射器。
而根应用上下文则负责管理所有与业务相关的组件,如 Service 层和 Repositories 数据访问层。
开始配置
说了半天,终于开始配置 Spring MVC 了。我们先打开WebApplicationInitializer 接口,看看 Spring 为我们提供的注释。
注释太长了,里面写了详细的 xml 配置方式。我主要翻译并截取了重要的部分

真的就跟写论文一样······。老外做程序员太吃香了,这些文档看完还有不会的道理么?
屁话不多说,这里主要强调了该接口的一个抽象实现,也是本文的重头戏:AbstractAnnotationConfigDispatcherServletInitializer。Spring-MVC官方推荐我们实现这个类,来对 Web 容器进行初始化的工作。先来看看这个类。
public abstract class AbstractAnnotationConfigDispatcherServletInitializer
extends AbstractDispatcherServletInitializer {
/**
* {@inheritDoc}
* <p>This implementation creates an {@link AnnotationConfigWebApplicationContext},
* providing it the annotated classes returned by {@link #getRootConfigClasses()}.
* Returns {@code null} if {@link #getRootConfigClasses()} returns {@code null}.
*/
@Override
@Nullable
protected WebApplicationContext createRootApplicationContext() {
Class<?>[] configClasses = getRootConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(configClasses);
return context;
}
else {
return null;
}
}
/**
* {@inheritDoc}
* <p>This implementation creates an {@link AnnotationConfigWebApplicationContext},
* providing it the annotated classes returned by {@link #getServletConfigClasses()}.
*/
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
Class<?>[] configClasses = getServletConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
context.register(configClasses);
}
return context;
}
/**
* 模板方法1:通过子类重写该方法,来返回 Root 应用上下文的配置类类型
* @return the configuration for the root application context, or {@code null}
* if creation and registration of a root context is not desired
*/
@Nullable
protected abstract Class<?>[] getRootConfigClasses();
/**
* 模板方法2:通过子类重写该方法,来返回 Servlet 应用上下文的配置类类型
* {@code null} if all configuration is specified through root config classes.
*/
@Nullable
protected abstract Class<?>[] getServletConfigClasses();
}
可以看到 AbstractAnnotationConfigDispatcherServletInitializer 中应用了模板方法设计模式,上面两个方法分别创建了一个 WebApplicationContext 实例。顾名思义,createRootApplicationContext 方法负责创建根应用上下文环境,而createServletApplicationContext 负责创建 Servlet 应用上下文实例。这里的 WebApplicationContext 实例都是通过 new 关键字创建的,也就是说不存在单例模式的可能,两个方法都是实打实的创建出两个上下文,地址绝对不相同。
createRootApplicationContext方法调用了模板方法getRootConfigClasses来获取根应用的配置类,这个类里面可以注入一些 Bean ,指定包扫描等操作,也就是相当于一个 springApplication.xml 配置文件。createServletApplicationContext方法调用了模板方法getServletConfigClasses来获取 Servlet 容器的配置类,可以在这里配置前端控制器,视图解析器等组件。
我们只需要实现getRootConfigClasses 方法和 getServletConfigClasses 方法就可以分别配置 根应用上下文和 Servlet 上下文,再实现 getServletMappings 方法即可设置前端控制器的拦截路径。
代码实现
啰嗦一堆,重头戏终于来了!!!我会在方法的注释上贴上对应的 xml 配置方式,方便大家对比差异。
- 先看
AbstractAnnotationConfigDispatcherServletInitializer的子类,也就是我们的配置类
public class WebMvcConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 指明跟容器的配置类的类型
* 对应 XML 配置,这里可能不太对,在 xml 配置中。根应用上下文是在容器启动监听器中创建的
* <context-param>
* <param-name>contextConfigLocation</param-name>
* <param-value>classpath:spring/springApplication.xml</param-value>
* </context-param>
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootContext.class};
}
/**
* 指明 Web 容器的配置类的类型
* <servlet>
* <servlet-name>SpringMVC</servlet-name>
* <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
* <load-on-startup>1</load-on-startup>
* </servlet>
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{ServletContext.class};
}
/**
* 为 DispatcherServlet 添加 URL 映射,例如:/ 或者 /app
* 对应 XML 的配置
* <servlet-mapping>
* <servlet-name>SpringMVC</servlet-name>
* <url-pattern>/</url-pattern>
* </servlet-mapping>
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
- 根应用上下文配置类,只是用于演示配置,就只配了个数据源做做样子
/**
* Spring 容器只扫描和 web 无关的组件,所以要排除 Controller
*/
@ComponentScan(basePackages = "com.mvc.demo",
excludeFilters = {
@ComponentScan.Filter(classes = Controller.class)
}
)
public class RootContext {
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("123");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306");
return dataSource;
}
}
- Servlet 应用上下文配置类,配置了前端控制器、视图解析器。还应该配置一下静态资源拦截器,这里就不写那么多了。
/**
* Dispatcher 容器,只负责扫描 Controller,要排除其他注解标注的类
* 默认就会扫描 Spring 所有 Component 的衍生注解标注的类,所以要禁用
* 默认规则
*/
@ComponentScan(basePackages = "com.mvc.demo", useDefaultFilters = false,
includeFilters = @ComponentScan.Filter(classes = Controller.class)
)
public class ServletContext {
/**
* 对应的 xml 配置
*<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
*/
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
return dispatcherServlet;
}
}
好了,这就是用 Java 配置 Spring MVC的全部过程,是不是很方便?比起 xml 繁琐的配置,我还是更喜欢用 Java 来配置。
如果使用 SpringBoot ,这些配置都不用写了,更加方便。但是理解了这种 Java 配置形式之后,再去理解 SpringBoot 的自动配置原理会容易许多。苦尽甘来,写过 XML 的配置形式,才能体验 Java 配置的简单。配过 Spring MVC 才能知道 SpringBoot 是多么的方便。如果大家喜欢我的文章,觉得讲的还算通俗易懂,麻烦点个赞支持下!
来源:oschina
链接:https://my.oschina.net/u/4280951/blog/4311381