Servlet3.0与SpringMVC整合

Servlet3.0与SpringMVC的整合,永久告别web.xml的配置文件啦~~

创建maven-web工程

如果大家对idea创建maven-web工程不熟悉的话,去看我的另一篇博客idea创建maven-web工程

打开pom.xml文件

添加两个依赖,SpringMVC和Servlet的依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>4.1.9.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>  <!-- 3.0以上的版本都可以-->
    <scope>provided</scope>   <!-- 因为Tomcat容器中有servlet的jar,所以打成war包的时候,不需要这个-->
</dependency>

现在开始整合SpringMVC开发。打开Spring的官网。

找到相关的文档处Spring-web

我们能发现在servlet3.0以后,SpringMVC的官网推不再使用web.xml开发,直接使用java的配置类来替代web.xml文件。

我们只需要编写一个类实现WebApplicationInitializer接口,就相当于web.xml文件了。

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);
        ac.refresh();

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}

容器启动后,会执行这个实现WebApplicationInitializer接口的实现类。然后执行onStartup()方法。

在该方法中,创建了注解式配置应用上下文,传入spring的配置文件。然后向该容器中注入DispatcherServlet,来完成SpringMVC的前端控制器。

重点:

那为啥实现了WebApplicationInitializer接口的实现类,在容器启动时调用呢?

首先打开这个接口:

public interface WebApplicationInitializer {

    /**
     * Configure the given {@link ServletContext} with any servlets, filters, listeners
     * context-params and attributes necessary for initializing this web application. See
     * examples {@linkplain WebApplicationInitializer above}.
     * @param servletContext the {@code ServletContext} to initialize
     * @throws ServletException if any call against the given {@code ServletContext}
     * throws a {@code ServletException}
     */
    void onStartup(ServletContext servletContext) throws ServletException;

}

只有一个方法,看不出什么头绪。但是,在这个包下有另外一个类,SpringServletContainerInitializer。它的实现如下:

@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;
        }

        AnnotationAwareOrderComparator.sort(initializers);
        servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers);

        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }

}

这个类就比较有意思了,先不管其他的,读一下这段代码,可以得到这样的意思。

先判断webAppInitializerClasses这个Set是否为空。如果不为空的话,找到这个set中不是接口,不是抽象类,并且是WebApplicationInitializer接口实现类的类,将它们保存到list中。当这个list为空的时候,直接return。不为空的话就按照一定的顺序排序,并将它们按照一定的顺序实例化。调用其onStartup方法执行。到这里,就可以解释WebApplicationInitializer实现类的工作过程了。

但是,在web项目运行的时候,SpringServletContainerInitializer这个类又是怎样被调用的呢?

再看被它实现的ServletContainerInitializer这个接口
它只有一个接口方法onStartup(),通过它就可以解释SpringServletContainerInitializer是如何被调用的。它的内容如下:

package javax.servlet;

import java.util.Set;

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;

}

首先,这个接口是javax.servlet下的,官方的解释是这样的:

为了支持可以不使用web.xml。提供了ServletContainerInitializer,它可以通过SPI机制。

当启动web容器的时候,会自动到项目中添加的jar包下找到META-INF/services/javax.servlet.ServletContainerInitializer文件。

它的内容为ServletContainerInitializer实现类的全路径,将它们实例化。

既然这样的话,那么SpringServletContainerInitializer作为ServletContainerInitializer的实现类,它的jar包下也应该有相应的文件。

哈,现在就可以解释清楚了。首先,SpringServletContainerInitializer作为ServletContainerInitializer的实现类,通过SPI机制,在web容器加载的时候会自动的被调用。(这个类上还有一个注解@HandlesTypes,它的作用是将感兴趣的一些类注入到ServletContainerInitializer), 而这个类的方法又会扫描找到WebApplicationInitializer的实现类,调用它的onStartup方法,从而起到启动web.xml相同的作用。


