入门-Spring整合web与三层架构回顾

从 0 开始深入学习 Spring 专栏目录总览

终于到了 SpringFramework 学习的下一站,咱要对 SpringFramework 中的一个非常非常重要,重要到好多人都把它单独拿出来跟 Spring 放到一起论述的,那就是 SpringWebMvc 。作为 Web 部分的入门章,小册要回顾一下三层架构,然后学习一下 SpringFramework 如何整合 Web 开发。

1. MVC三层架构【回顾】

关于三层架构的演进过程,小册就不展开讲解了,咱直接来回顾 MVC 三层架构的最终形态就 OK ,毕竟前面的过程都是不完全形态,也没必要再调动起回忆了。

1.1 MVC

MVC 想必各位都很熟悉了,MVC 的三个字母分别代表 Model View Controller ,它们在一个应用中的地位和职责应该是下面图的样子:

入门-Spring整合web与三层架构回顾

如果回忆不起来的小伙伴一定要加深印象,后面咱学习 SpringWebMvc 的时候还会用它的。

1.2 三层架构

JavaEE 中的三层架构,是为了实现代码逻辑解耦的经典设计模式,这个想必咱也都很熟悉了。三层架构分别为 web 层、service 层、dao 层,它们的职责及调用逻辑可以用下图表示:

入门-Spring整合web与三层架构回顾

这个虽说是老生常谈了,接下来的学习和实际的项目开发中也都会用到,所以小伙伴们还是加深一下印象哈。

2. Spring整合web开发【掌握】

接下来的内容就是 SpringFramework 如何整合 web 工程进行实际的开发了。注意现在还没有涉及到 mvc 的东西,所以小伙伴们先不要着急,先回顾一下 Servlet 的东西也未尝不可。

接下来,咱分为基于 web.xml 和 Servlet 3.0 规范的方式,分别讲解 SpringFramework 如何整合 web 开发。

2.1 工程创建

先把项目工程创建出来吧。还是照常,使用 Maven 创建一个新的模块 spring-04-web 就好啦,本章的所有源码也都在这个工程下。

创建出来之后,不要忘了先把打包方式改为 war :

    <groupId>com.linkedbear.spring</groupId>
    <artifactId>spring-04-web</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

之后是添加依赖的部分,既然是 SpringFramework 整合 web ,那必然要多导入一个依赖啦:

<properties>
    <spring.framework.version>5.2.8.RELEASE</spring.framework.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

当然,不要忘记导 Servlet API 的依赖哈。

导入完成后,照例将其先部署到 Tomcat 中:(下面是 IDEA 的配置方法)

入门-Spring整合web与三层架构回顾

入门-Spring整合web与三层架构回顾

配置完成后,启动 Tomcat ,无任何报错则说明一切配置正确,可以接着往下进行了。

2.2 基于web.xml的web整合

首先,咱来准备一点基础代码。

2.2.1 基础代码准备

基础代码也不用折腾的很复杂,只需要一个配置文件,一个 Service 即可,不用写的很麻烦。

public class UserService {
    
    public String get() {
        return "hahaha";
    }
}

spring-web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
    <bean class="com.linkedbear.spring.xml.a_basic.UserService"/>
</beans>

这样基本代码就算准备完成了。

2.2.2 web.xml

下面咱先来编写 web.xml 的基本内容。

上面咱导依赖的时候使用的 Servlet API 是 3.1 ,所以这里咱也是用 3.1 版本即可。先把外壳写上:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

</web-app>

接下来里头该配置啥呢???小册打算先让小伙伴思考几个问题。(此时考虑的所有问题均使用 xml 配置,小伙伴们记得切换思路)

2.2.2.1 问题思考

既然 web.xml 中是对一个 Web 工程的最基本配置,那么如果想让 SpringFramework 也参与其中,应该如何设计?换句话说,现在我们的一个工程中已经有上面的 spring-web.xml 配置文件了,怎么让 web.xml 把 Spring 的配置文件加载上来呢?什么时机加载呢?加载完成后应该放在哪儿呢?

一个一个来思考。首先,加载的方式,可能我们自己去自己创建 ApplicationContext ,读取配置文件,这个貌似不是问题。但实际上,这种做法貌似搞不定,咱先往下思考,三个问题都考虑好之后,小伙伴也就可以理解为什么搞不定了。

其次,加载的时机,可以选择在整个 Web 容器启动时就加载配置文件,也可以在应用第一次被访问时再去加载,但显然前者更合适,因为 Web 容器启动的过程中服务是不对外开放的,只有等应用初始化完成后外界才能访问得到;而使用后者的话,可能应用的初始化耗时太长,导致第一次请求等待时间过长等问题。所以选用 Web 容器启动时就初始化,这种方案更合理一些。那随之又出现一个问题:用什么初始化呢?Servlet 的三个核心组件 Servlet 、Filter 、Listener ,想都不用想肯定是 Listener 最合适,因为前两者触发的时机都比较靠后,而选择合适的 Listener 可以在应用刚开始初始化的时候就触发。所以,对于这个问题,最终的结论是:在 Web 容器刚启动时,借助 Listener 加载配置文件

接下来,IOC 容器加载完成后,放到哪里比较合适呢?Web 中的四大作用域分别是 pageContext 、request 、session 、application ,很明显放在 application 作用域最好吧,因为 IOC 容器本来就是对应着一个应用的,放到 application 域中任何位置都能拿得到。

好,这三个问题考虑完成,下面就可以动手了。

2.2.2.2 编写web.xml配置

在 SpringFramework 中,其实它内置了一个监听器,就是给 web.xml 中提供 IOC 容器初始化时机的,它叫 ContextLoaderListener 。所以我们可以在 web.xml 中配置这样一个监听器:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

哇塞这么简单吗?配置好就 OK 了吗?那咱启动 Tomcat 吧!别高兴得太早,你现在就启动,只能在控制台找到一个异常:

Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/applicationContext.xml]

很明显,它默认会去找 WEB-INF 目录下的 applicationContext.xml 文件。而我们的 xml 配置文件是放在 resources 目录下的,这可咋整?

很简单,在 web.xml 中再添加一个初始化参数:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-web.xml</param-value>
</context-param>

这个参数的名称一看就明白,上下文的配置路径,那不就是指定配置文件的路径嘛。这个配置的值,可以写一个特定的配置文件,也可以写通配符(例如 spring-*.xml ,则会加载所有文件名称符合 spring- 开头,.xml 结尾的文件)。

配置好之后,再启动 Tomcat ,这次就没有报任何异常了,控制台也能打印 IOC 容器初始化成功的日志:

信息 [RMI TCP Connection(3)-127.0.0.1] org.springframework.web.context.ContextLoader.initWebApplicationContext Root WebApplicationContext: initialization started

2.2.3 Servlet的编写

这样干启动了,没法测效果呀,这样咱再写一个 UserServlet ,让它依赖 UserService :

@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
    
    private UserService userService;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String user = userService.get();
        resp.getWriter().println(user);
    }
}

emmmmm 问题又来了:现在这样写,userService 会报空指针异常的,所以 UserServlet 如何才能拿到 UserService 呢?什么时机拿为好呢?

咱刚才在上面分析过了,IOC 容器已经被放到 application 域中,也就是 ServletContext 中了,那既然从进入 Servlet 开始后,任意位置都能拿到 ServletContext ,那我们完全可以在 doGet 中取到 ServletContext ,然后从中取出 UserService ,这样就可以调用它的方法了。

但是。。。有没有更好的办法呢?总不能每次 doGet 都取一次吧?可能这个时候有小伙伴会联想到懒汉单例模式,第一次取到之后就赋值给成员变量不就得了?这样可以是可以,有没有更优雅的方案呢?

哎,Servlet 不是有它自己的生命周期嘛!咱前面 AOP 部分还用到过呢!所以更好地办法,是重写 HttpServlet 的 父类 GenericServlet 的 init 方法,正好这个方法中有一个 ServletConfig ,可以从中取到 ServletContext :

private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
}

接下来就是取 IOC 容器了,SpringFramework 给我们提供了一个工具类:WebApplicationContextUtils ,用它就可以取出 IOC 容器了:

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
    this.userService = ctx.getBean(UserService.class);
}

这样 userService 就有值了,程序应该就可以跑起来了吧。

重新启动 Tomcat ,并在浏览器访问 http://localhost:8080/spring-web/user ,发现可以成功响应 hahaha ,证明编写没有问题。

2.2.4 注解依赖注入

如果 Servlet 依赖的类太多,那 init 里挨个 getBean 那也太费劲了,SpringFramework 还给我们提供了一个支持类,用它可以快速支持 Servlet 的注入:

@Autowired
private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, servletContext);
}

可以发现这样写更简单了吧,甚至都不需要咱自己获取 ApplicationContext 了。重启 Tomcat ,重新访问上面的路径,效果一致,说明效果都是一样的。

以上就是使用 xml 方式整合 web 的基本方式,接下来咱继续介绍注解整合的方式。

2.3 基于Servlet3.0规范的web整合

下面咱先介绍 Servlet 3.0 规范在整合 web 中的应用,然后再讲解具体的整合方式。

2.3.1 Servlet3.0规范节选

从 jcp 的官网,搜索 315 ,可以找到 JSR 315 规范:www.jcp.org/en/jsr/deta… 。

入门-Spring整合web与三层架构回顾

然后,下载第一个 Maintenance Release 里面的 pdf ,就可以得到 Servlet 3.0 的官方文档了。

当然,如果小伙伴能找到中文版的,那就更妙了。作者手里拿到的是 Servlet 3.1 的规范,都是一样看的。

