IOC容器实际上就是一个工厂设计模式里的工厂, 当然还强化了很多功能. 这个工厂在启动容器的时候根据配置来创建好所有的Bean, 然后向工厂就可以获取这些Bean来进行使用.

在最早接触Spring的时候, 就听到说这个框架不仅仅可以用于Web应用, 但当时只是按部就班的学习如何编写Web程序, 对于Java的理解也不够深.

现在终于明白了这个东西的本质就是容器, 容器可以单独被外部使用, 而套上了Servlet的外皮, 和ServletContext互相引用之后, 就成了Web应用的框架.

关于依赖注入的几种方式比如构造器注入, setter方法注入, 这不是属于Spring 特有的内容, 这次RE就要来深入看看Spring的东西.

  1. Resource接口
  2. IOC容器
  3. BeanFactory
  4. ApplicationContext
  5. WebApplicationContext
  6. IOC容器的思考

Resource接口

Resource接口是Spring提供的, 访问一切资源的抽象接口. 有很多具体实现类. 这些类是按照所加载的资源的不同类型来区分的, 有些加载二进制数据, 有些加载文件, 有些加载URL对应的资源.

实际上可以Resource接口对于Spring的意义就好比File对于Java的意义, 都是提供了可供操作的资源的一种抽象.

在使用这些接口的具体实现类的时候, 根据要加载的资源不同, 可以使用不同的方式. 这里我在IDEA里直接选创建Spring项目(但不要选JavaEE-Web)项目, IDEA会自动创建一个项目并在lib中下载好Spring 4.3.18 的一系列包.

然后可以尝试来使用各种Resource类型:

package cc.conyli;

import org.springframework.core.io.*;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.util.FileCopyUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;

public class ResourceLearn {

    public static void main(String[] args) throws IOException {

        //本地文件系统加载
        FileSystemResource resource1 = new FileSystemResource("D:\\test.txt");

        if (resource1.exists()) {
            //可以获取文件名和长度
            System.out.println(resource1.getFilename() + "的字节数是" + resource1.contentLength());

            //可以获取输入流
            InputStream in = resource1.getInputStream();
            //可以获取输出流, 因为FileSystemResource实现了WritableResource这个接口
            //实现这个接口的有 FileSystemResource, FileUrlResource, PathResource, 注意, 如果使用Resource类型则无法多态调用这个getOutPutStream()方法
            OutputStream out = resource1.getOutputStream();
            //可以获取File对象, 不过注意, 如果查找的文件实际上位于一个jar包中, 则getFile()会报异常, 因为不存在文件系统中的对应文件, 要读取该文件改用getInputStream打开输入流即可
            File textTxt = resource1.getFile();
            //可以在资源的相对地址上创建新文件
            Resource newFile = resource1.createRelative("text2.text");

        }

        //类路径加载, 所谓类路径, 就是寻找类的路径, 在WEB应用下是/lib和/WEB-INF/classes作为类路径, 而在普通程序里, 编译的程序包根目录就是类路径
        // 由于不是web应用, 在 src下创建一个 test2.html, 使用类路径加载器, 此时要用相对classpath的相对路径来写
        Resource resource2 = new ClassPathResource("test2.html");

        if (resource2.exists()) {
            System.out.println(resource2.getFilename() + "的字节数是" + resource2.contentLength());
            //也可以获取input, 无法获取output
            InputStream in2 = resource2.getInputStream();
            //也可以获取File对象
            File test2HTML = resource2.getFile();
            //还可以通过装饰器来指定编码, 这个装饰器可以直接获取字符流
            EncodedResource encodedResource = new EncodedResource(resource2, StandardCharsets.UTF_8);
            //FileCopyUtil是org.springframework.util提供的工具, 看来这个工具包也有不少东西可以用
            String content = FileCopyUtils.copyToString(encodedResource.getReader());
            System.out.println(content);

        }

        //还可以引用URL网络资源
        Resource resource3 = new UrlResource("https://conyli.cc");
        if (resource3.exists()) {
            System.out.println(resource3.getURI());
        }

    }
}

如果对于具体的类很了解, 是可以直接使用对应类的. 后来根据新加的Path类带来的PathResource类可以打开URL和本地文件资源. 但还是有点烦, 有没有一种更统一的只使用一个类加载文件呢. 答案是有的.

在了解统一的加载方式之前先来看两种简化方式, 第一种是资源地址表达式, 第二种是Ant风格通配符:

  1. 资源地址表达式
    1. classpath:, 从类路径中加载
    2. classpath*:, 从类路径中加载, 扫描全部的相同的路径和包内路径
    3. file:, 从文件系统中加载,其后可以跟相对或者绝对路径
    4. http:, 从网络加载
    5. ftp:, 从ftp加载
    6. 无前缀, 根据具体的ApplicationContext而定
  2. Ant风格资源地址
    1. ?, 匹配一个字符
    2. *, 匹配任意字符
    3. **, 匹配任意多层路径