等价于web.xml文件:

<web-app>

    <!-- 注册这个监听器来注入Spring的父容器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>

    <!-- 注册这个Servlet来注入SpringMVC的子容器-->
    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>

总结:

1)servlet3.0以后的Web容器在启动的时候,会扫描每个jar包下的META-INF/services/javax.servlet.ServletContainerInitializer文件

2)加载这个文件指定的类(实现了ServletContainerInitializer接口类),并调用这个接口的onStartup()方法。
void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;

3)现在,我们在pom.xml文件中加入了spring-mvc的依赖,其中有一个jar:

在该META-INF/services/javax.servlet.ServletContainerInitializer文件下,有一个:org.springframework.web.SpringServletContainerInitializer的全路径。

打开SpringServletContainerInitializer类。

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
  ···
}

该类上面有一个@HandlesTypes注解,该注解会在web容器启动的时候,传入我们感兴趣的类型,这里会传入WebApplicationInitializer的类型。

然后在该类的onStartup()方法中:

        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);
                    }
                }
            }
        }

这里的webAppInitializerClasses就是传入的我们感兴趣的类型,判断该传入的类型,在不是接口,不是抽象类,而且是WebApplicationInitializer类型的时候,实例化该传入的类型。

for (WebApplicationInitializer initializer : initializers) {
    initializer.onStartup(servletContext);
}

最后调用WebApplicationInitializer的
onStartup(ServletContext servletContext) throws ServletException;


我们可以直接实现WebApplicationInitializer接口,像Spring官网一样整合SpringMVC。

但是,这里,我们来看看WebApplicationInitializer的继承体系
会发现有三个抽象类:
AbstractContextLoaderInitializer
AbstractDispatcherServletInitializer
AbstractAnnotationConfigDispatcherServletInitializer


AbstractContextLoaderInitializer:

public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {

    /** Logger available to subclasses */
    protected final Log logger = LogFactory.getLog(getClass());


    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        registerContextLoaderListener(servletContext);
    }

    protected void registerContextLoaderListener(ServletContext servletContext) {
        WebApplicationContext rootAppContext = createRootApplicationContext();
        if (rootAppContext != null) {
            servletContext.addListener(new ContextLoaderListener(rootAppContext));
        }
        else {
            logger.debug("No ContextLoaderListener registered, as " +
                    "createRootApplicationContext() did not return an application context");
        }
    }

    protected abstract WebApplicationContext createRootApplicationContext();

}

这里,我们可以看到:首先执行onStartup(),然后执行registerContextLoaderListener().

在然后执行registerContextLoaderListener中,调用createRootApplicationContext()方法来获取Spring的根容器。该方法需要我们来实现。

然后将我们创建的根容器注入到ServletContext中。


AbstractDispatcherServletInitializer:

public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {
    public static final String DEFAULT_SERVLET_NAME = "dispatcher";

    public AbstractDispatcherServletInitializer() {
    }

    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        this.registerDispatcherServlet(servletContext);
    }

    protected void registerDispatcherServlet(ServletContext servletContext) {
        String servletName = this.getServletName();
        Assert.hasLength(servletName, "getServletName() must not return empty or null");
        WebApplicationContext servletAppContext = this.createServletApplicationContext();
        Assert.notNull(servletAppContext, "createServletApplicationContext() did not return an application context for servlet [" + servletName + "]");
        DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext);
        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);
        registration.addMapping(this.getServletMappings());
        registration.setAsyncSupported(this.isAsyncSupported());
        Filter[] filters = this.getServletFilters();
        if (!ObjectUtils.isEmpty(filters)) {
            Filter[] var7 = filters;
            int var8 = filters.length;

            for(int var9 = 0; var9 < var8; ++var9) {
                Filter filter = var7[var9];
                this.registerServletFilter(servletContext, filter);
            }
        }

        this.customizeRegistration(registration);
    }