在 Servlet 3.0 规范文档中,找到 8.2.4 节,它介绍的是 Shared libraries / runtimes pluggability ,即 “共享库 / 运行时的可插拔性” ,这里面讲了一个 ServletContainerInitializer 的东西,借助 Java 的 SPI 技术,可以从项目或者项目依赖的 jar 包中,找到一个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件,并加载项目中所有它的实现类。这个家伙就是为了代替 web.xml 的,所以它的加载时机也是在项目的初始化阶段就触发的。

然后,ServletContainerInitializer 这个接口通常会配合 @HandlesTypes 注解一起使用,这个注解中可以传入一些我们所需要的接口 / 抽象类(原文表达的意思是感兴趣的,怕小伙伴们觉得不好理解,所以小册调整了一下意思),支持 Servlet 3.0 规范的 Web 容器会在容器启动项目初始化时,把这些接口 / 抽象类的实现类全部都找出来,整合为一个 Set ,传入 ServletContainerInitializer 的 onStartup 方法参数中,这样我们就可以在 onStartUp 中拿到这些实现类,随机反射创建调用等等的动作。

Servlet 3.0 规范设计这个用法,目的之一是为了做到组件的可插拔,有了组件的可插拔,就可以在导入一个带 ServletContainerInitializer 的实现类的 jar 包时,自动加载对应的实现类。SpringFramework 也利用了这个机制,接下来咱来了解一下 SpringFramework 是如何支持这个规范的。

2.3.2 Spring支持Servlet3.0规范

SpringFramework 当然考虑到了这一点,所以我们翻开 spring-web 的 jar 包中,就可以找到这个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件:

入门-Spring整合web与三层架构回顾

它里面定义好的那个类是这个家伙:

org.springframework.web.SpringServletContainerInitializer

翻开它的源码,可以发现它需要的实现类的接口类型是 WebApplicationInitializer :

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer

所以我们只需要编写一个 WebApplicationInitializer 的实现类,就可以搞定咯。

不过先别着急,SpringFramework 可不会只想到这些,它帮我们封装了一个 AbstractContextLoaderInitializer 的抽象类,这里面实现了大部分逻辑,我们要做的,只是如何创建 IOC 容器,使用哪些注解配置类,扫描哪些包,仅此而已。

2.3.3 WebApplicationInitializer的编写

明白了整合的套路,那咱接下来就编写一个 WebApplicationInitializer 的实现类,放在哪里无所谓:

public class DemoWebApplicationInitializer extends AbstractContextLoaderInitializer {
    
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        // ???
    }
}

接下来就是如何创建 IOC 容器了。这里我们要做的,就是编程式的加载那些 Spring 的 xml 配置文件,或者注解配置类。所以我们可以这样编写:

@Override
protected WebApplicationContext createRootApplicationContext() {
    AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
    ctx.register(UserConfiguration.class);
    return ctx;
}

对应的,再编写一个 UserConfiguration 的配置类:

@Configuration
public class UserConfiguration {
    
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

这样就写完了,不需要在 DemoWebApplicationInitializer 中再标注什么东西了。

但是!记得一点,把 web.xml 的东西全部注释掉!不然会产生两个 IOC 容器的。。。

注释掉之后,重启 Tomcat ,再次访问 /user2 路径,发现仍然可以正常在浏览器上响应 hahaha ,证明这种基于 Servlet 3.0 规范的方法也整合成功了。

以上就是两种整合的方式,相对都比较简单,后面的 SpringWebMvc 中,咱也会讲解用这两种整合方式来搭建基于 SpringWebMvc 的工程。

终于到了 SpringFramework 学习的下一站,咱要对 SpringFramework 中的一个非常非常重要,重要到好多人都把它单独拿出来跟 Spring 放到一起论述的,那就是 SpringWebMvc 。作为 Web 部分的入门章,小册要回顾一下三层架构,然后学习一下 SpringFramework 如何整合 Web 开发。

1. MVC三层架构【回顾】

关于三层架构的演进过程,小册就不展开讲解了,咱直接来回顾 MVC 三层架构的最终形态就 OK ,毕竟前面的过程都是不完全形态,也没必要再调动起回忆了。

1.1 MVC

MVC 想必各位都很熟悉了,MVC 的三个字母分别代表 Model View Controller ,它们在一个应用中的地位和职责应该是下面图的样子:

入门-Spring整合web与三层架构回顾

如果回忆不起来的小伙伴一定要加深印象,后面咱学习 SpringWebMvc 的时候还会用它的。

1.2 三层架构

JavaEE 中的三层架构,是为了实现代码逻辑解耦的经典设计模式,这个想必咱也都很熟悉了。三层架构分别为 web 层、service 层、dao 层,它们的职责及调用逻辑可以用下图表示:

入门-Spring整合web与三层架构回顾

这个虽说是老生常谈了,接下来的学习和实际的项目开发中也都会用到,所以小伙伴们还是加深一下印象哈。

2. Spring整合web开发【掌握】

接下来的内容就是 SpringFramework 如何整合 web 工程进行实际的开发了。注意现在还没有涉及到 mvc 的东西,所以小伙伴们先不要着急,先回顾一下 Servlet 的东西也未尝不可。

接下来,咱分为基于 web.xml 和 Servlet 3.0 规范的方式,分别讲解 SpringFramework 如何整合 web 开发。

2.1 工程创建

先把项目工程创建出来吧。还是照常,使用 Maven 创建一个新的模块 spring-04-web 就好啦,本章的所有源码也都在这个工程下。

创建出来之后,不要忘了先把打包方式改为 war :

    <groupId>com.linkedbear.spring</groupId>
    <artifactId>spring-04-web</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

之后是添加依赖的部分,既然是 SpringFramework 整合 web ,那必然要多导入一个依赖啦:

<properties>
    <spring.framework.version>5.2.8.RELEASE</spring.framework.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

当然,不要忘记导 Servlet API 的依赖哈。

导入完成后,照例将其先部署到 Tomcat 中:(下面是 IDEA 的配置方法)

入门-Spring整合web与三层架构回顾

入门-Spring整合web与三层架构回顾

配置完成后,启动 Tomcat ,无任何报错则说明一切配置正确,可以接着往下进行了。

2.2 基于web.xml的web整合

首先,咱来准备一点基础代码。

2.2.1 基础代码准备

基础代码也不用折腾的很复杂,只需要一个配置文件,一个 Service 即可,不用写的很麻烦。

public class UserService {
    
    public String get() {
        return "hahaha";
    }
}

spring-web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
    <bean class="com.linkedbear.spring.xml.a_basic.UserService"/>
</beans>

这样基本代码就算准备完成了。

2.2.2 web.xml

下面咱先来编写 web.xml 的基本内容。

上面咱导依赖的时候使用的 Servlet API 是 3.1 ,所以这里咱也是用 3.1 版本即可。先把外壳写上:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

</web-app>

接下来里头该配置啥呢???小册打算先让小伙伴思考几个问题。(此时考虑的所有问题均使用 xml 配置,小伙伴们记得切换思路)

2.2.2.1 问题思考

既然 web.xml 中是对一个 Web 工程的最基本配置,那么如果想让 SpringFramework 也参与其中,应该如何设计?换句话说,现在我们的一个工程中已经有上面的 spring-web.xml 配置文件了,怎么让 web.xml 把 Spring 的配置文件加载上来呢?什么时机加载呢?加载完成后应该放在哪儿呢?

一个一个来思考。首先,加载的方式,可能我们自己去自己创建 ApplicationContext ,读取配置文件,这个貌似不是问题。但实际上,这种做法貌似搞不定,咱先往下思考,三个问题都考虑好之后,小伙伴也就可以理解为什么搞不定了。

其次,加载的时机,可以选择在整个 Web 容器启动时就加载配置文件,也可以在应用第一次被访问时再去加载,但显然前者更合适,因为 Web 容器启动的过程中服务是不对外开放的,只有等应用初始化完成后外界才能访问得到;而使用后者的话,可能应用的初始化耗时太长,导致第一次请求等待时间过长等问题。所以选用 Web 容器启动时就初始化,这种方案更合理一些。那随之又出现一个问题:用什么初始化呢?Servlet 的三个核心组件 Servlet 、Filter 、Listener ,想都不用想肯定是 Listener 最合适,因为前两者触发的时机都比较靠后,而选择合适的 Listener 可以在应用刚开始初始化的时候就触发。所以,对于这个问题,最终的结论是:在 Web 容器刚启动时,借助 Listener 加载配置文件

接下来,IOC 容器加载完成后,放到哪里比较合适呢?Web 中的四大作用域分别是 pageContext 、request 、session 、application ,很明显放在 application 作用域最好吧,因为 IOC 容器本来就是对应着一个应用的,放到 application 域中任何位置都能拿得到。

好,这三个问题考虑完成,下面就可以动手了。

2.2.2.2 编写web.xml配置

在 SpringFramework 中,其实它内置了一个监听器,就是给 web.xml 中提供 IOC 容器初始化时机的,它叫 ContextLoaderListener 。所以我们可以在 web.xml 中配置这样一个监听器:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

哇塞这么简单吗?配置好就 OK 了吗?那咱启动 Tomcat 吧!别高兴得太早,你现在就启动,只能在控制台找到一个异常:

Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/applicationContext.xml]

很明显,它默认会去找 WEB-INF 目录下的 applicationContext.xml 文件。而我们的 xml 配置文件是放在 resources 目录下的,这可咋整?

很简单,在 web.xml 中再添加一个初始化参数:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-web.xml</param-value>
</context-param>

这个参数的名称一看就明白,上下文的配置路径,那不就是指定配置文件的路径嘛。这个配置的值,可以写一个特定的配置文件,也可以写通配符(例如 spring-*.xml ,则会加载所有文件名称符合 spring- 开头,.xml 结尾的文件)。

配置好之后,再启动 Tomcat ,这次就没有报任何异常了,控制台也能打印 IOC 容器初始化成功的日志:

