1

如何向一个WebApp引入Spring与Spring MVC

1

在Servlet 3.0环境中,容器(加载运行webapp的软件,如Tomcat)会在类路径中查找实现==javax.servlet.ServletContainerInitializer==接口的类(这一行为本质上是Java EE标准和协定所要求的,Tomcat是基于该协定的一种实现),如果能发现的话,就会用它来配置Servlet容器。

Spring提供了这个接口的实现,名为SpringServletContainerInitializer,因此一个引入的SringMVC的web项目在没有其它设置的情况下会被Tomcat找到SpringServletContainerInitializer。

SpringServletContainerInitializer

2

==SpringServletContainerInitializer==又会查找实现==WebApplicationInitializer==接口的类并调用其onStartup(ServletContext servletContext)方法,其中ServletContext对象由其负责将服务器生成的唯一的ServletContext实例传入。

WebApplicationInitializer

Interface to be implemented in Servlet 3.0+ environments in order to configure the ServletContext programmatically -- as opposed to (or possibly in conjunction with) the traditional web.xml-based approach.

ServletContext

Defines a set of methods that a servlet uses to communicate with its servlet container, for example,

ServletContextMethods

到目前位置,我们已经可以使用SpringMVC来增设Servlet了,虽然这看起来并不美观也不简便。代码如下所示。

package spittr.config;

import org.springframework.web.WebApplicationInitializer;
import spittr.web.AServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class SpittrWebAppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //增加一个Servelt 其中AServlet是Servlet接口的实现类,我的实现直接继承了HttpServlet
        ServletRegistration.Dynamic aServlet = servletContext.addServlet("AServlet", AServlet.class);
        //为AServlet增设映射路径,其作用等同于@WebServlet(urlPatterns={"/AServlet"})
        aServlet.addMapping(new String[]{"/AServlet"});
    }
}
package spittr.web;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=utf-8");

        PrintWriter writer = resp.getWriter();

        writer.write("我收到了你的GET");
    }
}

现在我们可以向浏览器直接访问AServlet

然而,这样的实现在美观和便利上远远不如使用Servlet3.0引入和更新的@WebServlet等机制。

并且完全没有涉及Spring和Spring MVC,只是按照Servlet3.0的标准的一种添加Servlet的方式罢了。

那么接下来我们就要开始引入Spring和Spring MVC了。

3

第一步肯定是引入Spring,也即引入一个Spring的容器。

这很简单,在onStartup中实例化一个ApplicationContext的实例即可。查询ApplicationContext的javadoc,看到目前所有的ApplicationContext实现类:

All Known Implementing Classes:
AbstractApplicationContext, AbstractRefreshableApplicationContext, AbstractRefreshableConfigApplicationContext, AbstractRefreshableWebApplicationContext, AbstractXmlApplicationContext, AnnotationConfigApplicationContext, AnnotationConfigWebApplicationContext, ClassPathXmlApplicationContext, FileSystemXmlApplicationContext, GenericApplicationContext, GenericGroovyApplicationContext, GenericWebApplicationContext, GenericXmlApplicationContext, GroovyWebApplicationContext, ResourceAdapterApplicationContext, StaticApplicationContext, StaticWebApplicationContext, XmlWebApplicationContext

而我们打算使用基于Java代码的配置并开启基于注解的自动扫描,同时应用场景为webapp,所以应该使用AnnotationConfigWebApplicationContext实现类。

综上所述,可以得到如下代码:

package spittr.config;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.WebApplicationInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

public class SpittrWebAppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);
    }
}
@Configuration
@ComponentScan("spittr.web")
public class AppConfig {
}

至此,我们已经在这个webapp中集成了Spring容器,从理论上讲,我们应该可以对一个Servlet标注@Controller后使其自动被注册和使用。但是由于@RequestMapping我们还不知道能不能用,实际上无法对其进行测试(因为即便将服务器注册到了Spring容器中,我们也无法为它配置映射路径)。

那么现在就该去解决@RequestMapping了。

4

javadoc:@RequestMapping

@RequestMapping javadoc这一注解做了如下解读

Annotation for mapping web requests onto methods in request-handling classes with flexible method signatures.

Both Spring MVC and Spring WebFlux support this annotation through a RequestMappingHandlerMapping and RequestMappingHandlerAdapter in their respective modules and package structure. For the exact list of supported handler method arguments and return types in each, please use the reference documentation links below:

Note: This annotation can be used both at the class and at the method level. In most cases, at the method level applications will prefer to use one of the HTTP method specific variants @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, or @PatchMapping.

NOTE: When using controller interfaces (e.g. for AOP proxying), make sure to consistently put all your mapping annotations - such as @RequestMapping and @SessionAttributes - on the controller interface rather than on the implementation class.