第一,创建Spring的WebApplicationContext容器。

protected abstract WebApplicationContext createServletApplicationContext();

需要我们自己实现,这个方法,返回一个WebApplicationContext容器。

第二,帮我们创建了一个dispatcherServlet,然后注入到ServletContext中。

第三,registration.addMapping(this.getServletMappings());添加映射,Filter[] filters = this.getServletFilters();注入过滤器等。

protected abstract String[] getServletMappings();

映射器也需要我们重写。


AbstractAnnotationConfigDispatcherServletInitializer:

注解版注册DispatcherServlet的版本

public abstract class AbstractAnnotationConfigDispatcherServletInitializer extends AbstractDispatcherServletInitializer {
    public AbstractAnnotationConfigDispatcherServletInitializer() {
    }

    protected WebApplicationContext createRootApplicationContext() {
        Class<?>[] configClasses = this.getRootConfigClasses();
        if (!ObjectUtils.isEmpty(configClasses)) {
            AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
            rootAppContext.register(configClasses);
            return rootAppContext;
        } else {
            return null;
        }
    }

    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext servletAppContext = new AnnotationConfigWebApplicationContext();
        Class<?>[] configClasses = this.getServletConfigClasses();
        if (!ObjectUtils.isEmpty(configClasses)) {
            servletAppContext.register(configClasses);
        }

        return servletAppContext;
    }

    protected abstract Class<?>[] getRootConfigClasses();

    protected abstract Class<?>[] getServletConfigClasses();
}

里面就重写了两个方法:

createRootApplicationContext():创建配置类的根容器
重写getRootConfigClasses():传入一个配置类。

createServletApplicationContext():创建Web的ioc容器
重写getServletConfigClasses():获取Web版的配置类。


总结:

以注解方法来启动SpringMVC的话:继承AbstractAnnotationConfigDispatcherServletInitializer;

实现抽象方法指定DispatcherServlet的配置信息。

Servlet3.0与SpringMVC的整合

1) 在src下的main下面的java里面写我们的源代码:

如果还不知道怎么配置,看我的idea创建maven-web的博客,最后有说明。

创建com.liuzhuo包,并在该包下面,创建MyWebAppInitializer类,继承AbstractAnnotationConfigDispatcherServletInitializer:

//Web容器启动的时候创建对象;调用方法来初始化容器以前的前端控制器
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    //获取根容器的配置类;(Spring的配置文件)父容器。
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{RootConfig.class};
    }

    //获取Web容器的配置类(SpringMVC配置文件)子容器
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{AppConfig.class};
    }

    //获取DispatchServlet的映射信息
    // /:拦截所有请求(包括静态资源(XX.js,XX.png)),但是不包括*.jsp
    // /*:拦截所有请求;连*.jsp页面都拦截;jsp页面需要Tomcat的jsp引擎解析的。
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

2) 在com.liuzhuo.config包下,创建RootConfig、AppConfig类:

@ComponentScan(value = "com.liuzhuo", excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Controller.class})
})
//Spring的根容器(父容器),扫描排除含有Controller注解的bean对象
public class RootConfig {
}

@ComponentScan(value = "com.liuzhuo", includeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Controller.class})
}, useDefaultFilters = false)
//SpringMVC的子容器,只扫描含有Controller注解的Bean对象
//禁用默认的规则(扫描所有):useDefaultFilters = false
public class AppConfig {
}

这样配置,是根据Spring的官方给的建议:

父容器: 配置@service、@repository,数据连接池,事务管理等。
子容器:配置@Controller层,视图解析器,映射配置。

3)在com.liuzhuo.service包下,创建HelloService:

@Service
public class HelloService {

    public String sayHello(String name) {
        return "Hello:" + name;
    }
}

4) 在com.liuzhuo.controller包下,创建HelloController:

@Controller
public class HelloController {

    @Autowired
    private HelloService helloService;