信息 [RMI TCP Connection(3)-127.0.0.1] org.springframework.web.context.ContextLoader.initWebApplicationContext Root WebApplicationContext: initialization started

2.2.3 Servlet的编写

这样干启动了,没法测效果呀,这样咱再写一个 UserServlet ,让它依赖 UserService :

@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
    
    private UserService userService;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String user = userService.get();
        resp.getWriter().println(user);
    }
}

emmmmm 问题又来了:现在这样写,userService 会报空指针异常的,所以 UserServlet 如何才能拿到 UserService 呢?什么时机拿为好呢?

咱刚才在上面分析过了,IOC 容器已经被放到 application 域中,也就是 ServletContext 中了,那既然从进入 Servlet 开始后,任意位置都能拿到 ServletContext ,那我们完全可以在 doGet 中取到 ServletContext ,然后从中取出 UserService ,这样就可以调用它的方法了。

但是。。。有没有更好的办法呢?总不能每次 doGet 都取一次吧?可能这个时候有小伙伴会联想到懒汉单例模式,第一次取到之后就赋值给成员变量不就得了?这样可以是可以,有没有更优雅的方案呢?

哎,Servlet 不是有它自己的生命周期嘛!咱前面 AOP 部分还用到过呢!所以更好地办法,是重写 HttpServlet 的 父类 GenericServlet 的 init 方法,正好这个方法中有一个 ServletConfig ,可以从中取到 ServletContext :

private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
}

接下来就是取 IOC 容器了,SpringFramework 给我们提供了一个工具类:WebApplicationContextUtils ,用它就可以取出 IOC 容器了:

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
    this.userService = ctx.getBean(UserService.class);
}

这样 userService 就有值了,程序应该就可以跑起来了吧。

重新启动 Tomcat ,并在浏览器访问 http://localhost:8080/spring-web/user ,发现可以成功响应 hahaha ,证明编写没有问题。

2.2.4 注解依赖注入

如果 Servlet 依赖的类太多,那 init 里挨个 getBean 那也太费劲了,SpringFramework 还给我们提供了一个支持类,用它可以快速支持 Servlet 的注入:

@Autowired
private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, servletContext);
}

可以发现这样写更简单了吧,甚至都不需要咱自己获取 ApplicationContext 了。重启 Tomcat ,重新访问上面的路径,效果一致,说明效果都是一样的。

以上就是使用 xml 方式整合 web 的基本方式,接下来咱继续介绍注解整合的方式。

2.3 基于Servlet3.0规范的web整合

下面咱先介绍 Servlet 3.0 规范在整合 web 中的应用,然后再讲解具体的整合方式。

2.3.1 Servlet3.0规范节选

从 jcp 的官网,搜索 315 ,可以找到 JSR 315 规范:www.jcp.org/en/jsr/deta… 。

入门-Spring整合web与三层架构回顾

然后,下载第一个 Maintenance Release 里面的 pdf ,就可以得到 Servlet 3.0 的官方文档了。

当然,如果小伙伴能找到中文版的,那就更妙了。作者手里拿到的是 Servlet 3.1 的规范,都是一样看的。

在 Servlet 3.0 规范文档中,找到 8.2.4 节,它介绍的是 Shared libraries / runtimes pluggability ,即 “共享库 / 运行时的可插拔性” ,这里面讲了一个 ServletContainerInitializer 的东西,借助 Java 的 SPI 技术,可以从项目或者项目依赖的 jar 包中,找到一个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件,并加载项目中所有它的实现类。这个家伙就是为了代替 web.xml 的,所以它的加载时机也是在项目的初始化阶段就触发的。

然后,ServletContainerInitializer 这个接口通常会配合 @HandlesTypes 注解一起使用,这个注解中可以传入一些我们所需要的接口 / 抽象类(原文表达的意思是感兴趣的,怕小伙伴们觉得不好理解,所以小册调整了一下意思),支持 Servlet 3.0 规范的 Web 容器会在容器启动项目初始化时,把这些接口 / 抽象类的实现类全部都找出来,整合为一个 Set ,传入 ServletContainerInitializer 的 onStartup 方法参数中,这样我们就可以在 onStartUp 中拿到这些实现类,随机反射创建调用等等的动作。

Servlet 3.0 规范设计这个用法,目的之一是为了做到组件的可插拔,有了组件的可插拔,就可以在导入一个带 ServletContainerInitializer 的实现类的 jar 包时,自动加载对应的实现类。SpringFramework 也利用了这个机制,接下来咱来了解一下 SpringFramework 是如何支持这个规范的。

2.3.2 Spring支持Servlet3.0规范

SpringFramework 当然考虑到了这一点,所以我们翻开 spring-web 的 jar 包中,就可以找到这个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件:

入门-Spring整合web与三层架构回顾

它里面定义好的那个类是这个家伙:

org.springframework.web.SpringServletContainerInitializer

翻开它的源码,可以发现它需要的实现类的接口类型是 WebApplicationInitializer :

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer

所以我们只需要编写一个 WebApplicationInitializer 的实现类,就可以搞定咯。

不过先别着急,SpringFramework 可不会只想到这些,它帮我们封装了一个 AbstractContextLoaderInitializer 的抽象类,这里面实现了大部分逻辑,我们要做的,只是如何创建 IOC 容器,使用哪些注解配置类,扫描哪些包,仅此而已。

2.3.3 WebApplicationInitializer的编写

明白了整合的套路,那咱接下来就编写一个 WebApplicationInitializer 的实现类,放在哪里无所谓:

public class DemoWebApplicationInitializer extends AbstractContextLoaderInitializer {
    
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        // ???
    }
}

接下来就是如何创建 IOC 容器了。这里我们要做的,就是编程式的加载那些 Spring 的 xml 配置文件,或者注解配置类。所以我们可以这样编写:

@Override
protected WebApplicationContext createRootApplicationContext() {
    AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
    ctx.register(UserConfiguration.class);
    return ctx;
}

对应的,再编写一个 UserConfiguration 的配置类:

@Configuration
public class UserConfiguration {
    
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

这样就写完了,不需要在 DemoWebApplicationInitializer 中再标注什么东西了。

但是!记得一点,把 web.xml 的东西全部注释掉!不然会产生两个 IOC 容器的。。。

注释掉之后,重启 Tomcat ,再次访问 /user2 路径,发现仍然可以正常在浏览器上响应 hahaha ,证明这种基于 Servlet 3.0 规范的方法也整合成功了。

以上就是两种整合的方式,相对都比较简单,后面的 SpringWebMvc 中,咱也会讲解用这两种整合方式来搭建基于 SpringWebMvc 的工程。

终于到了 SpringFramework 学习的下一站,咱要对 SpringFramework 中的一个非常非常重要,重要到好多人都把它单独拿出来跟 Spring 放到一起论述的,那就是 SpringWebMvc 。作为 Web 部分的入门章,小册要回顾一下三层架构,然后学习一下 SpringFramework 如何整合 Web 开发。

1. MVC三层架构【回顾】

关于三层架构的演进过程,小册就不展开讲解了,咱直接来回顾 MVC 三层架构的最终形态就 OK ,毕竟前面的过程都是不完全形态,也没必要再调动起回忆了。

1.1 MVC

MVC 想必各位都很熟悉了,MVC 的三个字母分别代表 Model View Controller ,它们在一个应用中的地位和职责应该是下面图的样子:

入门-Spring整合web与三层架构回顾

如果回忆不起来的小伙伴一定要加深印象,后面咱学习 SpringWebMvc 的时候还会用它的。

1.2 三层架构

JavaEE 中的三层架构,是为了实现代码逻辑解耦的经典设计模式,这个想必咱也都很熟悉了。三层架构分别为 web 层、service 层、dao 层,它们的职责及调用逻辑可以用下图表示:

入门-Spring整合web与三层架构回顾

这个虽说是老生常谈了,接下来的学习和实际的项目开发中也都会用到,所以小伙伴们还是加深一下印象哈。

2. Spring整合web开发【掌握】

接下来的内容就是 SpringFramework 如何整合 web 工程进行实际的开发了。注意现在还没有涉及到 mvc 的东西,所以小伙伴们先不要着急,先回顾一下 Servlet 的东西也未尝不可。

接下来,咱分为基于 web.xml 和 Servlet 3.0 规范的方式,分别讲解 SpringFramework 如何整合 web 开发。

2.1 工程创建

先把项目工程创建出来吧。还是照常,使用 Maven 创建一个新的模块 spring-04-web 就好啦,本章的所有源码也都在这个工程下。

创建出来之后,不要忘了先把打包方式改为 war :

    <groupId>com.linkedbear.spring</groupId>
    <artifactId>spring-04-web</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

之后是添加依赖的部分,既然是 SpringFramework 整合 web ,那必然要多导入一个依赖啦:

<properties>
    <spring.framework.version>5.2.8.RELEASE</spring.framework.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

当然,不要忘记导 Servlet API 的依赖哈。

导入完成后,照例将其先部署到 Tomcat 中:(下面是 IDEA 的配置方法)

入门-Spring整合web与三层架构回顾

入门-Spring整合web与三层架构回顾

配置完成后,启动 Tomcat ,无任何报错则说明一切配置正确,可以接着往下进行了。

2.2 基于web.xml的web整合

首先,咱来准备一点基础代码。

2.2.1 基础代码准备

基础代码也不用折腾的很复杂,只需要一个配置文件,一个 Service 即可,不用写的很麻烦。

public class UserService {
    
