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

39次阅读

共计 12860 个字符,预计需要花费 33 分钟才能阅读完成。

如何向一个 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,

到目前位置,我们已经可以使用 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:

  • Spring MVC Method Arguments and Return Values
  • Spring WebFlux Method Arguments and Return Values

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 中的设定。

总结

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

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

正文完
 0