统一加载资源的接口叫做ResourceLoader, 在此基础上又扩展一个接口叫做ResourcePatternResolver, 听名字就知道可以根据字符形式的路径解析, 实现类是在此基础上的PathMatchingResourcePatternResolver

前两个接口的区别是, ResourceLoader只能使用资源地址表达式, ResourcePatternResolver可以使用资源地址表达式加上Ant通配符. 针对上边的例子, 修改如下:

package cc.conyli;

import org.springframework.core.io.*;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.FileCopyUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;

public class ResourceLearn {

    public static void main(String[] args) throws IOException {

        Resource resource1 = new PathMatchingResourcePatternResolver().getResource("file:d:\\test.txt");
        if (resource1.exists()) {
            System.out.println(resource1.getFilename() + "的字节数是" + resource1.contentLength());

        }

        Resource resource2 = new PathMatchingResourcePatternResolver().getResource("test2.html");
        if (resource2.exists()) {
            System.out.println(resource2.getFilename() + "的字节数是" + resource2.contentLength());
            InputStream in2 = resource2.getInputStream();
            File test2HTML = resource2.getFile();
            EncodedResource encodedResource = new EncodedResource(resource2, StandardCharsets.UTF_8);
            String content = FileCopyUtils.copyToString(encodedResource.getReader());
            System.out.println(content);
        }

        Resource resource3 = new PathMatchingResourcePatternResolver().getResource("https://conyli.cc");
        if (resource3.exists()) {
            EncodedResource encodedResource = new EncodedResource(resource3, StandardCharsets.UTF_8);
            String content = FileCopyUtils.copyToString(encodedResource.getReader());
            System.out.println(content);
        }
    }
}

搞完了Resource, 就可以来看看如何启动IOC容器, 也就是Spring创建Bean的工厂了.

IOC容器

IOC容器最基础的接口有两个, 一个是org.springframework.beans.factory.BeanFactory, 一个是 org.springframework.context.ApplicationContext.

学过了前边Java Web会知道, 后边那个很像ServletContext, 实际上二者的含义也很相似. ApplicationContext代表的是IOC容器, 创建与BeanFactory之上.

可以说BeanFactory更像是一个为Spring其他组件提供基础服务的对象, 而ApplicationContext提供了更多面向应用的功能, 所以一般使用, 都会使用ApplicationContext.

不过既然要研究一下容器, 这两个东西还是都要来看看. 在之前Java Web的时候已经知道, 需要进行一定的配置, 才能启动容器, 容器启动的时候就会将其中使用到的东西组装和设置好.

Spring的IOC容器也是类似原理, 需要想办法让容器知道配置在哪里, 需要组装哪些类, 然后用一个命令启动容器并且获取容器的引用, 就可以通过容器获取Bean了.

所以启动IOC容器的套路就是:

  1. 编写好配置文件
  2. 将配置文件弄成一个Resource东西以供Spring使用
  3. 通过IOC容器的具体实现类加载Resource对象, 启动容器

BeanFactory

BeanFactory下边有一堆继承体系, 就不放了. 最常用的启动IOC容器的类是XmlBeanDefinitionReader, 顾名思义, 这是一个读取XML配置然后启动IOC容器的类, 具体来说, 这个类构造的时候注入一个Factory系列的实现类(常用的是DefaultListableBeanFactory), 然后加载配置, 之后其中的Factory就是一个IOC容器了.. 下边就来实践一下.

首先要编写一个让IOC容器进行组装的类, 简单一点好了, 就以博主打算要玩的下一个游戏SD GUNDAM G 世纪 Cross Rays为例编写一个Game类:

package cc.conyli;

public class Game {

    private int price;

    private String name;

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

这个类再简单不过了. 然后创建一个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"
           xmlns:p="http://www.springframework.org/schema/p">

    <bean id="sdgggcr" class="cc.conyli.Game" p:name="SDGGGCR" p:price="476"/>

</beans>

之后要做的是把这个XML文件加载进一个Resource对象, 然后再用对应的类来启动IOC容器

package cc.conyli;

import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.*;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import java.io.*;

public class ResourceLearn {