    public String get() {
        return "hahaha";
    }
}

spring-web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
    <bean class="com.linkedbear.spring.xml.a_basic.UserService"/>
</beans>

这样基本代码就算准备完成了。

2.2.2 web.xml

下面咱先来编写 web.xml 的基本内容。

上面咱导依赖的时候使用的 Servlet API 是 3.1 ,所以这里咱也是用 3.1 版本即可。先把外壳写上:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

</web-app>

接下来里头该配置啥呢???小册打算先让小伙伴思考几个问题。(此时考虑的所有问题均使用 xml 配置,小伙伴们记得切换思路)

2.2.2.1 问题思考

既然 web.xml 中是对一个 Web 工程的最基本配置,那么如果想让 SpringFramework 也参与其中,应该如何设计?换句话说,现在我们的一个工程中已经有上面的 spring-web.xml 配置文件了,怎么让 web.xml 把 Spring 的配置文件加载上来呢?什么时机加载呢?加载完成后应该放在哪儿呢?

一个一个来思考。首先,加载的方式,可能我们自己去自己创建 ApplicationContext ,读取配置文件,这个貌似不是问题。但实际上,这种做法貌似搞不定,咱先往下思考,三个问题都考虑好之后,小伙伴也就可以理解为什么搞不定了。

其次,加载的时机,可以选择在整个 Web 容器启动时就加载配置文件,也可以在应用第一次被访问时再去加载,但显然前者更合适,因为 Web 容器启动的过程中服务是不对外开放的,只有等应用初始化完成后外界才能访问得到;而使用后者的话,可能应用的初始化耗时太长,导致第一次请求等待时间过长等问题。所以选用 Web 容器启动时就初始化,这种方案更合理一些。那随之又出现一个问题:用什么初始化呢?Servlet 的三个核心组件 Servlet 、Filter 、Listener ,想都不用想肯定是 Listener 最合适,因为前两者触发的时机都比较靠后,而选择合适的 Listener 可以在应用刚开始初始化的时候就触发。所以,对于这个问题,最终的结论是:在 Web 容器刚启动时,借助 Listener 加载配置文件

接下来,IOC 容器加载完成后,放到哪里比较合适呢?Web 中的四大作用域分别是 pageContext 、request 、session 、application ,很明显放在 application 作用域最好吧,因为 IOC 容器本来就是对应着一个应用的,放到 application 域中任何位置都能拿得到。

好,这三个问题考虑完成,下面就可以动手了。

2.2.2.2 编写web.xml配置

在 SpringFramework 中,其实它内置了一个监听器,就是给 web.xml 中提供 IOC 容器初始化时机的,它叫 ContextLoaderListener 。所以我们可以在 web.xml 中配置这样一个监听器:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

哇塞这么简单吗?配置好就 OK 了吗?那咱启动 Tomcat 吧!别高兴得太早,你现在就启动,只能在控制台找到一个异常:

Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/applicationContext.xml]

很明显,它默认会去找 WEB-INF 目录下的 applicationContext.xml 文件。而我们的 xml 配置文件是放在 resources 目录下的,这可咋整?

很简单,在 web.xml 中再添加一个初始化参数:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-web.xml</param-value>
</context-param>

这个参数的名称一看就明白,上下文的配置路径,那不就是指定配置文件的路径嘛。这个配置的值,可以写一个特定的配置文件,也可以写通配符(例如 spring-*.xml ,则会加载所有文件名称符合 spring- 开头,.xml 结尾的文件)。

配置好之后,再启动 Tomcat ,这次就没有报任何异常了,控制台也能打印 IOC 容器初始化成功的日志:

信息 [RMI TCP Connection(3)-127.0.0.1] org.springframework.web.context.ContextLoader.initWebApplicationContext Root WebApplicationContext: initialization started

2.2.3 Servlet的编写

这样干启动了,没法测效果呀,这样咱再写一个 UserServlet ,让它依赖 UserService :

@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
    
    private UserService userService;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String user = userService.get();
        resp.getWriter().println(user);
    }
}

emmmmm 问题又来了:现在这样写,userService 会报空指针异常的,所以 UserServlet 如何才能拿到 UserService 呢?什么时机拿为好呢?

咱刚才在上面分析过了,IOC 容器已经被放到 application 域中,也就是 ServletContext 中了,那既然从进入 Servlet 开始后,任意位置都能拿到 ServletContext ,那我们完全可以在 doGet 中取到 ServletContext ,然后从中取出 UserService ,这样就可以调用它的方法了。

但是。。。有没有更好的办法呢?总不能每次 doGet 都取一次吧?可能这个时候有小伙伴会联想到懒汉单例模式,第一次取到之后就赋值给成员变量不就得了?这样可以是可以,有没有更优雅的方案呢?

哎,Servlet 不是有它自己的生命周期嘛!咱前面 AOP 部分还用到过呢!所以更好地办法,是重写 HttpServlet 的 父类 GenericServlet 的 init 方法,正好这个方法中有一个 ServletConfig ,可以从中取到 ServletContext :

private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
}

接下来就是取 IOC 容器了,SpringFramework 给我们提供了一个工具类:WebApplicationContextUtils ,用它就可以取出 IOC 容器了:

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
    this.userService = ctx.getBean(UserService.class);
}

这样 userService 就有值了,程序应该就可以跑起来了吧。

重新启动 Tomcat ,并在浏览器访问 http://localhost:8080/spring-web/user ,发现可以成功响应 hahaha ,证明编写没有问题。

2.2.4 注解依赖注入

如果 Servlet 依赖的类太多,那 init 里挨个 getBean 那也太费劲了,SpringFramework 还给我们提供了一个支持类,用它可以快速支持 Servlet 的注入:

@Autowired
private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, servletContext);
}

可以发现这样写更简单了吧,甚至都不需要咱自己获取 ApplicationContext 了。重启 Tomcat ,重新访问上面的路径,效果一致,说明效果都是一样的。

以上就是使用 xml 方式整合 web 的基本方式,接下来咱继续介绍注解整合的方式。

2.3 基于Servlet3.0规范的web整合

下面咱先介绍 Servlet 3.0 规范在整合 web 中的应用,然后再讲解具体的整合方式。

2.3.1 Servlet3.0规范节选

从 jcp 的官网,搜索 315 ,可以找到 JSR 315 规范:www.jcp.org/en/jsr/deta… 。

入门-Spring整合web与三层架构回顾

然后,下载第一个 Maintenance Release 里面的 pdf ,就可以得到 Servlet 3.0 的官方文档了。

当然,如果小伙伴能找到中文版的,那就更妙了。作者手里拿到的是 Servlet 3.1 的规范,都是一样看的。

在 Servlet 3.0 规范文档中,找到 8.2.4 节,它介绍的是 Shared libraries / runtimes pluggability ,即 “共享库 / 运行时的可插拔性” ,这里面讲了一个 ServletContainerInitializer 的东西,借助 Java 的 SPI 技术,可以从项目或者项目依赖的 jar 包中,找到一个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件,并加载项目中所有它的实现类。这个家伙就是为了代替 web.xml 的,所以它的加载时机也是在项目的初始化阶段就触发的。

然后,ServletContainerInitializer 这个接口通常会配合 @HandlesTypes 注解一起使用,这个注解中可以传入一些我们所需要的接口 / 抽象类(原文表达的意思是感兴趣的,怕小伙伴们觉得不好理解,所以小册调整了一下意思),支持 Servlet 3.0 规范的 Web 容器会在容器启动项目初始化时,把这些接口 / 抽象类的实现类全部都找出来,整合为一个 Set ,传入 ServletContainerInitializer 的 onStartup 方法参数中,这样我们就可以在 onStartUp 中拿到这些实现类,随机反射创建调用等等的动作。

Servlet 3.0 规范设计这个用法,目的之一是为了做到组件的可插拔,有了组件的可插拔,就可以在导入一个带 ServletContainerInitializer 的实现类的 jar 包时,自动加载对应的实现类。SpringFramework 也利用了这个机制,接下来咱来了解一下 SpringFramework 是如何支持这个规范的。

2.3.2 Spring支持Servlet3.0规范

SpringFramework 当然考虑到了这一点,所以我们翻开 spring-web 的 jar 包中,就可以找到这个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件:

入门-Spring整合web与三层架构回顾

它里面定义好的那个类是这个家伙:

org.springframework.web.SpringServletContainerInitializer

翻开它的源码,可以发现它需要的实现类的接口类型是 WebApplicationInitializer :

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer

所以我们只需要编写一个 WebApplicationInitializer 的实现类,就可以搞定咯。

不过先别着急,SpringFramework 可不会只想到这些,它帮我们封装了一个 AbstractContextLoaderInitializer 的抽象类,这里面实现了大部分逻辑,我们要做的,只是如何创建 IOC 容器,使用哪些注解配置类,扫描哪些包,仅此而已。

2.3.3 WebApplicationInitializer的编写

明白了整合的套路,那咱接下来就编写一个 WebApplicationInitializer 的实现类,放在哪里无所谓:

public class DemoWebApplicationInitializer extends AbstractContextLoaderInitializer {
    
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        // ???
    }
}

接下来就是如何创建 IOC 容器了。这里我们要做的,就是编程式的加载那些 Spring 的 xml 配置文件,或者注解配置类。所以我们可以这样编写:

@Override
protected WebApplicationContext createRootApplicationContext() {
    AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
    ctx.register(UserConfiguration.class);
    return ctx;
}

对应的,再编写一个 UserConfiguration 的配置类:

