1.自定义ServletContainerInitializer
2.使用ServletContext注册web三大组件
3.Servlet3.0与SpringMVC整合
1.引入依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sdehua</groupId>
<artifactId>springmvc-annotation-coffee</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>springmvc-annotation-coffee</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- spring mvc依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.11.RELEASE</version>
</dependency>
<!-- servlet api依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<!-- 使用Servlet 3.0以上的特性 -->
<version>3.1.0</version>
<!--
由于Tomcat服务器里面也有servlet api,即目标环境已经该jar包了,
所以我们在这儿将以上servlet api的scope设置成provided。
这样的话,我们的项目在被打成war包时,就不会带上该jar包了,
否则就会引起jar包冲突
-->
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.4</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.两种方式整合SpringMVC
1)编码方式
// 我们可以编写一个类来实现WebApplicationInitializer接口哟,当然了,你也可以编写一个类来实现ServletContainerInitializer接口
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// 然后,我们来创建一个AnnotationConfigWebApplicationContext对象,它应该代表的是web的IOC容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
// 加载我们的配置类
context.register(AppConfig.class);
// 在容器启动的时候,我们自己来创建一个DispatcherServlet对象,并将其注册在ServletContext中
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
// 这儿是来配置DispatcherServlet的映射信息的
registration.addMapping("/app/*");
}
}
2)继承抽象类AbstractAnnotationConfigDispatcherServletInitializer
展开我们maven工程下的Maven Dependencies目录,发现我们导入了spring-web-4.3.11.RELEASE.jar这样一个jar包,如下图所示:
展开该jar包,发现它里面有一个META-INF/services/目录,而且在该目录下有一个名字叫javax.servlet.ServletContainerInitializer的文件,其内容如下所示:
Servlet容器在启动应用的时候,会扫描当前应用每一个jar包里面的META-INF/services/javax.servlet.ServletContainerInitializer文件中指定的实现类,然后加载该实现类并运行它里面的方法。
那我们就来看一下spring-web-4.3.11.RELEASE.jar中META-INF/services/目录里面的javax.servlet.ServletContainerInitializer文件中到底指定的哪一个类,从上图我们可以知道其指定的是org.springframework.web.SpringServletContainerInitializer这个类。
我们不妨查看一下该类的源码,如下图所示,它实现的就是ServletContainerInitializer接口:
它里面也只有一个onStartup方法,所以我们重点来看该方法的具体实现。
Servlet容器在启动我们Spring应用之后,会传入一个我们感兴趣的类型的集合,然后在onStartup方法中拿到之后就会来挨个遍历,如果遍历出来的我们感兴趣的类型不是接口,也不是抽象类,但是WebApplicationInitializer接口旗下的,那么就会创建该类型的一个实例,并将其存储到名为initializers的LinkedList
也可以这样说,我们Spring的应用一启动就会加载感兴趣的WebApplicationInitializer接口旗下的所有组件,并且为这些WebApplicationInitializer组件创建对象,当然前提是这些组件即不是接口,也不是抽象类。
接下来,我们就来到咱们感兴趣的WebApplicationInitializer接口中,并查看该接口的继承树(快捷键Ctrl + T),发现它下面有三个抽象类,如下图所示:
因此,接下来,我们就得来好好研究一下以上这三个抽象类:
第一层抽象类,即AbstractContextLoaderInitializer
我们先来研究WebApplicationInitializer接口下面的第一层抽象类,即AbstractContextLoaderInitializer。不妨点进该抽象类里面去看一看,如下图所示,我们主要来看其onStartUp方法:
发现在该方法中调用了一个registerContextLoaderListener方法,见名思意,应该是来注册ContextLoaderListener的。
我们继续点进registerContextLoaderListener方法里面去看一看,发现它里面调用了一个createRootApplicationContext方法,该方法是来创建根容器的,而且该方法是一个抽象方法,需要子类自己去实现。然后,根据创建的根容器创建上下文加载监听器(即ContextLoaderListener),接着,向ServletContext中注册这个监听器。
至此,以上这个抽象类,我们算是大概地分析完了。
接下来,我们来研究一下它下面的子类,即AbstractDispatcherServletInitializer,从名字上我们应该能知道它就是一个DispatcherServlet(即Spring MVC的前端控制器)的初始化器。
第二层抽象类,即AbstractDispatcherServletInitializer
我们也不妨点进该抽象类里面去看一看,并且主要来看其onStartUp方法,发现会注册DispatcherServlet,如下所示:
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// 调用父类(即AbstractContextLoaderInitializer)的onStartup方法,先把根容器创建出来
super.onStartup(servletContext);
// 往ServletContext中注册DispatcherServlet
registerDispatcherServlet(servletContext);
}
然后,继续点进registerDispatcherServlet方法里面去看一看,看看究竟是怎么向ServletContext中注册DispatcherServlet的,如下所示:
protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() must not return empty or null");
// 调用createServletApplicationContext方法来创建一个web的IOC容器,而且该方法还是一个抽象方法,需要子类去实现
WebApplicationContext servletAppContext = createServletApplicationContext();
Assert.notNull(servletAppContext,
"createServletApplicationContext() did not return an application " +
"context for servlet [" + servletName + "]");
// 调用createDispatcherServlet方法来创建一个DispatcherServlet
FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
// 将创建好的DispatcherServlet注册到ServletAppContext中
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
Assert.notNull(registration,
"Failed to register servlet with name '" + servletName + "'." +
"Check if there is another servlet registered under the same name.");
registration.setLoadOnStartup(1);
// 配置DispatcherServlet的映射映射信息,其中getServletMappings方法是一个抽象方法,需要由子类自己来重写
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());
Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
registerServletFilter(servletContext, filter);
}
}
customizeRegistration(registration);
}
我们发现会先调用createServletApplicationContext方法来创建一个WebApplicationContext(即web的IOC容器),再调用createDispatcherServlet方法来创建一个DispatcherServlet,如下所示,在这new了一个DispatcherServlet:
protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
// 创建一个DispatcherServlet
return new DispatcherServlet(servletAppContext);
}
此外,我们还发现在registerDispatcherServlet方法中还会将创建好的DispatcherServlet注册到ServletAppContext中,很显然,这时会返回一个ServletRegistration.Dynamic对象,自然地就要来配置该DispatcherServlet的映射信息了,好家伙,在配置该DispatcherServlet的映射信息时,还调用了一个getServletMapppings方法,不过该方法是一个抽象方法,需要由子类自己来重写。
至此,我们就算分析完了AbstractDispatcherServletInitializer抽象类。
接下来,我们来研究一下它下面的子类,即AbstractAnnotationConfigDispatcherServletInitializer,从名字上我们应该能知道它就是一个注解方式配置的DispatcherServlet初始化器,它是我们研究的重点。
[
](https://blog.csdn.net/yerenyuan_pku/article/details/114915111)
第三层抽象类,即AbstractAnnotationConfigDispatcherServletInitializer
点进该抽象类里面去看,可以看到它重写了AbstractContextLoaderInitializer抽象父类里面的createRootApplicationContext方法,如下所示,而且我们知道该方法是来创建根容器的:
@Override
protected WebApplicationContext createRootApplicationContext() {
// 传入一个配置类(用户自定义)
Class<?>[] configClasses = getRootConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
// 创建一个根容器
AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
// 注册获取到的配置类,相当于注册配置类里面的组件
rootAppContext.register(configClasses);
return rootAppContext;
}
else {
return null;
}
}
那是怎样创建根容器的呢?首先获取到一个配置类,调用的可是getRootConfigClasses方法,调用该方法能传入一个配置类(其实就是我们自己写的),而且该方法还是一个抽象方法,需要由子类自己来重写。继续,要知道我们以前写的可是xml配置文件,获取到了之后,会new一个AnnotationConfigWebApplicationContext,这就相当于创建了一个根容器,然后将获取到的配置类注册进去,相当于是注册配置类里面的组件,最终返回创建的根容器。
此外,该抽象类里面还有一个createServletApplicationContext方法,如下所示,它是来创建web的IOC容器的,其实这个方法就是重写的AbstractDispatcherServletInitializer抽象类里面的createServletApplicationContext方法:
@Override
protected WebApplicationContext createServletApplicationContext() {
// 创建一个web容器
AnnotationConfigWebApplicationContext servletAppContext = new AnnotationConfigWebApplicationContext();
// 获取一个配置类
Class<?>[] configClasses = getServletConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
// 注册获取到的配置类,相当于注册配置类里面的组件
servletAppContext.register(configClasses);
}
return servletAppContext;
}
可以看到,在以上方法中首先会创建一个web的IOC容器(即AnnotationConfigWebApplicationContext对象),然后再获取一个配置类,调用的是getServletConfigClasses方法,我们不妨点进该方法里面去看一下,如下所示,发现它是一个抽象方法,需要由子类自己来重写:
protected abstract Class<?>[] getServletConfigClasses();
获取到配置类之后,最终会将其注册进去。
至此,我们就算分析完了AbstractAnnotationConfigDispatcherServletInitializer抽象类。
3.概括
如果我们想以注解方式(也可以说是以配置类的方式)来整合Spring MVC,即再也不要在web.xml文件中进行配置,那么我们只需要自己来继承AbstractAnnotationConfigDispatcherServletInitializer这个抽象类就行了。继承它之后,它里面会给我们预留一些抽象方法,例如getServletConfigClasses、getRootConfigClasses以及getServletMappings等抽象方法,我们只须重写这些抽象方法即可,这样就能指定DispatcherServlet的配置信息了,随即,DispatcherServlet就会被自动地注册到ServletContext对象中。
查看官方文档可以看到Spring官方也推荐使用父子容器的概念,分为根容器和web容器:
web容器:也即子容器,只来扫描controller控制层组件(一般不包含核心业务逻辑,只有数据校验和视图渲染等工作)与视图解析器等等
根容器:扫描业务逻辑核心组件,包括不同的数据源等等
继续往下看1.1.1. Context Hierarchy这一小节中的内容,发现跟我们分析的一样,要想以注解方式(也可以说是以配置类的方式)来整合Spring MVC,那么只需要我们自己来继承AbstractAnnotationConfigDispatcherServletInitializer这个抽象类就行了,这样,web容器启动的时候就能处理我们实现的这个类的内容了。
4.案例实战
首先,我们来编写一个类,例如MyWebAppInitializer,来继承AbstractAnnotationConfigDispatcherServletInitializer这个抽象类,一开始我们写成下面这样:
package com.sdehua;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{
/*
* getRootConfigClasses方法:它是来获取根容器的配置类的,
* 该配置类就类似于我们以前经常写的Spring的配置文件,
* 而且我们以前是利用监听器的方式来读取Spring的配置文件的哟~,
* 还记得吗?然后,就能创建出一个父容器了
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
/*
* getServletConfigClasses方法:它是来获取web容器的配置类的,
* 该配置类就类似于我们以前经常写的Spring MVC的配置文件,
* 而且我们以前是利用前端控制器来加载Spring MVC的配置文件的哟~,
* 你还记得吗?然后,就能创建出一个子容器了
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
/*
* getServletMappings方法:它是来获取DispatcherServlet的映射信息的。
* 该方法需要返回一个String[],
*
* 如果我们返回的是这样一个new String[]{"/"},
* 那么DispatcherServlet就会来拦截所有请求,包括静态资源,比如xxx.js文件、
* xxx.png等,但是不包括*.jsp,也即不会拦截所有的jsp页面。
*
* 如果我们返回的是这样一个new String[]{"/*"}
* 那么DispatcherServlet就是真正来拦截所有请求了,包括*.jsp,
* 也就是说就连jsp页面都拦截,所以,我们切忌不可写成这样(即/*)。
* 否则的话,jsp页面一旦被Spring MVC拦截,
* 最终极有可能我们就看不到jsp页面了,
* 因为jsp页面是由Tomcat服务器中的jsp引擎来解析的。
*/
@Override
protected String[] getServletMappings() {
return new String[] {"/"};
}
}
由于我们还需要在getRootConfigClasses和getServletConfigClasses这俩方法中指定两个配置类的位置,所以我们来创建上两个配置类,分别如下:
- 根容器的配置类,例如RootConfig ```java package com.sdehua.config;
public class RootConfig {
}
- web容器的配置类,例如AppConfig
```java
package com.sdehua.config;
public class AppConfig {
}
以上这两个配置类最终需要形成父子容器的效果。还要一点需要重点来说明,即AppConfig配置类只来扫描所有的控制器(即Controller),以及和网站功能有关的那些逻辑组件;RootConfig配置类只来扫描所有的业务逻辑核心组件,包括dao层组件、不同的数据源等等,即只扫描和业务逻辑相关的组件。
[
](https://blog.csdn.net/yerenyuan_pku/article/details/114968293)
接下来,我们来完善以上两个配置类。首先,先来完善RootConfig配置类,我们可以使用@ComponentScan注解来指定扫描com.meimeixia包以及子包下的所有组件,而且为了形成父子容器,还必须得排除掉一些组件,那排除掉哪些组件呢?很显然,应该排除掉controller控制层组件,即Controller。所以,我们得通过@ComponentScan注解的excludeFilters()方法按照注解的方式来排除掉所有标注了@Controller注解的组件。
package com.sdehua.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
//该配置类相当于Spring的配置文件
//Spring容器不扫描Controller,它是一个父容器
@ComponentScan(value="com.sdehua",excludeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
})
public class RootConfig {
}
然后,再来完善AppConfig配置类,我们同样使用@ComponentScan注解来指定扫描com.meimeixia包以及子包下的所有组件,但是呢,与上面正好相反,这儿只扫描controller控制层组件,即Controller,如此一来就能与上面形成互补配置了。OK,那我们就通过@ComponentScan注解的includeFilters()方法按照注解的方式来指定只扫描controller控制层组件。
package com.sdehua.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
@ComponentScan(value="com.sdehua",includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
},useDefaultFilters=false)
public class AppConfig {
}
尤其要注意这一点,在以上配置类中通过@ComponentScan注解的includeFilters()方法来指定只扫描controller控制层组件时,需要禁用掉默认的过滤规则,即必须得加上useDefaultFilters=false这样一个配置。千万记得必须要禁用掉默认的过滤规则哟,否则扫描就不会生效了。
但是,在RootConfig配置类中通过@ComponentScan注解的excludeFilters()方法来指定排除哪些组件时,是不需要对useDefaultFilters进行设置的,因为其默认值就是true,表示默认情况下标注了@Component、@Repository、@Service以及@Controller这些注解的组件都会被扫描,即扫描所有。
接下来,我们就要分别来编写一个controller控制层组件和service业务层组件来进行测试了。首先,编写一个service业务层组件,例如HelloService,如下所示:
package com.sdehua.service;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
public String sayHello(String name){
return "Hello,"+name;
}
}
然后,编写一个controller控制层组件,例如HelloController,并且我们还可以在该HelloController中注入HelloService组件,来调用其方法。
package com.sdehua.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.sdehua.service.HelloService;
@Controller
public class HelloController {
@Autowired
HelloService helloService;
@ResponseBody
@RequestMapping("/hello")
public String hello() {
String hello = helloService.sayHello("tomcat...");
return hello;
}
}
从以上HelloController的代码中,我们可以看到它里面的hello方法是来处理hello请求的,而且通过@ResponseBody注解会直接将返回的结果(即字符串)写到浏览器页面中。
现在,我们能不能启动咱们的项目进行测试了呢?还不可以,因为我们还没有在我们自己编写的MyWebAppInitializer类中指定两个配置类的位置。OK,那我们来分别指定一下,如下所示:
package com.sdehua;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import com.sdehua.config.AppConfig;
import com.sdehua.config.RootConfig;
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{
/*
* getRootConfigClasses方法:它是来获取根容器的配置类的,
* 该配置类就类似于我们以前经常写的Spring的配置文件,
* 而且我们以前是利用监听器的方式来读取Spring的配置文件的哟~,
* 还记得吗?然后,就能创建出一个父容器了
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] {RootConfig.class};
}
/*
* getServletConfigClasses方法:它是来获取web容器的配置类的,
* 该配置类就类似于我们以前经常写的Spring MVC的配置文件,
* 而且我们以前是利用前端控制器来加载Spring MVC的配置文件的哟~,
* 你还记得吗?然后,就能创建出一个子容器了
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {AppConfig.class};
}
/*
* getServletMappings方法:它是来获取DispatcherServlet的映射信息的。
* 该方法需要返回一个String[],
*
* 如果我们返回的是这样一个new String[]{"/"},
* 那么DispatcherServlet就会来拦截所有请求,包括静态资源,比如xxx.js文件、
* xxx.png等,但是不包括*.jsp,也即不会拦截所有的jsp页面。
*
* 如果我们返回的是这样一个new String[]{"/*"}
* 那么DispatcherServlet就是真正来拦截所有请求了,包括*.jsp,
* 也就是说就连jsp页面都拦截,所以,我们切忌不可写成这样(即/*)。
* 否则的话,jsp页面一旦被Spring MVC拦截,
* 最终极有可能我们就看不到jsp页面了,
* 因为jsp页面是由Tomcat服务器中的jsp引擎来解析的。
*/
@Override
protected String[] getServletMappings() {
return new String[] {"/"};
}
}
这就相当于分别来指定Spring配置文件和Spring MVC配置文件的位置。
最后,我们就可以启动项目来进行测试了。项目启动成功之后,我们在浏览器地址栏中输入http://localhost:8080/springmvc-annotation-coffee/hello进行访问:
这说明我们controller控制层组件和service业务层组件都起作用了。
至此,使用注解的方式(即配置类的方式)来整合Spring MVC,彻底讲完了。
5.总结
我们知道web容器(即Tomcat服务器)在启动应用的时候,会扫描当前应用每一个jar包里面的META-INF/services/javax.servlet.ServletContainerInitializer文件中指定的实现类,然后再运行该实现类中的方法。
恰好在spring-web-4.3.11.RELEASE.jar中的META-INF/services/目录里面有一个javax.servlet.ServletContainerInitializer文件,并且在该文件中指定的实现类就是org.springframework.web.SpringServletContainerInitializer,打开该实现类,发现它上面标注了@HandlesTypes(WebApplicationInitializer.class)这样一个注解。
因此,web容器在启动应用的时候,便会来扫描并加载org.springframework.web.SpringServletContainerInitializer实现类,而且会传入我们感兴趣的类型(即WebApplicationInitializer接口)的所有后代类型,最终再运行其onStartup方法。
package org.springframework.web;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer) 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);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
从以上onStartup方法中,我们可以看到它会遍历感兴趣的类型(即WebApplicationInitializer接口)的所有后代类型,然后利用反射技术创建WebApplicationInitializer类型的对象,而我们自定义的MyWebAppInitializer就是WebApplicationInitializer这种类型的。而且创建完之后,都会存储到名为initializers的LinkedList
遍历到每一个WebApplicationInitializer对象之后,调用其onStartup方法,实际上就是先调用其(例如我们自定义的MyWebAppInitializer)最高父类的onStartup方法,创建根容器;然后再调用其次高父类的onStartup方法,创建web容器以及DispatcherServlet;接着,根据其重写的getServletMappings方法来为DispatcherServlet配置映射信息,差不多就是这样了。
4.定制与接管Spring MVC
试着回顾一下我们以前整合Spring MVC的开发,是不是应该有一个Spring MVC的配置文件?例如mvc.xml,在该配置文件中我们会写非常多的配置!下面列举一下该配置文件中的一些常用配置,比如经常会写上这样的配置:
<mvc:default-servlet-handler/>
该配置的作用就是将Spring MVC处理不了的请求交给Tomcat服务器,它是专门来针对静态资源的。试想,如果Spring MVC拦截了所有请求,必然地,静态资源也被一起拦截了,那么静态资源就无法访问到了,而写上该配置之后,静态资源就可以被访问到了。
还有,我们还写过这样的配置:
<mvc:annotation-driven />
一般而言,以上配置经常会跟
还有,我们还配置过拦截器,就像下面这样:
<mvc:interceptors>
...
</mvc:interceptors>
此外,我们还有可能配置视图映射,就像下面这样:
<mvc:view-controller path=""/>
也就是说,我们以前会在Spring MVC的配置文件中配置非常多的东西,但是现在没有该配置文件了,那么我们该怎么做到上述的这些事情呢?其实非常简单,只要查看Spring MVC的官方文档就知道了,找到1.11.1. Enable MVC Configuration这一小节,映入眼帘的就是一个@EnableWebMvc注解,如下图所示:
这告诉我们首先要做的第一件事就是使用@EnableWebMvc注解,它的作用就相当于来启动Spring MVC的自定义配置。
现在,我们就要开始定制Spring MVC,分为两步。
第一步,首先你得写一个配置类,然后将@EnableWebMvc注解标注在该配置类上。我们就以上一讲中的AppConfig配置类为例,将@EnableWebMvc注解标注在该配置类上,如下所示:
package com.sdehua.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ComponentScan(value="com.sdehua",includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
},useDefaultFilters=false)
@EnableWebMvc
public class AppConfig {
}
@EnableWebMvc注解的作用就是来开启Spring MVC的定制配置功能。我们查看Spring MVC官方文档中的1.11.1. Enable MVC Configuration这一小节的内容,发现在配置类上标注了@EnableWebMvc注解之后,相当于我们以前在xml配置文件中加上了
第二步,配置组件。要配置的组件还是挺多的,比如视图解析器、视图映射、静态资源映射以及拦截器等等,直接参考Spring MVC的官方文档。
我们查看一下Spring MVC官方文档中1.11.2. MVC Config API这一小节的内容,发现只须让Java配置类实现WebMvcConfigurer接口,就可以来定制配置。不妨让AppConfig配置类来实现该接口,如下所示:
package com.sdehua.config;
import java.util.List;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@ComponentScan(value="com.sdehua",includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
},useDefaultFilters=false)
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer{
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> arg0) {
// TODO Auto-generated method stub
}
@Override
public void addCorsMappings(CorsRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void addFormatters(FormatterRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void addInterceptors(InterceptorRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> arg0) {
// TODO Auto-generated method stub
}
@Override
public void addViewControllers(ViewControllerRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> arg0) {
// TODO Auto-generated method stub
}
@Override
public void configurePathMatch(PathMatchConfigurer arg0) {
// TODO Auto-generated method stub
}
@Override
public void configureViewResolvers(ViewResolverRegistry arg0) {
// TODO Auto-generated method stub
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> arg0) {
// TODO Auto-generated method stub
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> arg0) {
// TODO Auto-generated method stub
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
// TODO Auto-generated method stub
return null;
}
@Override
public Validator getValidator() {
// TODO Auto-generated method stub
return null;
}
}
发现这个WebMvcConfigurer接口里面定义了好多的方法!如下图所示:
实现该接口之后,我们就得来实现其里面的每一个方法了,这就是来定制Spring MVC。
来看一下其中的configurePathMatch方法,该方法的作用就是来配置路径映射规则的:
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
// TODO Auto-generated method stub
}
再来看一下其中的configureAsyncSupport方法,它的作用是来配置是否开启异步支持的:
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// TODO Auto-generated method stub
}
再再来看一下其中的configureDefaultServletHandling方法,它的作用是来配置是否开启静态资源的。我们不妨实现一下该方法,如下所示:
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
// TODO Auto-generated method stub
configurer.enable()
}
实现以上方法之后,效果就相当于我们以前在xml配置文件中写上
继续往下看吧,来看一下其中的addFormatters方法,它的作用是可以来添加一些自定义的类型转换器以及格式化器:
@Override
public void addFormatters(FormatterRegistry registry) {
// TODO Auto-generated method stub
}
最后,看一下其中的addInterceptors方法,顾名思义,它是来添加拦截器的:
@Override
public void addInterceptors(InterceptorRegistry registry) {
// TODO Auto-generated method stub
}
后面还有非常多的方法,这时,你会发现配置类只要实现了WebMvcConfigurer接口,那么你就得一个一个来实现其中的方法了,麻烦吗?!
于是,我们就要看看WebMvcConfigurer接口的源码了,如下图所示,我们不妨查看一下该接口的继承树(快捷键Ctrl + T),发现它下面有一个叫WebMvcConfigurerAdapter的适配器:
我们点进去看一下它的源码,发现它是一个实现了WebMvcConfigurer接口的抽象类,如下图所示:
该抽象类把WebMvcConfigurer接口中的方法都实现了,只不过每一个方法里面都是空的而已,所以,我们的配置类继承WebMvcConfigurerAdapter抽象类会比较好一点。这是因为如果我们的配置类实现了WebMvcConfigurer接口,那么其中的每一个方法我们都得来具体实现,但是呢,大多数情况下我们并不需要实现这么多方法!
于是,就要修改一下AppConfig配置类,让其继承WebMvcConfigurerAdapter抽象类,如下所示:
package com.sdehua.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@ComponentScan(value="com.sdehua",includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
},useDefaultFilters=false)
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter{
}
接下来,我们可以来个性化定制Spring MVC了,因为只须复写WebMvcConfigurerAdapter抽象类中的某些方法就行了。这里,我们不妨先来定制一下视图解析器,要想达成这一目的,只须复写WebMvcConfigurerAdapter抽象类中的configureViewResolvers方法:
package com.sdehua.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@ComponentScan(value="com.sdehua",includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
},useDefaultFilters=false)
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter{
// 定制视图解析器
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// TODO Auto-generated method stub
// super.configureViewResolvers(registry); 注释掉这行代码,因为其父类中的方法都是空的
// 如果直接调用jsp方法,那么默认所有的页面都从/WEB-INF/目录下开始找,即找所有的jsp页面
// registry.jsp();
/*
* 当然了,我们也可以自己来编写规则,比如指定一个前缀,即/WEB-INF/views/,再指定一个后缀,即.jsp,
* 很显然,此时,所有的jsp页面都会存放在/WEB-INF/views/目录下,自然地,程序就会去/WEB-INF/views/目录下面查找jsp页面了
*/
registry.jsp("/WEB-INF/views/", ".jsp");
}
}
为了达到测试的目的,我们在项目的webapp目录下新建一个WEB-INF/views目录,该目录是专门用于存放jsp页面的,然后再在WEB-INF/views目录新建一个jsp页面,例如success.jsp,其内容如下所示:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Success</title>
</head>
<body>
<h1>成功success!</h1>
</body>
</html>
接着,我们在HelloController中新增一个如下success方法,以便来处理success请求:
package com.sdehua.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.sdehua.service.HelloService;
@Controller
public class HelloController {
@Autowired
HelloService helloService;
@ResponseBody
@RequestMapping("/hello")
public String hello() {
String hello = helloService.sayHello("tomcat...");
return hello;
}
// 处理success请求
@RequestMapping("/success")
public String success() {
// 这儿直接返回"success",那么它就会跟我们视图解析器中指定的那个前后缀进行拼串,来到指定的页面
return "success";
}
}
当客户端发送过来一个suc请求,那么HelloController中的以上success方法就会来处理这个请求。由于该方法直接返回了一个success字符串,所以该字符串就会跟我们视图解析器中指定的那个前后缀进行拼串,并最终来到所指定的页面。
说人话就是,只要客户端发送过来一个suc请求,那么服务端就会响应/WEB-INF/views/目录下的success.jsp页面给客户端。
OK,我们启动项目,启动成功之后,在浏览器地址栏中输入http://localhost:8080/springmvc-annotation-liayun/success进行访问,效果如下图所示:
这说明我们已经成功定制了视图解析器。
然后,我们来定制一下静态资源的访问。假设我们项目的webapp目录下有一些静态资源,比如有一张图片,名字就叫test.jpg,打开发现它是一张美女图片:
此时,我们在项目的webapp目录下新建一个jsp页面,例如index.jsp,很显然,该页面是项目的首页,随即我们在首页中使用一个标签来引入上面那张美女图片,即在页面中引入静态资源:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<img alt="" src="test.jpg">
</body>
</html>
此时,在浏览器中访问项目的首页,你会发现上面那张美女图片压根就显示不出来,与此同时,Eclipse控制台会打印如下这样一个警告:
这是因为请求被Spring MVC拦截处理了,这样,它就得要找@RequestMapping注解中写的映射了,但是实际上呢,test.jpg是一个静态资源,它得交给Tomcat服务器去处理,因此,我们就得来定制静态资源的访问。
要想达成这一目的,我们只须复写WebMvcConfigurerAdapter抽象类中的configureDefaultServletHandling方法。
package com.sdehua.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@ComponentScan(value="com.sdehua",includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
},useDefaultFilters=false)
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter{
// 定制视图解析器
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// TODO Auto-generated method stub
// super.configureViewResolvers(registry); 注释掉这行代码,因为其父类中的方法都是空的
// 如果直接调用jsp方法,那么默认所有的页面都从/WEB-INF/目录下开始找,即找所有的jsp页面
// registry.jsp();
/*
* 当然了,我们也可以自己来编写规则,比如指定一个前缀,即/WEB-INF/views/,再指定一个后缀,即.jsp,
* 很显然,此时,所有的jsp页面都会存放在/WEB-INF/views/目录下,自然地,程序就会去/WEB-INF/views/目录下面查找jsp页面了
*/
registry.jsp("/WEB-INF/views/", ".jsp");
}
// 定制静态资源的访问
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
在以上configureDefaultServletHandling方法中调用configurer.enable(),其实就相当于我们以前在xml配置文件中写上
此时,我们重启项目,成功之后,再次来访问项目的首页,发现那张美女图片终于在浏览器页面中显示出来了,效果如下:
OK,静态资源就能访问了。
接着,定制拦截器,这还是稍微有点复杂的。
先编写一个拦截器,例如MyFirstInterceptor,要知道一个类要想成为拦截器,那么它必须得实现Spring MVC提供的HandlerInterceptor接口,如下所示:
package com.sdehua.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class MyFirstInterceptor implements HandlerInterceptor{
// 在页面响应以后执行
@Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
throws Exception {
// TODO Auto-generated method stub
System.out.println("*【afterCompletion】");
}
// 在目标方法运行正确以后执行
@Override
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
throws Exception {
// TODO Auto-generated method stub
System.out.println("*【postHandle】");
}
// 在目标方法运行之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse arg1, Object arg2) throws Exception {
// TODO Auto-generated method stub
System.out.println("*【preHandle】");
return true; // 返回true,表示放行(目标方法)
}
}
编写好以上拦截器之后,如果要是搁以前,那么我们就得在xml配置文件里面像下面这样配置该拦截器:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.meimeixia.controller.MyFirstInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
而现在我们只须复写WebMvcConfigurerAdapter抽象类中的addInterceptors方法就行了,就像下面这样:
package com.sdehua.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.sdehua.interceptor.MyFirstInterceptor;
@ComponentScan(value="com.sdehua",includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
},useDefaultFilters=false)
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter{
// 定制视图解析器
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// super.configureViewResolvers(registry); 注释掉这行代码,因为其父类中的方法都是空的
// 如果直接调用jsp方法,那么默认所有的页面都从/WEB-INF/目录下开始找,即找所有的jsp页面
// registry.jsp();
/*
* 当然了,我们也可以自己来编写规则,比如指定一个前缀,即/WEB-INF/views/,再指定一个后缀,即.jsp,
* 很显然,此时,所有的jsp页面都会存放在/WEB-INF/views/目录下,自然地,程序就会去/WEB-INF/views/目录下面查找jsp页面了
*/
registry.jsp("/WEB-INF/views/", ".jsp");
}
// 定制静态资源的访问
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
// 定制拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// super.addInterceptors(registry);
/*
* addInterceptor方法里面要传一个拦截器对象,该拦截器对象可以从容器中获取过来,也可以我们自己来new一个,
* 很显然,这儿我们是new了一个我们自定义的拦截器对象。
*
* 虽然创建出了一个拦截器,但是最关键的一点还是指示拦截器要拦截哪些请求,因此还得继续使用addPathPatterns方法来配置一下,
* 若在addPathPatterns方法中传入了"/**",则表示拦截器会拦截任意请求,而不管该请求是不是有任意多层路径
*/
registry.addInterceptor(new MyFirstInterceptor()).addPathPatterns("/**");
}
}
OK,我们来看一下以上定制的拦截器能不能生效。重启项目,项目启动成功之后,在浏览器地址栏中输入http://localhost:8080/springmvc-annotation-liayun/suc进行访问,即访问suc请求,发现Eclipse控制台打印出了如下内容:
这说明定制的拦截器生效了。
那么,剩余其他的对Spring MVC的个性化定制,就是照葫芦画瓢,很简单,而且还可以参考Spring MVC的官方文档,比方说要定制类型转换器,那么可以参考Spring MVC官方文档中的1.11.3. Type Conversion这一小节中的内容,主要是参考Java代码:
所以,常看官方写的文档,在每一小节中,上面都会先用一段Java代码告诉你应该复写哪个方法,下面则会告诉你复写之后相当于我们以前在xml配置文件中写的什么样的配置。
最后,我们总结一下,如果我们想要个性化定制Spring MVC,那么只须编写一个配置类来继承WebMvcConfigurerAdapter抽象类就行了,当然,前提是该配置类上得有@EnableWebMvc注解。这就是个性化定制Spring MVC的规则。