其中最重要的在第二段,它说明了Spring MVC通过使用RequestMappingHandlerMappingRequestMappingHandlerAdapter 得以支持@RequestMappin注解。

javadoc:RequestMappingHandlerMapping

javadoc:RequestMappingHandlerAdapter

可以发现,这两个类都是可以被实例化的,且构造器不需要参数。

既然如此,我们可以试着在AppConfig中配置这两个类。

@Configuration
@ComponentScan("spittr.web")
public class AppConfig {
    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter(){
        return new RequestMappingHandlerAdapter();
    }
    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping(){
        return new RequestMappingHandlerMapping();
    }
}

然后使用带@Controller和@RequestMapping的类

package spittr.web;

@Controller
@RequestMapping("/BServlet")
public class BServlet{
    @RequestMapping(method = RequestMethod.GET)
    public void doGet() {
        System.out.println("BServlet:我收到了你的GET");
    }
}

不过测试结果是糟糕的,我们没有如愿实现访问BServlet。

失败的原因没有官方文档直接告知,但结合之后进一步的学习,不难猜测理由应该是:我们AppConfig的Spring-beans容器其实没有和Servlet容器结合起来。我们只是在onStartUp方法中实例化了一个Spring-beans容器,甚至可以认为在方法的生命周期结束之后,这个实例就直接没了。如若真的如此,我们就连实际上把Spring集成到这个WebApp中都没有做到,怎么可能做到开启Spring MVC注解呢。

5

事已至此,就只能阅读官方文档了。官方文档

开门见山地:

Spring MVC, as many other web frameworks, is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing, while actual work is performed by configurable delegate components. This model is flexible and supports diverse workflows.

→Spring MVC围绕一个前线控制器模式(front controller pattern)而设计,在这种模式下一个核心Servlet,也就是DispatchereServlet(由Spring实现的Servlet类),会为处理客户端请求提供了算法,而真正的工作(处理请求)由可配置的代理组件来执行。

因此可以认为,要充分利用SpringMVC,必然要加载SpringMVC自行实现的Servlet类:org.springframework.web.servlet.DispatcherServlet

org.springframework.web.servlet.DispatcherServlet

官方文档给出了一段初始化代码:

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        //AppConfig是自定义的带@Configuration注解的类
        ac.register(AppConfig.class);
        ac.refresh();

        // Create and register the DispatcherServlet
        // 将Spring容器与DispatcherServlet绑定
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}

这段代码的前半部分,我们是很熟悉的。第三章就做过。

这段代码的后半部分其实没有什么新意,但下半部分的第一行非常关键

DispatcherServlet servlet = new DispatcherServlet(ac);

接受一个AnnotationConfigWebApplicationContext作为构造器参数!这实际上解决了我们在第四章测试失败后反思的可能的疑惑——我们配置的Spring容器实际上并没有和tomcat融合起来。

那么现在,将官方代码中的ac换成我们自己的,是不是就能成功了呢?不妨一试

public class SpittrWebAppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);

        DispatcherServlet dispatcher = new DispatcherServlet(ac);
        ServletRegistration.Dynamic d = servletContext.addServlet("dispatcher", dispatcher);
        d.setLoadOnStartup(1);
        d.addMapping("/");
    }
}
/*
AppConfig
BServlet
相较之前完全没有变化,所以不展示
*/

结果是喜人的,我们尝试成功了。可以看到输出BServlet:我收到了你的GET

官方文档进一步说明:

The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification by using Java configuration or in web.xml. In turn, the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.

The following example of the Java configuration registers and initializes the DispatcherServlet, which is auto-detected by the Servlet container (see Servlet Config):

这段话应该分成这两个部分:

  • The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification by using Java configuration or in web.xml.The following example of the Java configuration registers and initializes the DispatcherServlet, which is auto-detected by the Servlet container (see Servlet Config):

    这一部分上来先说,DispatcherServlet就像任何Servlet一样,也是需要做好声明和映射的。下面的代码介绍了使用Servlet container提供的自动探测注册功能来注册和初始化DispatcherServlet。这里所谓的Servlet container的自动探测,其实就是指之前提到的1,2两个阶段。

  • In turn, the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.

    这一部分说,DispatcherServlet被配置注册好之后,也可以反过来使用Spring配置来发现和委派为它为请求映射,视图渲染,异常处理所需要的组件。
    那么,DispatcherServlet要如何反过来配置它自己的组件呢?带着这一疑问,我们继续往下看。

6

官方文档紧接着提到了一个WebApplicationInitializer的Spring实现类AbstractAnnotationConfigDispatcherServletInitializer,它可以避免直接使用ServletContext(它自己已经用了),通过重写特定的方法完成配置。

In addition to using the ServletContext API directly, you can also extendAbstractAnnotationConfigDispatcherServletInitializer and override specific methods (see the example under Context Hierarchy).