@Configuration
public class UserConfiguration {
    
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

这样就写完了,不需要在 DemoWebApplicationInitializer 中再标注什么东西了。

但是!记得一点,把 web.xml 的东西全部注释掉!不然会产生两个 IOC 容器的。。。

注释掉之后,重启 Tomcat ,再次访问 /user2 路径,发现仍然可以正常在浏览器上响应 hahaha ,证明这种基于 Servlet 3.0 规范的方法也整合成功了。

以上就是两种整合的方式,相对都比较简单,后面的 SpringWebMvc 中,咱也会讲解用这两种整合方式来搭建基于 SpringWebMvc 的工程。

终于到了 SpringFramework 学习的下一站,咱要对 SpringFramework 中的一个非常非常重要,重要到好多人都把它单独拿出来跟 Spring 放到一起论述的,那就是 SpringWebMvc 。作为 Web 部分的入门章,小册要回顾一下三层架构,然后学习一下 SpringFramework 如何整合 Web 开发。

1. MVC三层架构【回顾】

关于三层架构的演进过程,小册就不展开讲解了,咱直接来回顾 MVC 三层架构的最终形态就 OK ,毕竟前面的过程都是不完全形态,也没必要再调动起回忆了。

1.1 MVC

MVC 想必各位都很熟悉了,MVC 的三个字母分别代表 Model View Controller ,它们在一个应用中的地位和职责应该是下面图的样子:

入门-Spring整合web与三层架构回顾

如果回忆不起来的小伙伴一定要加深印象,后面咱学习 SpringWebMvc 的时候还会用它的。

1.2 三层架构

JavaEE 中的三层架构,是为了实现代码逻辑解耦的经典设计模式,这个想必咱也都很熟悉了。三层架构分别为 web 层、service 层、dao 层,它们的职责及调用逻辑可以用下图表示:

入门-Spring整合web与三层架构回顾

这个虽说是老生常谈了,接下来的学习和实际的项目开发中也都会用到,所以小伙伴们还是加深一下印象哈。

2. Spring整合web开发【掌握】

接下来的内容就是 SpringFramework 如何整合 web 工程进行实际的开发了。注意现在还没有涉及到 mvc 的东西,所以小伙伴们先不要着急,先回顾一下 Servlet 的东西也未尝不可。

接下来,咱分为基于 web.xml 和 Servlet 3.0 规范的方式,分别讲解 SpringFramework 如何整合 web 开发。

2.1 工程创建

先把项目工程创建出来吧。还是照常,使用 Maven 创建一个新的模块 spring-04-web 就好啦,本章的所有源码也都在这个工程下。

创建出来之后,不要忘了先把打包方式改为 war :

    <groupId>com.linkedbear.spring</groupId>
    <artifactId>spring-04-web</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

之后是添加依赖的部分,既然是 SpringFramework 整合 web ,那必然要多导入一个依赖啦:

<properties>
    <spring.framework.version>5.2.8.RELEASE</spring.framework.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

当然,不要忘记导 Servlet API 的依赖哈。

导入完成后,照例将其先部署到 Tomcat 中:(下面是 IDEA 的配置方法)

入门-Spring整合web与三层架构回顾

入门-Spring整合web与三层架构回顾

配置完成后,启动 Tomcat ,无任何报错则说明一切配置正确,可以接着往下进行了。

2.2 基于web.xml的web整合

首先,咱来准备一点基础代码。

2.2.1 基础代码准备

基础代码也不用折腾的很复杂,只需要一个配置文件,一个 Service 即可,不用写的很麻烦。

public class UserService {
    
    public String get() {
        return "hahaha";
    }
}

spring-web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
    <bean class="com.linkedbear.spring.xml.a_basic.UserService"/>
</beans>

这样基本代码就算准备完成了。

2.2.2 web.xml

下面咱先来编写 web.xml 的基本内容。

上面咱导依赖的时候使用的 Servlet API 是 3.1 ,所以这里咱也是用 3.1 版本即可。先把外壳写上:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

</web-app>

接下来里头该配置啥呢???小册打算先让小伙伴思考几个问题。(此时考虑的所有问题均使用 xml 配置,小伙伴们记得切换思路)

2.2.2.1 问题思考

既然 web.xml 中是对一个 Web 工程的最基本配置,那么如果想让 SpringFramework 也参与其中,应该如何设计?换句话说,现在我们的一个工程中已经有上面的 spring-web.xml 配置文件了,怎么让 web.xml 把 Spring 的配置文件加载上来呢?什么时机加载呢?加载完成后应该放在哪儿呢?

一个一个来思考。首先,加载的方式,可能我们自己去自己创建 ApplicationContext ,读取配置文件,这个貌似不是问题。但实际上,这种做法貌似搞不定,咱先往下思考,三个问题都考虑好之后,小伙伴也就可以理解为什么搞不定了。

其次,加载的时机,可以选择在整个 Web 容器启动时就加载配置文件,也可以在应用第一次被访问时再去加载,但显然前者更合适,因为 Web 容器启动的过程中服务是不对外开放的,只有等应用初始化完成后外界才能访问得到;而使用后者的话,可能应用的初始化耗时太长,导致第一次请求等待时间过长等问题。所以选用 Web 容器启动时就初始化,这种方案更合理一些。那随之又出现一个问题:用什么初始化呢?Servlet 的三个核心组件 Servlet 、Filter 、Listener ,想都不用想肯定是 Listener 最合适,因为前两者触发的时机都比较靠后,而选择合适的 Listener 可以在应用刚开始初始化的时候就触发。所以,对于这个问题,最终的结论是:在 Web 容器刚启动时,借助 Listener 加载配置文件

接下来,IOC 容器加载完成后,放到哪里比较合适呢?Web 中的四大作用域分别是 pageContext 、request 、session 、application ,很明显放在 application 作用域最好吧,因为 IOC 容器本来就是对应着一个应用的,放到 application 域中任何位置都能拿得到。

好,这三个问题考虑完成,下面就可以动手了。

2.2.2.2 编写web.xml配置

在 SpringFramework 中,其实它内置了一个监听器,就是给 web.xml 中提供 IOC 容器初始化时机的,它叫 ContextLoaderListener 。所以我们可以在 web.xml 中配置这样一个监听器:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

哇塞这么简单吗?配置好就 OK 了吗?那咱启动 Tomcat 吧!别高兴得太早,你现在就启动,只能在控制台找到一个异常:

Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/applicationContext.xml]

很明显,它默认会去找 WEB-INF 目录下的 applicationContext.xml 文件。而我们的 xml 配置文件是放在 resources 目录下的,这可咋整?

很简单,在 web.xml 中再添加一个初始化参数:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-web.xml</param-value>
</context-param>

这个参数的名称一看就明白,上下文的配置路径,那不就是指定配置文件的路径嘛。这个配置的值,可以写一个特定的配置文件,也可以写通配符(例如 spring-*.xml ,则会加载所有文件名称符合 spring- 开头,.xml 结尾的文件)。

配置好之后,再启动 Tomcat ,这次就没有报任何异常了,控制台也能打印 IOC 容器初始化成功的日志:

信息 [RMI TCP Connection(3)-127.0.0.1] org.springframework.web.context.ContextLoader.initWebApplicationContext Root WebApplicationContext: initialization started

2.2.3 Servlet的编写

这样干启动了,没法测效果呀,这样咱再写一个 UserServlet ,让它依赖 UserService :

@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
    
    private UserService userService;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String user = userService.get();
        resp.getWriter().println(user);
    }
}

emmmmm 问题又来了:现在这样写,userService 会报空指针异常的,所以 UserServlet 如何才能拿到 UserService 呢?什么时机拿为好呢?

咱刚才在上面分析过了,IOC 容器已经被放到 application 域中,也就是 ServletContext 中了,那既然从进入 Servlet 开始后,任意位置都能拿到 ServletContext ,那我们完全可以在 doGet 中取到 ServletContext ,然后从中取出 UserService ,这样就可以调用它的方法了。

但是。。。有没有更好的办法呢?总不能每次 doGet 都取一次吧?可能这个时候有小伙伴会联想到懒汉单例模式,第一次取到之后就赋值给成员变量不就得了?这样可以是可以,有没有更优雅的方案呢?

哎,Servlet 不是有它自己的生命周期嘛!咱前面 AOP 部分还用到过呢!所以更好地办法,是重写 HttpServlet 的 父类 GenericServlet 的 init 方法,正好这个方法中有一个 ServletConfig ,可以从中取到 ServletContext :

private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
}

接下来就是取 IOC 容器了,SpringFramework 给我们提供了一个工具类:WebApplicationContextUtils ,用它就可以取出 IOC 容器了:

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
    this.userService = ctx.getBean(UserService.class);
}

这样 userService 就有值了,程序应该就可以跑起来了吧。

重新启动 Tomcat ,并在浏览器访问 http://localhost:8080/spring-web/user ,发现可以成功响应 hahaha ,证明编写没有问题。

2.2.4 注解依赖注入

如果 Servlet 依赖的类太多,那 init 里挨个 getBean 那也太费劲了,SpringFramework 还给我们提供了一个支持类,用它可以快速支持 Servlet 的注入:

@Autowired
private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, servletContext);
}

可以发现这样写更简单了吧,甚至都不需要咱自己获取 ApplicationContext 了。重启 Tomcat ,重新访问上面的路径,效果一致,说明效果都是一样的。

以上就是使用 xml 方式整合 web 的基本方式,接下来咱继续介绍注解整合的方式。

2.3 基于Servlet3.0规范的web整合

下面咱先介绍 Servlet 3.0 规范在整合 web 中的应用,然后再讲解具体的整合方式。

2.3.1 Servlet3.0规范节选

从 jcp 的官网,搜索 315 ,可以找到 JSR 315 规范:www.jcp.org/en/jsr/deta… 。

入门-Spring整合web与三层架构回顾

然后,下载第一个 Maintenance Release 里面的 pdf ,就可以得到 Servlet 3.0 的官方文档了。

当然,如果小伙伴能找到中文版的,那就更妙了。作者手里拿到的是 Servlet 3.1 的规范,都是一样看的。

在 Servlet 3.0 规范文档中,找到 8.2.4 节,它介绍的是 Shared libraries / runtimes pluggability ,即 “共享库 / 运行时的可插拔性” ,这里面讲了一个 ServletContainerInitializer 的东西,借助 Java 的 SPI 技术,可以从项目或者项目依赖的 jar 包中,找到一个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件,并加载项目中所有它的实现类。这个家伙就是为了代替 web.xml 的,所以它的加载时机也是在项目的初始化阶段就触发的。

