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 {@Overridepublic 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,如下所示:
@Overridepublic void onStartup(ServletContext servletContext) throws ServletException {// 调用父类(即AbstractContextLoaderInitializer)的onStartup方法,先把根容器创建出来super.onStartup(servletContext);// 往ServletContext中注册DispatcherServletregisterDispatcherServlet(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方法来创建一个DispatcherServletFrameworkServlet 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) {// 创建一个DispatcherServletreturn 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方法,如下所示,而且我们知道该方法是来创建根容器的:
@Overrideprotected 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方法:
@Overrideprotected 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的配置文件的哟~,* 还记得吗?然后,就能创建出一个父容器了*/@Overrideprotected Class<?>[] getRootConfigClasses() {return null;}/** getServletConfigClasses方法:它是来获取web容器的配置类的,* 该配置类就类似于我们以前经常写的Spring MVC的配置文件,* 而且我们以前是利用前端控制器来加载Spring MVC的配置文件的哟~,* 你还记得吗?然后,就能创建出一个子容器了*/@Overrideprotected 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引擎来解析的。*/@Overrideprotected String[] getServletMappings() {return new String[] {"/"};}}
由于我们还需要在getRootConfigClasses和getServletConfigClasses这俩方法中指定两个配置类的位置,所以我们来创建上两个配置类,分别如下:
- 根容器的配置类,例如RootConfig ```java package com.sdehua.config;
public class RootConfig {
}
- web容器的配置类,例如AppConfig```javapackage 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;@Servicepublic 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;@Controllerpublic class HelloController {@AutowiredHelloService 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的配置文件的哟~,* 还记得吗?然后,就能创建出一个父容器了*/@Overrideprotected Class<?>[] getRootConfigClasses() {return new Class<?>[] {RootConfig.class};}/** getServletConfigClasses方法:它是来获取web容器的配置类的,* 该配置类就类似于我们以前经常写的Spring MVC的配置文件,* 而且我们以前是利用前端控制器来加载Spring MVC的配置文件的哟~,* 你还记得吗?然后,就能创建出一个子容器了*/@Overrideprotected 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引擎来解析的。*/@Overrideprotected 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 {@Overridepublic 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)@EnableWebMvcpublic 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)@EnableWebMvcpublic class AppConfig implements WebMvcConfigurer{@Overridepublic void addArgumentResolvers(List<HandlerMethodArgumentResolver> arg0) {// TODO Auto-generated method stub}@Overridepublic void addCorsMappings(CorsRegistry arg0) {// TODO Auto-generated method stub}@Overridepublic void addFormatters(FormatterRegistry arg0) {// TODO Auto-generated method stub}@Overridepublic void addInterceptors(InterceptorRegistry arg0) {// TODO Auto-generated method stub}@Overridepublic void addResourceHandlers(ResourceHandlerRegistry arg0) {// TODO Auto-generated method stub}@Overridepublic void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> arg0) {// TODO Auto-generated method stub}@Overridepublic void addViewControllers(ViewControllerRegistry arg0) {// TODO Auto-generated method stub}@Overridepublic void configureAsyncSupport(AsyncSupportConfigurer arg0) {// TODO Auto-generated method stub}@Overridepublic void configureContentNegotiation(ContentNegotiationConfigurer arg0) {// TODO Auto-generated method stub}@Overridepublic void configureDefaultServletHandling(DefaultServletHandlerConfigurer arg0) {// TODO Auto-generated method stub}@Overridepublic void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> arg0) {// TODO Auto-generated method stub}@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> arg0) {// TODO Auto-generated method stub}@Overridepublic void configurePathMatch(PathMatchConfigurer arg0) {// TODO Auto-generated method stub}@Overridepublic void configureViewResolvers(ViewResolverRegistry arg0) {// TODO Auto-generated method stub}@Overridepublic void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> arg0) {// TODO Auto-generated method stub}@Overridepublic void extendMessageConverters(List<HttpMessageConverter<?>> arg0) {// TODO Auto-generated method stub}@Overridepublic MessageCodesResolver getMessageCodesResolver() {// TODO Auto-generated method stubreturn null;}@Overridepublic Validator getValidator() {// TODO Auto-generated method stubreturn null;}}
发现这个WebMvcConfigurer接口里面定义了好多的方法!如下图所示:
实现该接口之后,我们就得来实现其里面的每一个方法了,这就是来定制Spring MVC。
来看一下其中的configurePathMatch方法,该方法的作用就是来配置路径映射规则的:
@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {// TODO Auto-generated method stub}
再来看一下其中的configureAsyncSupport方法,它的作用是来配置是否开启异步支持的:
@Overridepublic void configureAsyncSupport(AsyncSupportConfigurer configurer) {// TODO Auto-generated method stub}
再再来看一下其中的configureDefaultServletHandling方法,它的作用是来配置是否开启静态资源的。我们不妨实现一下该方法,如下所示:
@Overridepublic void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {// TODO Auto-generated method stubconfigurer.enable()}
实现以上方法之后,效果就相当于我们以前在xml配置文件中写上
继续往下看吧,来看一下其中的addFormatters方法,它的作用是可以来添加一些自定义的类型转换器以及格式化器:
@Overridepublic void addFormatters(FormatterRegistry registry) {// TODO Auto-generated method stub}
最后,看一下其中的addInterceptors方法,顾名思义,它是来添加拦截器的:
@Overridepublic 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)@EnableWebMvcpublic 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)@EnableWebMvcpublic class AppConfig extends WebMvcConfigurerAdapter{// 定制视图解析器@Overridepublic 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;@Controllerpublic class HelloController {@AutowiredHelloService 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)@EnableWebMvcpublic class AppConfig extends WebMvcConfigurerAdapter{// 定制视图解析器@Overridepublic 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");}// 定制静态资源的访问@Overridepublic 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{// 在页面响应以后执行@Overridepublic void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)throws Exception {// TODO Auto-generated method stubSystem.out.println("*【afterCompletion】");}// 在目标方法运行正确以后执行@Overridepublic void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)throws Exception {// TODO Auto-generated method stubSystem.out.println("*【postHandle】");}// 在目标方法运行之前执行@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse arg1, Object arg2) throws Exception {// TODO Auto-generated method stubSystem.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)@EnableWebMvcpublic class AppConfig extends WebMvcConfigurerAdapter{// 定制视图解析器@Overridepublic 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");}// 定制静态资源的访问@Overridepublic void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {configurer.enable();}// 定制拦截器@Overridepublic 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的规则。