    @ResponseBody
    @RequestMapping("/hello")
    public String hello() {
        String hello = helloService.sayHello("tomcat·····");
        return hello;
    }
}

整体的项目结构:

5) 启动项目,运行

浏览器输入:
http://localhost:8080/hello

到现在为止,Servlet3.0与SpringMVC整合完成。

定性配置SpringMVC

打开官网,查看 MVC Config章节。

开启MVC的高级功能,需要@EnableWebMvc注解。

比如:

@Configuration
@EnableWebMvc
public class WebConfig {
}

相等于xml文件:<mvc:annotation-driven/>

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven/>

</beans>

只有开启了@EnableWebMvc或者<mvc:annotation-driven/>才能定制SpringMVC的其他信息。

在官网中,我们使用配置实现 WebMvcConfigurer 接口,然后实现其中的所有方法来定制其他组件(视图解析器、拦截器、过滤器等)。

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    // Implement configuration methods...
}

但是,这样,我们就必须实现其中的所有方法了,为了方便,我们可以继承:WebMvcConfigurerAdapter 适配器,其中所有的方法帮我们实现了,只是返回空方法。然后我们通过重写自己想要定制的方法来完成自身的需求。

定制视图解析器

1)改写我们的AppConfig配置类:

添加@EnableWebMvc、并且继承WebMvcConfigurerAdapter抽象类

@ComponentScan(value = "com.liuzhuo", includeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Controller.class})
}, useDefaultFilters = false)
//SpringMVC的子容器,只扫描含有Controller注解的Bean对象
//禁用默认的规则(扫描所有):useDefaultFilters = false
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter {



}

2)添加自定义的视图解析器

都是在AppConfig类中,直接重写方法

    //配置视图解析器
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        //定制jsp的视图解析器
        //参数一:视图的前缀路径
        //参数二:视图的后缀名
        registry.jsp("WEB-INF/views/", ".jsp");

    }

3)在HelloController中

    @RequestMapping("/suc")
    public String sucess() {
        return "sucess";
    }

4) 在WEB-INF/views下,添加一个sucess.jsp的文件

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>sucess!</h1>
</body>
</html>

5) 运行项目

在浏览器中输入:http://localhost:8080/suc

成功定制了jsp的视图解析器了。


定制静态资源

没有配置之前:

1)在webapp根目录下:

放入一张图片,随便一张就行。

2)在index.jsp中,添加img标签

3) 启动项目

浏览器中输入:http://localhost:8080

发现,我们的照片出不来,这是为啥呢?这是因为,我们的静态资源也会被DispatchServlet拦截。

在控制台中能发现:

30-Oct-2018 15:20:00.904 警告 [http-nio-8080-exec-4] org.springframework.web.servlet.PageNotFound.noHandlerFound No mapping found for HTTP request with URI [/gakki.jpg] in DispatcherServlet with name 'dispatcher'

发现 /gakki.jpg 也被DispatchServlet拦截了。

现在,我们需要不让DispatchServlet拦截我们的静态资源,使用Tomcat容器来加载静态资源。

4)在AppConfig下:

    //静态资源访问
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        //默认是匹配路径:/**
        configurer.enable();
    }

相等于xml中的:<mvc:default-servlet-handler/>

5) 启动项目


定制拦截器

1)在com.liuzhuo.interceptors在,创建MyInterceptor,实现HandlerInterceptor接口

//自定义的拦截器
public class MyInterceptor implements HandlerInterceptor {

    //拦截之前调用
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        //放行
        System.out.println("preHandle·······");
        return true;
    }

    //处理之后调用
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

        System.out.println("postHandle·······");
    }

    //成功处理后调用
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        System.out.println("afterCompletion·······");
    }

}

2)在AppConfig类下:

    //添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
    }

3) 启动项目

在浏览器中输入:http://localhost:8080/suc

拦截成功。

如果还想定制其他功能,请查看Spring的官方文档


  目录