然后,ServletContainerInitializer 这个接口通常会配合 @HandlesTypes 注解一起使用,这个注解中可以传入一些我们所需要的接口 / 抽象类(原文表达的意思是感兴趣的,怕小伙伴们觉得不好理解,所以小册调整了一下意思),支持 Servlet 3.0 规范的 Web 容器会在容器启动项目初始化时,把这些接口 / 抽象类的实现类全部都找出来,整合为一个 Set ,传入 ServletContainerInitializer 的 onStartup 方法参数中,这样我们就可以在 onStartUp 中拿到这些实现类,随机反射创建调用等等的动作。

Servlet 3.0 规范设计这个用法,目的之一是为了做到组件的可插拔,有了组件的可插拔,就可以在导入一个带 ServletContainerInitializer 的实现类的 jar 包时,自动加载对应的实现类。SpringFramework 也利用了这个机制,接下来咱来了解一下 SpringFramework 是如何支持这个规范的。

2.3.2 Spring支持Servlet3.0规范

SpringFramework 当然考虑到了这一点,所以我们翻开 spring-web 的 jar 包中,就可以找到这个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件:

入门-Spring整合web与三层架构回顾

它里面定义好的那个类是这个家伙:

org.springframework.web.SpringServletContainerInitializer

翻开它的源码,可以发现它需要的实现类的接口类型是 WebApplicationInitializer :

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer

所以我们只需要编写一个 WebApplicationInitializer 的实现类,就可以搞定咯。

不过先别着急,SpringFramework 可不会只想到这些,它帮我们封装了一个 AbstractContextLoaderInitializer 的抽象类,这里面实现了大部分逻辑,我们要做的,只是如何创建 IOC 容器,使用哪些注解配置类,扫描哪些包,仅此而已。

2.3.3 WebApplicationInitializer的编写

明白了整合的套路,那咱接下来就编写一个 WebApplicationInitializer 的实现类,放在哪里无所谓:

public class DemoWebApplicationInitializer extends AbstractContextLoaderInitializer {
    
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        // ???
    }
}

接下来就是如何创建 IOC 容器了。这里我们要做的,就是编程式的加载那些 Spring 的 xml 配置文件,或者注解配置类。所以我们可以这样编写:

@Override
protected WebApplicationContext createRootApplicationContext() {
    AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
    ctx.register(UserConfiguration.class);
    return ctx;
}

对应的,再编写一个 UserConfiguration 的配置类:

@Configuration
public class UserConfiguration {
    
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

这样就写完了,不需要在 DemoWebApplicationInitializer 中再标注什么东西了。

但是!记得一点,把 web.xml 的东西全部注释掉!不然会产生两个 IOC 容器的。。。

注释掉之后,重启 Tomcat ,再次访问 /user2 路径,发现仍然可以正常在浏览器上响应 hahaha ,证明这种基于 Servlet 3.0 规范的方法也整合成功了。

以上就是两种整合的方式,相对都比较简单,后面的 SpringWebMvc 中,咱也会讲解用这两种整合方式来搭建基于 SpringWebMvc 的工程。

终于到了 SpringFramework 学习的下一站,咱要对 SpringFramework 中的一个非常非常重要,重要到好多人都把它单独拿出来跟 Spring 放到一起论述的,那就是 SpringWebMvc 。作为 Web 部分的入门章,小册要回顾一下三层架构,然后学习一下 SpringFramework 如何整合 Web 开发。

1. MVC三层架构【回顾】

关于三层架构的演进过程,小册就不展开讲解了,咱直接来回顾 MVC 三层架构的最终形态就 OK ,毕竟前面的过程都是不完全形态,也没必要再调动起回忆了。

1.1 MVC

MVC 想必各位都很熟悉了,MVC 的三个字母分别代表 Model View Controller ,它们在一个应用中的地位和职责应该是下面图的样子:

入门-Spring整合web与三层架构回顾

如果回忆不起来的小伙伴一定要加深印象,后面咱学习 SpringWebMvc 的时候还会用它的。

1.2 三层架构

JavaEE 中的三层架构,是为了实现代码逻辑解耦的经典设计模式,这个想必咱也都很熟悉了。三层架构分别为 web 层、service 层、dao 层,它们的职责及调用逻辑可以用下图表示:

入门-Spring整合web与三层架构回顾

这个虽说是老生常谈了,接下来的学习和实际的项目开发中也都会用到,所以小伙伴们还是加深一下印象哈。

2. Spring整合web开发【掌握】

接下来的内容就是 SpringFramework 如何整合 web 工程进行实际的开发了。注意现在还没有涉及到 mvc 的东西,所以小伙伴们先不要着急,先回顾一下 Servlet 的东西也未尝不可。

接下来,咱分为基于 web.xml 和 Servlet 3.0 规范的方式,分别讲解 SpringFramework 如何整合 web 开发。

2.1 工程创建

先把项目工程创建出来吧。还是照常,使用 Maven 创建一个新的模块 spring-04-web 就好啦,本章的所有源码也都在这个工程下。

创建出来之后,不要忘了先把打包方式改为 war :

    <groupId>com.linkedbear.spring</groupId>
    <artifactId>spring-04-web</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

之后是添加依赖的部分,既然是 SpringFramework 整合 web ,那必然要多导入一个依赖啦:

<properties>
    <spring.framework.version>5.2.8.RELEASE</spring.framework.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

当然,不要忘记导 Servlet API 的依赖哈。

导入完成后,照例将其先部署到 Tomcat 中:(下面是 IDEA 的配置方法)

入门-Spring整合web与三层架构回顾

入门-Spring整合web与三层架构回顾

配置完成后,启动 Tomcat ,无任何报错则说明一切配置正确,可以接着往下进行了。

2.2 基于web.xml的web整合

首先,咱来准备一点基础代码。

2.2.1 基础代码准备

基础代码也不用折腾的很复杂,只需要一个配置文件,一个 Service 即可,不用写的很麻烦。

public class UserService {
    
    public String get() {
        return "hahaha";
    }
}

spring-web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
    <bean class="com.linkedbear.spring.xml.a_basic.UserService"/>
</beans>

这样基本代码就算准备完成了。

2.2.2 web.xml

下面咱先来编写 web.xml 的基本内容。

上面咱导依赖的时候使用的 Servlet API 是 3.1 ,所以这里咱也是用 3.1 版本即可。先把外壳写上:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

</web-app>

接下来里头该配置啥呢???小册打算先让小伙伴思考几个问题。(此时考虑的所有问题均使用 xml 配置,小伙伴们记得切换思路)

2.2.2.1 问题思考

既然 web.xml 中是对一个 Web 工程的最基本配置,那么如果想让 SpringFramework 也参与其中,应该如何设计?换句话说,现在我们的一个工程中已经有上面的 spring-web.xml 配置文件了,怎么让 web.xml 把 Spring 的配置文件加载上来呢?什么时机加载呢?加载完成后应该放在哪儿呢?

一个一个来思考。首先,加载的方式,可能我们自己去自己创建 ApplicationContext ,读取配置文件,这个貌似不是问题。但实际上,这种做法貌似搞不定,咱先往下思考,三个问题都考虑好之后,小伙伴也就可以理解为什么搞不定了。

其次,加载的时机,可以选择在整个 Web 容器启动时就加载配置文件,也可以在应用第一次被访问时再去加载,但显然前者更合适,因为 Web 容器启动的过程中服务是不对外开放的,只有等应用初始化完成后外界才能访问得到;而使用后者的话,可能应用的初始化耗时太长,导致第一次请求等待时间过长等问题。所以选用 Web 容器启动时就初始化,这种方案更合理一些。那随之又出现一个问题:用什么初始化呢?Servlet 的三个核心组件 Servlet 、Filter 、Listener ,想都不用想肯定是 Listener 最合适,因为前两者触发的时机都比较靠后,而选择合适的 Listener 可以在应用刚开始初始化的时候就触发。所以,对于这个问题,最终的结论是:在 Web 容器刚启动时,借助 Listener 加载配置文件

接下来,IOC 容器加载完成后,放到哪里比较合适呢?Web 中的四大作用域分别是 pageContext 、request 、session 、application ,很明显放在 application 作用域最好吧,因为 IOC 容器本来就是对应着一个应用的,放到 application 域中任何位置都能拿得到。

好,这三个问题考虑完成,下面就可以动手了。

2.2.2.2 编写web.xml配置

在 SpringFramework 中,其实它内置了一个监听器,就是给 web.xml 中提供 IOC 容器初始化时机的,它叫 ContextLoaderListener 。所以我们可以在 web.xml 中配置这样一个监听器:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

哇塞这么简单吗?配置好就 OK 了吗?那咱启动 Tomcat 吧!别高兴得太早,你现在就启动,只能在控制台找到一个异常:

Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/applicationContext.xml]

很明显,它默认会去找 WEB-INF 目录下的 applicationContext.xml 文件。而我们的 xml 配置文件是放在 resources 目录下的,这可咋整?

很简单,在 web.xml 中再添加一个初始化参数:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-web.xml</param-value>
</context-param>

这个参数的名称一看就明白,上下文的配置路径,那不就是指定配置文件的路径嘛。这个配置的值,可以写一个特定的配置文件,也可以写通配符(例如 spring-*.xml ,则会加载所有文件名称符合 spring- 开头,.xml 结尾的文件)。

配置好之后,再启动 Tomcat ,这次就没有报任何异常了,控制台也能打印 IOC 容器初始化成功的日志:

信息 [RMI TCP Connection(3)-127.0.0.1] org.springframework.web.context.ContextLoader.initWebApplicationContext Root WebApplicationContext: initialization started

2.2.3 Servlet的编写