    public static void main(String[] args) throws IOException {

        //加载XML配置文件, 注意路径
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource xmlConfigFile = resolver.getResource("classpath:cc/conyli/beans.xml");
        System.out.println(xmlConfigFile.getURL());
        System.out.println(xmlConfigFile.getFilename());

        //启动IOC容器的步骤
        //第一步, 创建一个DefaultListableBeanFactory对象
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        //第二步, 创建XmlBeanDefinitionReader
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        //第三步, 加载配置文件, 这一步实际上就启动了IOC容器, 通过factory就可以获取Bean并使用了.
        reader.loadBeanDefinitions(xmlConfigFile);

        //通过工厂获取Bean并且使用
        Game sdgggcr = factory.getBean("sdgggcr", Game.class);

        System.out.println(sdgggcr.getName());
        System.out.println(sdgggcr.getPrice());
    }
}

吐槽一下SDGGGCR的豪华版还真的够贵, 其中的派遣任务似乎不是正版还没法玩….还是先看工厂吧, 通过XmlBeanDefinitionReader的工作, 加载完配置文件之后, 就可以从工厂中获取对应的Bean了.

这里如果继续深究细节, 可以知道工厂的默认配置就是单例模式, 也可以更改成Prototype就是每次都创建新对象的模式.有了这个例子, 对于如何使用Spring框架的认识就更深了. 由于Web容器中的Servlet天生也是要求单例, 所以这个工厂稍加改动就可以和Web应用配合. 但是这里的最大意义是我们没有通过Web应用来启动IOC容器, 而是单独启动了.

这就意味着如果想使用单例和(或)工厂模式来装配类, 除了自己编写代码之外, 也可以使用Spring框架.

ApplicationContext

从上边的知识中可以了解到这个本质上也是一个IOC容器, 所以启动的本质也是一样, 只不过还有更多的额外功能. 这个也有一堆继承体系, 不过最核心的是两个实现类:

  1. ClassPathXmlApplicationContext, 从类路径加载配置文件
  2. FileSystemXmlApplicationContext, 从文件路径加载配置文件
  3. AnnotationConfigApplicationContext, 从文件路径加载配置文件

ApplicationContext的使用更加简单, 上边BeanFactory的例子可以改写如下:

package cc.conyli;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.*;

public class ResourceLearn {

    public static void main(String[] args) throws IOException {
        //直接使用classpath加载文件
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:cc/conyli/beans.xml");
        //之后就是一个IOC容器了, 可以获取Bean并使用
        Game sdgggcr = context.getBean("sdgggcr", Game.class);

        System.out.println(sdgggcr.getName());
        System.out.println(sdgggcr.getPrice());
    }
}

如果追究细节, 一样可以配置成单例或者是Prototype模式, 还有不同的就是BeanFactory系列是惰性加载Bean, 而ApplicationContext在初始化过程中就完全装配好Bean了.

从文件路径加载并启动容器的方式和从类路径加载并启动一样, Spring 现在还支持通过配置类启动的方式, 我们直接将当前的类改造成一个配置类:

package cc.conyli;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.*;

@Configuration
public class ResourceLearn {

    @Bean(name = "sdgggcr")
    public Game createBean() {
        Game game = new Game();
        game.setName("SDGGGCR");
        game.setPrice(328);
        return game;
    }