跟随Context Hierarchy超链接一探究竟。先放上example code:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { App1Config.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/app1/*" };
    }
}

再看文字说明

DispatcherServlet expects a WebApplicationContext (an extension of a plain ApplicationContext) for its own configuration.

DispatcherServlet为它自己的配置需要一个WebApplicationContext(ApplicationContext的子接口)即一个Spring容器的配置实现类。

WebApplicationContext has a link to the ServletContext and the Servlet with which it is associated. It is also bound to the ServletContext such that applications can use static methods on RequestContextUtils to look up the WebApplicationContextif they need access to it.

一个Spring容器与ServletContext和与它共生的Servlet又关联。这个Spring容器因为绑定ServletContext,所以也可以通过类RequestContextUtils的静态方法去得到。

For many applications, having a single WebApplicationContext is simple and suffices. It is also possible to have a context hierarchy where one root WebApplicationContext is shared across multiple DispatcherServlet (or other Servlet) instances, each with its own child WebApplicationContext configuration. See Additional Capabilities of the ApplicationContext for more on the context hierarchy feature.

绝大部分应用来说,一个Spring容器就够用了。但也可以有一个有层级的容器结构——一个根Spring容器在多个(全部)Servlet实例中共享,同时每个Servlet实例也有自己的WebApplicationContext配置。

Java EE和Servlet3.0标准的Servlet接口其实是不支持Servlet实例共生一个ApplicationContext的,因为后者毕竟是Spring的专属。所以这里的Servlet实例考虑为像DispatcherServlet这样由Spring实现并提供的类,而不包括用户自定义的符合Java EE和Servlet3.0标准的Servlet接口的Servlet。

The root WebApplicationContext typically contains infrastructure beans, such as data repositories and business services that need to be shared across multiple Servlet instances. Those beans are effectively inherited and can be overridden (that is, re-declared) in the Servlet-specific child WebApplicationContext, which typically contains beans local to the given Servlet.

在层级话的Spring容器结构中,根Spring容器通常包含基础设施的组件,比如数据持久化层,商业服务层这种需要在各种Servlet中共享的组件。这些组件能够被有效地继承地同时,也可以被在Servlet相关的子Spring容器中被重新配置,使得组件可以针对给定的Servlet因地制宜。

到这里再回看代码。

    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

显然,这里的RootConfig.class是用户自定义的带@Configuration注解的Spring容器配置类,用以实现根Spring容器。

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { App1Config.class };
    }

这个就是AbstractAnnotationConfigDispatcherServletInitializer默认实现的那个DispatcherServlet的伴生Spring容器配置。

    protected String[] getServletMappings() {
        return new String[] { "/app1/*" };
    }

这个则是确定AbstractAnnotationConfigDispatcherServletInitializer默认实现的那个DispatcherServlet所要管理的request URI映射。

至此1.1.1 Context Hierarchy结束,我们之前就是根据超链接跳到这一章节的,这一章节结束后,我们返回之前的位置继续阅读文档。

发现紧接着就又是1.1.1 Context Hierarchy,直接跳过读下一章。

7

1.1.2. Special Bean Types

The DispatcherServlet delegates special beans to process requests and render the appropriate responses. By “special beans” we mean Spring-managed Object instances that implement framework contracts. Those usually come with built-in contracts, but you can customize their properties and extend or replace them.

1.1.3. Web MVC Config

Applications can declare the infrastructure beans listed in Special Bean Types that are required to process requests. The DispatcherServlet checks the WebApplicationContext for each special bean. If there are no matching bean types, it falls back on the default types listed in DispatcherServlet.properties.

In most cases, the MVC Config is the best starting point. It declares the required beans in either Java or XML and provides a higher-level configuration callback API to customize it.

这两个部分回答了我们的问题——DispatcherServlet要如何反过来配置它自己的组件——DispatcherServlet将会搜索它可以访问的WebApplicationContext(这包括根Spring容器和它自己伴生的子Spring容器)来查找每个special bean——即被委派来处理请求渲染回应等工作的组件——的设置。如果没有的话,它将使用默认的,保存在DispatcherServlet.properties中的设定。
很好理解的,我们之前所写的AppConfig中的两个Bean,它们是那么的基础——由Spring提供和实现,我们只是new出来什么自定义也没有——以至于使用DispatcherServlet的默认配置也不会更糟糕。所以我们去掉之前的AppConfig的配置,仅仅留下一个空的AppConfig,其它代码不变。

再次测试,仍然能够收到BServlet的输出。

总结

到这里,对于如何将Spring和Spring MVC集成到一个WebApp中的过程以及为什么可以集成进来已经分析得差不多了。

更进一步得学习Spring MVC,就继续仔细阅读官方文档吧!


阳光号
129 声望5 粉丝