这样干启动了,没法测效果呀,这样咱再写一个 UserServlet ,让它依赖 UserService :

@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
    
    private UserService userService;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String user = userService.get();
        resp.getWriter().println(user);
    }
}

emmmmm 问题又来了:现在这样写,userService 会报空指针异常的,所以 UserServlet 如何才能拿到 UserService 呢?什么时机拿为好呢?

咱刚才在上面分析过了,IOC 容器已经被放到 application 域中,也就是 ServletContext 中了,那既然从进入 Servlet 开始后,任意位置都能拿到 ServletContext ,那我们完全可以在 doGet 中取到 ServletContext ,然后从中取出 UserService ,这样就可以调用它的方法了。

但是。。。有没有更好的办法呢?总不能每次 doGet 都取一次吧?可能这个时候有小伙伴会联想到懒汉单例模式,第一次取到之后就赋值给成员变量不就得了?这样可以是可以,有没有更优雅的方案呢?

哎,Servlet 不是有它自己的生命周期嘛!咱前面 AOP 部分还用到过呢!所以更好地办法,是重写 HttpServlet 的 父类 GenericServlet 的 init 方法,正好这个方法中有一个 ServletConfig ,可以从中取到 ServletContext :

private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
}

接下来就是取 IOC 容器了,SpringFramework 给我们提供了一个工具类:WebApplicationContextUtils ,用它就可以取出 IOC 容器了:

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
    this.userService = ctx.getBean(UserService.class);
}

这样 userService 就有值了,程序应该就可以跑起来了吧。

重新启动 Tomcat ,并在浏览器访问 http://localhost:8080/spring-web/user ,发现可以成功响应 hahaha ,证明编写没有问题。

2.2.4 注解依赖注入

如果 Servlet 依赖的类太多,那 init 里挨个 getBean 那也太费劲了,SpringFramework 还给我们提供了一个支持类,用它可以快速支持 Servlet 的注入:

@Autowired
private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, servletContext);
}

可以发现这样写更简单了吧,甚至都不需要咱自己获取 ApplicationContext 了。重启 Tomcat ,重新访问上面的路径,效果一致,说明效果都是一样的。

以上就是使用 xml 方式整合 web 的基本方式,接下来咱继续介绍注解整合的方式。

2.3 基于Servlet3.0规范的web整合

下面咱先介绍 Servlet 3.0 规范在整合 web 中的应用,然后再讲解具体的整合方式。

2.3.1 Servlet3.0规范节选

从 jcp 的官网,搜索 315 ,可以找到 JSR 315 规范:www.jcp.org/en/jsr/deta… 。

入门-Spring整合web与三层架构回顾

然后,下载第一个 Maintenance Release 里面的 pdf ,就可以得到 Servlet 3.0 的官方文档了。

当然,如果小伙伴能找到中文版的,那就更妙了。作者手里拿到的是 Servlet 3.1 的规范,都是一样看的。

在 Servlet 3.0 规范文档中,找到 8.2.4 节,它介绍的是 Shared libraries / runtimes pluggability ,即 “共享库 / 运行时的可插拔性” ,这里面讲了一个 ServletContainerInitializer 的东西,借助 Java 的 SPI 技术,可以从项目或者项目依赖的 jar 包中,找到一个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件,并加载项目中所有它的实现类。这个家伙就是为了代替 web.xml 的,所以它的加载时机也是在项目的初始化阶段就触发的。

然后,ServletContainerInitializer 这个接口通常会配合 @HandlesTypes 注解一起使用,这个注解中可以传入一些我们所需要的接口 / 抽象类(原文表达的意思是感兴趣的,怕小伙伴们觉得不好理解,所以小册调整了一下意思),支持 Servlet 3.0 规范的 Web 容器会在容器启动项目初始化时,把这些接口 / 抽象类的实现类全部都找出来,整合为一个 Set ,传入 ServletContainerInitializer 的 onStartup 方法参数中,这样我们就可以在 onStartUp 中拿到这些实现类,随机反射创建调用等等的动作。

Servlet 3.0 规范设计这个用法,目的之一是为了做到组件的可插拔,有了组件的可插拔,就可以在导入一个带 ServletContainerInitializer 的实现类的 jar 包时,自动加载对应的实现类。SpringFramework 也利用了这个机制,接下来咱来了解一下 SpringFramework 是如何支持这个规范的。

2.3.2 Spring支持Servlet3.0规范

SpringFramework 当然考虑到了这一点,所以我们翻开 spring-web 的 jar 包中,就可以找到这个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件:

入门-Spring整合web与三层架构回顾

它里面定义好的那个类是这个家伙:

org.springframework.web.SpringServletContainerInitializer

翻开它的源码,可以发现它需要的实现类的接口类型是 WebApplicationInitializer :

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer

所以我们只需要编写一个 WebApplicationInitializer 的实现类,就可以搞定咯。

不过先别着急,SpringFramework 可不会只想到这些,它帮我们封装了一个 AbstractContextLoaderInitializer 的抽象类,这里面实现了大部分逻辑,我们要做的,只是如何创建 IOC 容器,使用哪些注解配置类,扫描哪些包,仅此而已。

2.3.3 WebApplicationInitializer的编写

明白了整合的套路,那咱接下来就编写一个 WebApplicationInitializer 的实现类,放在哪里无所谓:

public class DemoWebApplicationInitializer extends AbstractContextLoaderInitializer {
    
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        // ???
    }
}

接下来就是如何创建 IOC 容器了。这里我们要做的,就是编程式的加载那些 Spring 的 xml 配置文件,或者注解配置类。所以我们可以这样编写:

@Override
protected WebApplicationContext createRootApplicationContext() {
    AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
    ctx.register(UserConfiguration.class);
    return ctx;
}

对应的,再编写一个 UserConfiguration 的配置类:

@Configuration
public class UserConfiguration {
    
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

这样就写完了,不需要在 DemoWebApplicationInitializer 中再标注什么东西了。

但是!记得一点,把 web.xml 的东西全部注释掉!不然会产生两个 IOC 容器的。。。

注释掉之后,重启 Tomcat ,再次访问 /user2 路径,发现仍然可以正常在浏览器上响应 hahaha ,证明这种基于 Servlet 3.0 规范的方法也整合成功了。

以上就是两种整合的方式,相对都比较简单,后面的 SpringWebMvc 中,咱也会讲解用这两种整合方式来搭建基于 SpringWebMvc 的工程。

终于到了 SpringFramework 学习的下一站,咱要对 SpringFramework 中的一个非常非常重要,重要到好多人都把它单独拿出来跟 Spring 放到一起论述的,那就是 SpringWebMvc 。作为 Web 部分的入门章,小册要回顾一下三层架构,然后学习一下 SpringFramework 如何整合 Web 开发。

1. MVC三层架构【回顾】

关于三层架构的演进过程,小册就不展开讲解了,咱直接来回顾 MVC 三层架构的最终形态就 OK ,毕竟前面的过程都是不完全形态,也没必要再调动起回忆了。

1.1 MVC

MVC 想必各位都很熟悉了,MVC 的三个字母分别代表 Model View Controller ,它们在一个应用中的地位和职责应该是下面图的样子:

入门-Spring整合web与三层架构回顾

如果回忆不起来的小伙伴一定要加深印象,后面咱学习 SpringWebMvc 的时候还会用它的。

1.2 三层架构

JavaEE 中的三层架构,是为了实现代码逻辑解耦的经典设计模式,这个想必咱也都很熟悉了。三层架构分别为 web 层、service 层、dao 层,它们的职责及调用逻辑可以用下图表示:

入门-Spring整合web与三层架构回顾

这个虽说是老生常谈了,接下来的学习和实际的项目开发中也都会用到,所以小伙伴们还是加深一下印象哈。

2. Spring整合web开发【掌握】

接下来的内容就是 SpringFramework 如何整合 web 工程进行实际的开发了。注意现在还没有涉及到 mvc 的东西,所以小伙伴们先不要着急,先回顾一下 Servlet 的东西也未尝不可。

接下来,咱分为基于 web.xml 和 Servlet 3.0 规范的方式,分别讲解 SpringFramework 如何整合 web 开发。

2.1 工程创建

先把项目工程创建出来吧。还是照常,使用 Maven 创建一个新的模块 spring-04-web 就好啦,本章的所有源码也都在这个工程下。

创建出来之后,不要忘了先把打包方式改为 war :

    <groupId>com.linkedbear.spring</groupId>
    <artifactId>spring-04-web</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

之后是添加依赖的部分,既然是 SpringFramework 整合 web ,那必然要多导入一个依赖啦:

<properties>
    <spring.framework.version>5.2.8.RELEASE</spring.framework.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.framework.version}</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

当然,不要忘记导 Servlet API 的依赖哈。

导入完成后,照例将其先部署到 Tomcat 中:(下面是 IDEA 的配置方法)

入门-Spring整合web与三层架构回顾

入门-Spring整合web与三层架构回顾

配置完成后,启动 Tomcat ,无任何报错则说明一切配置正确,可以接着往下进行了。

2.2 基于web.xml的web整合

首先,咱来准备一点基础代码。

2.2.1 基础代码准备

基础代码也不用折腾的很复杂,只需要一个配置文件,一个 Service 即可,不用写的很麻烦。

public class UserService {
    
    public String get() {
        return "hahaha";
    }
}

spring-web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
    <bean class="com.linkedbear.spring.xml.a_basic.UserService"/>
</beans>

这样基本代码就算准备完成了。

2.2.2 web.xml

下面咱先来编写 web.xml 的基本内容。

上面咱导依赖的时候使用的 Servlet API 是 3.1 ,所以这里咱也是用 3.1 版本即可。先把外壳写上:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

</web-app>

接下来里头该配置啥呢???小册打算先让小伙伴思考几个问题。(此时考虑的所有问题均使用 xml 配置,小伙伴们记得切换思路)