    public static void main(String[] args) throws IOException {

        //使用AnnotationConfigApplicationContext加载配置类
        ApplicationContext context = new AnnotationConfigApplicationContext(cc.conyli.ResourceLearn.class);

        //之后一样获取并使用Bean
        Game sdgggcr = context.getBean("sdgggcr", Game.class);

        System.out.println(sdgggcr.getName());
        System.out.println(sdgggcr.getPrice());
    }
}

上边红色的部分就是配置类的注解, 我也不是第一天学Spring, 应该基本上都了解. 这么改造之后, 就选用加载类配置的另外一个容器启动器来加载配置类, 完成之后, 依然可以获取Bean并使用.

其他还有什么Groovy加载器, 可想而知也是先弄好配置文件, 再加载了.

到这里为止, 已经可以知道如何启动IOC容器了, Spring提供的IOC容器就是一个功能强大的组装Bean的工具, 没有和什么东西捆绑死.

既然已经研究了IOC容器, 就可以来看看IOC容器在Web应用里是怎么协同Web容器工作的了.

WebApplicationContext

知道了Web容器, 也知道了IOC容器, Web应用中使用Spring, 实际上就相当于有两个盒子, Web盒子里装满了Servlet, IOC盒子里装满了一堆组装的Bean, 要怎么能让Web里的东西用到IOC盒子里的东西呢.

想一下就会知道, 只要Web盒子里知道了IOC盒子的引用, 就可以任意的取出Bean来操作, 如果这些Bean恰好也接受一个Http请求, 返回一个Http相应, 那拿来就和用Servlet没什么区别.

所以在Java Web中应用Spring, 所做的事情就是按照Java Web标准启动Web容器的时候, 把IOC容器也启动了, 让两个容器互相知道彼此就可以了.

为了在Web中使用, Spring的ApplicationContext体系还扩展出了一派WebApplicationContext体系, 专供Web使用. 相比之下可以知道我们前边启动的容器其实是通用容器.

要说WebApplicationContext, 本质也没有什么特别, 只是加了一个特别的常量名称, 叫做 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, 通过ServletContext中获取这个键名, 就可以拿到IOC容器的引用.

然后你可能就会问, 那么Spring是什么时候把这个玩意放入到ServletContext中的呢, 还记得Web容器启动的时候吗, 可以配置成实例化一些Servlet和监听器, 只要在某个Servlet里或者监听器里使用了Spring提供的Servlet和监听器, 就可以让Web容器知道IOC容器啦.

再继续想, 如果是你来编写这个Servlet的话, 一定也会把Web容器的引用放到IOC容器里, 没错, IOC容器的 getServletContext()方法就可以获得使用当前IOC容器的Web容器引用.

这样两个容器互相都知道彼此, Web容器启动的时候, 也把IOC容器一起启动了. 这样当Web容器启动完成的时候, Web容器和IOC容器都已经就绪, Web容器接受到的所有请求, 都会被Spring放在Web容器中的Servlet拦截, 然后调用IOC容器中的Bean进行处理. 这就是Spring框架用于Web应用的真谛.

让Web容器启动的时候也启动IOC容器有很多种办法, 之前你肯定已经想到了, 网上各种Spring教程都会让你在web.xml里添加一个:

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>


    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/spring-mvc-demo-servlet.xml</param-value>
    </init-param>

    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

这个DispatcherServlet就会在后台启动一个IOC容器, 并且按照前边其中的xml文件进行配置, 然后设置好相互引用. 果然, 现在回头看看今年年初的Spring学习, 当时的感受还没有这么深刻.

除了插入Servlet的方法之外, 还可以通过监听器来启动容器, Spring提供了一个监听器:



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

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/conyli.xml, /WEB-INF/conylibase.xml</param-value>
</context-param>

啧啧, 看这名字, 如果说上边的Servlet名称还比较隐晦的话, 这个监听器的名称真的是把自己要做的活解释的非常清楚了. 当然, 因为是Spring提供的监听器, 必须遵照其要求配置Spring配置文件的路径.

这里再介绍两个一般也用不上的(因为现在直接显式配置的不多了, 大家都用Spring Boot了), 一是最原始的启动Spring容器的Servlet, 二是通过Java配置类启动IOC容器, 当然, 都必须要写在Web.xml里随着web容器一起启动.

DispatcherServlet启动的时候要求将路径全部转发给Spring容器处理. 如果咱就想启动IOC, 暂时用不到路径转发, 可以改用最原始的Servlet类:

<servlet>
    <servlet-name>springContextLoaderServlet</servlet-name>
    <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>

    <load-on-startup>1</load-on-startup>
</servlet>

这名字一下就标准起来了, 设置上自启动后, 这个Servlet就会启动IOC容器, 无需设置对应的Mapping.

通过配置类加载的时候, 是通过监听器实现的, 本身ContextLoaderListener监听器会正常启动IOC容器, 但是只要给其设置一个特殊的Web容器的全局变量, 就可以让其去加载配置类而不是xml配置文件:

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


<context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>cc.conyli.config.BaseConfig</param-value>
</context-param>

和上边的加载配置文件的监听器一比较, 你就会发现不同之处了. 看到这里我也是黑人恍然大悟逐渐发笑, 果然越来越清晰了, 终于非常清楚的搞明白了Spring容器与Web容器的关系.

IOC容器的思考

终于搞清楚了IOC容器, 其实这里的思考就是如何实现. 要编写一个最简单的IOC容器, 需要编写一个工厂类, 通过反射的方式, 组装Bean.

组装的Bean可以通过一个Map集合, 保存名称和对应的引用, 每次使用Bean之前, 先通过Map检索, 这样保证可以返回单例.

当然还可能需要写一些解析配置文件的类作为工具类, 提供给核心的工厂类来使用.

这样粗略的算一下, 一个工厂类, 一个类似容器类的管理类, 外加读取配置文件的核心类, 其实就可以组成一个IOC容器了. 确实有意思.

而且上边的代码里, 回头想想, Factory都是可以new的, 说明可以创建一个一个彼此独立的容器作为父子容器, 嗯确实不错.

IOC容器总算是搞明白了, 继续!