Spring RE 01 IOC容器

Spring RE 01 IOC容器

IOC容器实际上就是一个工厂设计模式里的工厂, 当然还强化了很多功能. 这个工厂在启动容器的时候根据配置来创建好所有的Bean, 然后向工厂就可以获取这些Bean来进行使用. 在最早接触Spring的时候, 就听到说这个框架不仅仅可以用于Web应用, 但当时只是按部就班的学习如何编写Web程序, 对

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容器总算是搞明白了, 继续!
LICENSED UNDER CC BY-NC-SA 4.0
Comment