2.2.2.1 问题思考

既然 web.xml 中是对一个 Web 工程的最基本配置,那么如果想让 SpringFramework 也参与其中,应该如何设计?换句话说,现在我们的一个工程中已经有上面的 spring-web.xml 配置文件了,怎么让 web.xml 把 Spring 的配置文件加载上来呢?什么时机加载呢?加载完成后应该放在哪儿呢?

一个一个来思考。首先,加载的方式,可能我们自己去自己创建 ApplicationContext ,读取配置文件,这个貌似不是问题。但实际上,这种做法貌似搞不定,咱先往下思考,三个问题都考虑好之后,小伙伴也就可以理解为什么搞不定了。

其次,加载的时机,可以选择在整个 Web 容器启动时就加载配置文件,也可以在应用第一次被访问时再去加载,但显然前者更合适,因为 Web 容器启动的过程中服务是不对外开放的,只有等应用初始化完成后外界才能访问得到;而使用后者的话,可能应用的初始化耗时太长,导致第一次请求等待时间过长等问题。所以选用 Web 容器启动时就初始化,这种方案更合理一些。那随之又出现一个问题:用什么初始化呢?Servlet 的三个核心组件 Servlet 、Filter 、Listener ,想都不用想肯定是 Listener 最合适,因为前两者触发的时机都比较靠后,而选择合适的 Listener 可以在应用刚开始初始化的时候就触发。所以,对于这个问题,最终的结论是:在 Web 容器刚启动时,借助 Listener 加载配置文件

接下来,IOC 容器加载完成后,放到哪里比较合适呢?Web 中的四大作用域分别是 pageContext 、request 、session 、application ,很明显放在 application 作用域最好吧,因为 IOC 容器本来就是对应着一个应用的,放到 application 域中任何位置都能拿得到。

好,这三个问题考虑完成,下面就可以动手了。

2.2.2.2 编写web.xml配置

在 SpringFramework 中,其实它内置了一个监听器,就是给 web.xml 中提供 IOC 容器初始化时机的,它叫 ContextLoaderListener 。所以我们可以在 web.xml 中配置这样一个监听器:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

哇塞这么简单吗?配置好就 OK 了吗?那咱启动 Tomcat 吧!别高兴得太早,你现在就启动,只能在控制台找到一个异常:

Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/applicationContext.xml]

很明显,它默认会去找 WEB-INF 目录下的 applicationContext.xml 文件。而我们的 xml 配置文件是放在 resources 目录下的,这可咋整?

很简单,在 web.xml 中再添加一个初始化参数:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-web.xml</param-value>
</context-param>

这个参数的名称一看就明白,上下文的配置路径,那不就是指定配置文件的路径嘛。这个配置的值,可以写一个特定的配置文件,也可以写通配符(例如 spring-*.xml ,则会加载所有文件名称符合 spring- 开头,.xml 结尾的文件)。

配置好之后,再启动 Tomcat ,这次就没有报任何异常了,控制台也能打印 IOC 容器初始化成功的日志:

信息 [RMI TCP Connection(3)-127.0.0.1] org.springframework.web.context.ContextLoader.initWebApplicationContext Root WebApplicationContext: initialization started

2.2.3 Servlet的编写

这样干启动了,没法测效果呀,这样咱再写一个 UserServlet ,让它依赖 UserService :

@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
    
    private UserService userService;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String user = userService.get();
        resp.getWriter().println(user);
    }
}

emmmmm 问题又来了:现在这样写,userService 会报空指针异常的,所以 UserServlet 如何才能拿到 UserService 呢?什么时机拿为好呢?

咱刚才在上面分析过了,IOC 容器已经被放到 application 域中,也就是 ServletContext 中了,那既然从进入 Servlet 开始后,任意位置都能拿到 ServletContext ,那我们完全可以在 doGet 中取到 ServletContext ,然后从中取出 UserService ,这样就可以调用它的方法了。

但是。。。有没有更好的办法呢?总不能每次 doGet 都取一次吧?可能这个时候有小伙伴会联想到懒汉单例模式,第一次取到之后就赋值给成员变量不就得了?这样可以是可以,有没有更优雅的方案呢?

哎,Servlet 不是有它自己的生命周期嘛!咱前面 AOP 部分还用到过呢!所以更好地办法,是重写 HttpServlet 的 父类 GenericServlet 的 init 方法,正好这个方法中有一个 ServletConfig ,可以从中取到 ServletContext :

private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
}

接下来就是取 IOC 容器了,SpringFramework 给我们提供了一个工具类:WebApplicationContextUtils ,用它就可以取出 IOC 容器了:

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
    this.userService = ctx.getBean(UserService.class);
}

这样 userService 就有值了,程序应该就可以跑起来了吧。

重新启动 Tomcat ,并在浏览器访问 http://localhost:8080/spring-web/user ,发现可以成功响应 hahaha ,证明编写没有问题。

2.2.4 注解依赖注入

如果 Servlet 依赖的类太多,那 init 里挨个 getBean 那也太费劲了,SpringFramework 还给我们提供了一个支持类,用它可以快速支持 Servlet 的注入:

@Autowired
private UserService userService;

@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ServletContext servletContext = config.getServletContext();
    SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, servletContext);
}

可以发现这样写更简单了吧,甚至都不需要咱自己获取 ApplicationContext 了。重启 Tomcat ,重新访问上面的路径,效果一致,说明效果都是一样的。

以上就是使用 xml 方式整合 web 的基本方式,接下来咱继续介绍注解整合的方式。

2.3 基于Servlet3.0规范的web整合

下面咱先介绍 Servlet 3.0 规范在整合 web 中的应用,然后再讲解具体的整合方式。

2.3.1 Servlet3.0规范节选

从 jcp 的官网,搜索 315 ,可以找到 JSR 315 规范:www.jcp.org/en/jsr/deta… 。

入门-Spring整合web与三层架构回顾

然后,下载第一个 Maintenance Release 里面的 pdf ,就可以得到 Servlet 3.0 的官方文档了。

当然,如果小伙伴能找到中文版的,那就更妙了。作者手里拿到的是 Servlet 3.1 的规范,都是一样看的。

在 Servlet 3.0 规范文档中,找到 8.2.4 节,它介绍的是 Shared libraries / runtimes pluggability ,即 “共享库 / 运行时的可插拔性” ,这里面讲了一个 ServletContainerInitializer 的东西,借助 Java 的 SPI 技术,可以从项目或者项目依赖的 jar 包中,找到一个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件,并加载项目中所有它的实现类。这个家伙就是为了代替 web.xml 的,所以它的加载时机也是在项目的初始化阶段就触发的。

然后,ServletContainerInitializer 这个接口通常会配合 @HandlesTypes 注解一起使用,这个注解中可以传入一些我们所需要的接口 / 抽象类(原文表达的意思是感兴趣的,怕小伙伴们觉得不好理解,所以小册调整了一下意思),支持 Servlet 3.0 规范的 Web 容器会在容器启动项目初始化时,把这些接口 / 抽象类的实现类全部都找出来,整合为一个 Set ,传入 ServletContainerInitializer 的 onStartup 方法参数中,这样我们就可以在 onStartUp 中拿到这些实现类,随机反射创建调用等等的动作。

Servlet 3.0 规范设计这个用法,目的之一是为了做到组件的可插拔,有了组件的可插拔,就可以在导入一个带 ServletContainerInitializer 的实现类的 jar 包时,自动加载对应的实现类。SpringFramework 也利用了这个机制,接下来咱来了解一下 SpringFramework 是如何支持这个规范的。

2.3.2 Spring支持Servlet3.0规范

SpringFramework 当然考虑到了这一点,所以我们翻开 spring-web 的 jar 包中,就可以找到这个 /META-INF/services/javax.servlet.ServletContainerInitializer 的文件:

入门-Spring整合web与三层架构回顾

它里面定义好的那个类是这个家伙:

org.springframework.web.SpringServletContainerInitializer

翻开它的源码,可以发现它需要的实现类的接口类型是 WebApplicationInitializer :

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer

所以我们只需要编写一个 WebApplicationInitializer 的实现类,就可以搞定咯。

不过先别着急,SpringFramework 可不会只想到这些,它帮我们封装了一个 AbstractContextLoaderInitializer 的抽象类,这里面实现了大部分逻辑,我们要做的,只是如何创建 IOC 容器,使用哪些注解配置类,扫描哪些包,仅此而已。

2.3.3 WebApplicationInitializer的编写

明白了整合的套路,那咱接下来就编写一个 WebApplicationInitializer 的实现类,放在哪里无所谓:

public class DemoWebApplicationInitializer extends AbstractContextLoaderInitializer {
    
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        // ???
    }
}

接下来就是如何创建 IOC 容器了。这里我们要做的,就是编程式的加载那些 Spring 的 xml 配置文件,或者注解配置类。所以我们可以这样编写:

@Override
protected WebApplicationContext createRootApplicationContext() {
    AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
    ctx.register(UserConfiguration.class);
    return ctx;
}

对应的,再编写一个 UserConfiguration 的配置类:

@Configuration
public class UserConfiguration {
    
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

这样就写完了,不需要在 DemoWebApplicationInitializer 中再标注什么东西了。

但是!记得一点,把 web.xml 的东西全部注释掉!不然会产生两个 IOC 容器的。。。

注释掉之后,重启 Tomcat ,再次访问 /user2 路径,发现仍然可以正常在浏览器上响应 hahaha ,证明这种基于 Servlet 3.0 规范的方法也整合成功了。

以上就是两种整合的方式,相对都比较简单,后面的 SpringWebMvc 中,咱也会讲解用这两种整合方式来搭建基于 SpringWebMvc 的工程。

 

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » 入门-Spring整合web与三层架构回顾