您的位置:首页 > 编程语言 > Java开发

spring3.x第三章 IOC容器概述

2017-02-07 13:14 375 查看

3.1 IOC概述

  IOC(Inverse of Control)是Spring容器的内核,AOP、声明式事务等功能在此基础上开花结果。

3.1.1 通过实例理解IOC的概念

  电影<墨攻>中,刘德华扮演的墨者革离达到都城下,城上问道:“来者何人?”,刘德华回答:“墨者革离”。

public class MoAttack{
public void cityGateAsk(){
//1.演员直接侵入剧本
LiuDeHua ldh = new LiuDeHua()
ldh.responseAsk("墨者革离");
}
}


  在剧本1处,作为具体角色扮演者的刘德华直接侵入到剧本中,使剧本和演员直接耦合在一起。

  一个明智的编剧在剧情创作时应围绕故事的角色进行,而不是应该考虑角色具体的扮演者,这样就可以自由的选择任何合适的演员,而非绑定到刘德华一人身上,通过以上分析,我们直到需要为该剧本主人公革离定义一个接口:

public class MoAttack{
public void cityGateAsk(){
//1.引入革离角色接口
GeLi geli = new LiuDeHua();
//2.通过接口开展剧情
geli.responseAsk("墨者革离");
}
}


  在1处引入了剧本的角色——革离,剧本的情节通过角色展开,而在拍摄时角色由演员饰演,如2处。但是,MoAttack同时依赖于GeLi接口和LiuDeHua类,并没有达到我们所期望的剧本仅依赖角色的目的。但是角色最终必须通过具体的演员才能完成拍摄,如何让LiuDeHua和剧本无关而又完成GeLi的具体动作呢?当然是在影片真正投拍时,让
导演
将剧本、角色、饰演者装配起来。

  通过引入了导演,使剧本和具体饰演者解耦了。对应到软件中,导演像是一个装配器,安排演员表演具体的角色。

  IOC包括两个内容:

* 控制

* 反转。

  那到底是什么东西的”控制”被”反转”了呢?对应到前面的例子,”控制”是指选择GeLi角色扮演者的控制权;”反转”是指这种控制权从<墨攻>剧本中移除,转交到导演的手中。对于软件来说,即是某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定。

  因为IOC不够开门见山,因此提出了DI(依赖注入:Dependency Injection)的概念用以代替IOC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。

3.1.2 IOC的类型

  从注入方法上看,主要分为三种类型:构造函数注入、属性注入和接口注入。Spring支持构造函数注入和属性注入。

  构造函数注入

public class MoAttack{
private GeLi geli;
public MoAttack(GeLi geli){
this.geli = geli;
}
public void CityGateAsk(){
geli.responseAsk("墨者革离");
}
}
//接着是Director;通过构造函数注入革离扮演者
public class Director{
public void direct(){
Geli ldh = new LiDeHua();
MoAttack m = new MoAttack(ldh);
m.CityGateAsk();
}
}


  属性注入

  有时,导演会发现,有的情节不需要革离的出现,这种情况下通过构造函数注入并不妥当(读者:可以通过构造器),这时可以考虑使用属性注入,更加的灵活:

public class MoAttack{
private GeLi geli;
public void SetGeli(GeLi geli){
this.geli = geli;
}
public void CityGateAsk(){
geli.responseAsk("墨者革离");
}
}
//接着是Director;通过构造函数注入革离扮演者
public class Director{
public void direct(){
Geli ldh = new LiDeHua();
MoAttack m = new MoAttack();
m.setGeLi(ldh);
m.CityGateAsk();
}
}


3.1.3 通过容器完成依赖关系

  虽然MoAttack和LiuDeHua实现了解耦,MoAttack无须关注角色实现类的实例化工作,但这些工作在代码中依然存在,只是转移到Director类中而已。假设,在选择某个剧后,希望通过一个“海选“或者第三中介机构来选择导演、演员。那剧本、导演、演员都实现了解耦。

  所谓”海选“和第三方中介机构在程序领域即是一个第三方的容器,它帮助完成类的初始化与装配工作,让开发者从这些底层实现类的实例化、依赖关系装配等工作中脱离出来,专注于业务逻辑开发工作。Spring就是这样的一个容器,它通过配置文件或注解描述类和类之间的依赖关系,自动完成类的初始化和依赖注入的工作。下面是对以上实例进行配置的配置文件片段:

<!-- 实现类实例化 -->
<bean id = "geli" class="LiuDeHua"/>
<!-- 通过geli-ref建立依赖关系 -->
<bean id = "moAttack" class="com.baobaotao.ioc.MoAttack"
p:geli-ref="geli"/>


  通过
new XmlBeanFactory("Beans.xml")
等方式即可启动容器,自动实例化Bean并完成依赖关系的装配。主要通过反射。

3.2 相关Java基础知识

  Java语言允许通过程序化的方式间接对Class的对象实例操作,Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数、属性和方法等。Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能。

package com.baobaotao.reflect;

public class Car {

public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}

private String brand;
private String color;
private int maxSpeed;
//省略Get、Set方法

public Car(){

}

public Car(String brand, String color, int maxSpeed){
this.brand = brand;
this.color = color;
this.maxSpeed = maxSpeed;
}

public void introduce(){
System.out.println("brand:" + brand + ",color:" + color + ",maxSpeed:" + maxSpeed);
}

}


package com.baobaotao.reflect;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ReflectTest {
public static Car initByDefaultConst() throws Exception{
//1.通过类装载器获取Car类对象
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class clazz = loader.loadClass("com.baobaotao.reflect.Car");

//2.获取类的默认构造器对象并通过它实例化Car
Constructor cons = clazz.getDeclaredConstructor((Class[])null);
Car car = (Car) cons.newInstance();

//3.通过反射方法设置属性
Method setBrand = clazz.getMethod("setBrand", String.class);
setBrand.invoke(car, "GT-R");
Method setColor = clazz.getMethod("setColor", String.class);
setColor.invoke(car, "红色");
Method setMaxSpeed = clazz.getMethod("setMaxSpeed", int.class);
setMaxSpeed.invoke(car, 200);
return car;
}

public static void main(String[] args) throws Exception{
Car car = initByDefaultConst();
car.introduce();
}
}


3.2.2 类装载器ClassLoader

  类装载器工作机制

  类装载器就是寻找类的字节码文件并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要经过一下步骤:

  1.装载:查找和导入Class文件。

  2.链接:执行校验、准备和解析步骤,其中解析步骤是可以选择的:

    a)校验:检查载入Class文件数据的;

    b)准备:给类的静态变量分配存储空间;

    c)解析:将符号引用转成直接引用;

  3.初始化:对类的静态变量、静态代码块执行初始化工作;

  类装载工作由ClassLoader及其子类负责,ClassLoader是一个重要的Java运行时系统组件,它负责在运行时查找和装入Class字节码文件。JVM在运行时会产生三个ClassLoader:跟装载器、ExtClassLoader(扩展类装载器)和AppClassLoader(系统类装载器)。其中,根装载器不是ClassLoader的子类,它使用C++编写,因此我们在Java看不到它,根装载器负责装载JRE的核心类库,如JRE目标下的rt.jar、charsets.jar等。ExtClassLoader和AppClassLoader都是ClassLoader的子类。其中ExtClassLoader负责装载JRE扩展目录ext中的JAR类包;AppClassLoader负责装载Classpath路径下的类包。

  这三个类装载器之间存在父子层级关系,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。默认情况下,使用AppClassLoader装载应用程序的类。可以做一个实验:

package com.baobaotao.reflect;
public class ClassLoaderTest {
public static void main(String[] args) {

ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("current loader:" + loader);
System.out.println("parrent loader:" + loader.getParent());
System.out.println("grandparrent loader:" + loader.getParent().getParent());
//output:
//current loader:sun.misc.Launcher$AppClassLoader@2a139a55
//parrent loader:sun.misc.Launcher$ExtClassLoader@7852e922
//grandparrent loader:null
}

}


  JVM装载类时使用”全盘负责委托机制”,”全盘负责”是指当一个ClassLoader装载一个类的时候,除非显示地使用另一个ClassLoader,该类所依赖及引用的类也由这个ClassLoader载入;”委托机制”是指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全角度考虑的,试想如果有人编写了一个恶意的基础类(如java.lang.String)并装在道JVM中将会引起可怕的后果。但由于有了”全盘负责委托机制”,java.lang.String永远是由根装载器来装载,这样就避免了上述事件的发生。

  ClassLoader重要方法

  ClassLoiader是一个抽象类,位于java.lang包下:

3.2.3 Java反射机制

3.3 资源访问利器

3.3.1 资源抽象接口

  JDK所提供的访问资源的类并不能很好地满足各种底层资源的访问需求,比如缺少从类路径或者Web容器的上下文中获取资源的操作类。因此,Spring设计了一个Resource接口,它为应用提供了更强的访问底层资源的能力。

FileSystemResource以文件系统绝对路径的方式进行访问;

ClassPathResource以类路径的方式进行访问;

ServletContextResource以相对与Web应用根目录的方式进行访问

package com.baobaotao.resource;

import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;

public class FileSourceExample {
public static void main(String[] args) throws IOException {
String filePathString = "C:/WiFi_Log.txt";
Resource res1 = new FileSystemResource(filePathString);
InputStream ins1 = res1.getInputStream();
System.out.println("res1:" +res1.getFilename());
//output:res1:WiFi_Log.txt
}
}


  采用特殊编码进行读取资源

package com.baobaotao.resource;

import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.util.FileCopyUtils;

public class FileSourceExample {

public static void main(String[] args) throws IOException {
String filePathString = "C:/WiFi_Log.txt";
Resource res1 = new FileSystemResource(filePathString);
EncodedResource encRes = new EncodedResource(res1);
String contentString = FileCopyUtils.copyToString(encRes.getReader());
System.out.println(contentString);
}
}


3.3.2 资源加载

  为了访问不同类型的资源,必须使用相应的Resource实现类。是否可以仅通过资源地址的特殊标识就可以加载相应的资源呢?Spring提供了一个强大的加载资源的机制,不但能通过”classpath:”、”file:”等资源地址前缀识别不同的资源类型,还支持Ant风格带通配符的资源地址。

  资源地址表达式

  1.
classpath:从类路径中加载资源,classpath:和classpath:/是等价的,都是相对于类的根路径。


  2.
file:从文件系统目录中加载资源,可采用绝对或相对路径


  3.
http://:从Web服务器中装载资源


  4.
ftp://:从FTP服务器中装载资源


  5.
没有前缀:根据ApplicationContext具体实现类采用对应类型的Resource


  其中”classpath*:”前缀。假设一个名为baobaotao的应用分为3个模块,每个模块都对应一个配置文件,分别是module1.xml,module2.xml,module3.xml,都放到com.baobaotao目录下。使用”classpath*:com/baobaotao/module*.xml”将可以加载这三个模块的配置文件,而使用”classpath:com/baobaotao/module*.xml”时只会加载一个模块的配置文件。

  Ant风格资源地址支持3种匹配符:

  
?
:匹配文件名中的一个字符;

  
*
:匹配文件名中任意个字符;

  
**
:匹配多层路径

  资源加载器

package com.baobaotao.resource;

import java.io.IOException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

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

ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource resources[] = resolver.getResources("classpath*:com/baobaotao/**/*.xml");
for (Resource resource : resources) {
System.out.println(resource.getDescription());
}
}
}


3.4 BeanFactory和ApplicationContext

  Spring通过一个配置文件描述Bean以Bean之间的依赖关系,利用Java语言的反射功能实例化Bean并建立Bean之间的依赖关系。Spring的IOC容器还提供了Bean实例缓存、声明周期管理、Bean实例代理、事件发布、资源装载等高级服务。

  Bean工厂(com.springframework.beans.factory.BeanFactory)是Spring最核心的接口,提供了高级IOC配置,管理不同类型的Java对象。

  应用上下文(com.springframework.context.ApplicationContext)建立在BeanFactory基础之上,提供了更多面向应用的功能,提供了国际化支持和框架事件体系,更易于创建实际应用。我们一般称BeanFactory为IOC容器,而称ApplicationContext为应用上下文。但有时为了行文方便,也将ApplicationContext称为Spring容器。

  BeanFactory是Spring框架的基础设施,面向Spring本身;ApplicationContext面向使用Spring框架的开发者,几乎所有的应用场合都直接使用ApplicationContext而非底层的BeanFactory。

3.4.1 BeanFactory介绍

  [b]BeanFactory的体系结构[/b]

  Spring为BeanFactory提供了多种实现,最常用的是XmlBeanFactory。

  初始化BeanFactory

  下面,我们使用Spring配置文件为Car提供配置信息,然后通过BeanFactory装载配置文件,启动Spring IOC容器。Spring配置文件如下所示:

<?xml version="1.0" encoding="UTF-8" ?>
<!-- 引用Spring的多个Schema空间的格式定义文件 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> 
<bean
id="car1"
class="com.baobaotao.Car"
p:brand="GT-R中国红"
p:color="红色"
p:maxSpeed="200"/>

</beans>


  通过XmlBeanFactory实现类启动SpringIOC容器:

package com.baobaotao.beanfactory;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import com.baobaotao.Car;

public class BeanFactoryTest {
public static void main(String[] args){
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource res = resolver.getResource("classpath:com/baobaotao/beanfactory/beans.xml");
BeanFactory bFactory = new XmlBeanFactory(res);
System.out.println("init BeanFactory.");
Car car = bFactory.getBean("car1", Car.class);
System.out.println("car bean is resdy for use");
}
}


  XmlBeanFactory通过Resource装载Spring配置信息并启动IOC容器,然后就可以通过BeanFactory#getBean(beanName)方法从IOC容器中获取Bean了。通过BeanFactory启动IOC容器时,并不会初始化配置文件中定义的Bean,初始化动作发生在第一个调用时。对于单实例的Bean来说,BeanFactory会缓存Bean实例,所以第二次使用getBean获取Bean时直接从IOC容器的缓存中获取Bean实例。使用HashMap实现的缓存器。

  在初始化BeanFactory时,必须为其提供一种日志框架,我们使用Log4J,即在类路径下提供Log4J配置文件,这样启动Spring容器才不会报错。

3.4.2 ApplicationContext介绍

  如果说BeanFactory是Spring的心脏,那么ApplicationContext就是完整的身躯了。ApplicationContext由BeanFactory派生而来,提供了更多面向实际应用的功能。通过配置的方式实现。

  [b]ApplicationContext类体系结构[/b]

  主要实现类是ClassPathXmlApplicationContext和FileSystemXmlApplicationContext,前者默认从类路径加载配置文件,后者默认从文件系统中加载配置文件。

  在获取ApplicationContext实例后,就可以像BeanFactory一样调用getBean(beanName)返回Bean了。ApplicationContext的初始化和BeanFactory由一个重大的区别:BeanFactory在初始化容器时,并未实例化Bean,直到第一次访问某个Bean时才实例目标Bean;而ApplicationContext则在初始化应用上下文时就实例化所有单实例的Bean。淫才ApplicationContext的初始化时间会比BeanFactory稍长一些,不过稍后的调用则没有”第一次惩罚”的问题。

  Spring3.0支持基于类注解的配置方式,主要功能来自于Spring的一个名为JavaConfig的子项目,目前JavaConfig已经升级为Spring核心框架的一部分。一个标注@Configuration注解的POJO即可提供Spring所需的Bean配置信息。

@Configuration //表示是一个配置信息提供类
public class Beans{
@Bean(name="car")
public Car buildCar(){
Car car = new Car();
car.setBrand("GT-R");
car.setMaxSpeed(498);
return car;
}
}


  相对于XML配置,类注解的配置方式更加灵活。

  Spring为基于注解类的配置提供了专门的ApplicationContext实现类AnnotationConfigApplicationContext。使用AnnotationConfigApplicationContext启动Spring容器:

ApplicationContext ctx = new AnnotationConfigApplicationContext(Beans.class);
Car car = ctx.getBean("car", Car.class);


  [b]WebApplicationContext类体系结构[/b]

  WebApplicationContext是专门为Web应用准备的,从相对于Web根目录的路径中装载配置文件完成初始化工作。扩展了ApplicationContext。使Spring的Web应用上下文和Web容器的上下文实现互访。

  [b]WebApplicationContext类初始化[/b]

  WebApplicationContext的初始化范式和BeanFactory、ApplicationContext有所区别,因为WebApplicationContext需要ServletContext实例(与Web应用的上下互访),也就是说它必须在拥有Web容器的前提下才能完成启动工作。有过Web开发经验的读者都知道可以在web.xml中配置自启动的Servlet或定义Web容器监听器(ServletContextListener),借助这两者中的任何一个,就可以完成启动Spring Web应用上下文的工作。

  下面使用Web容器监听器引导:

<!-- 指定配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/baobaotao-dao.xml,/WEB-INF/baobaotao-service.xml</param-value>
</context-param>
<!-- 声明Web容器监听器 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>


  下面使用自启动的Servlet引导:

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/baobaotao-dao.xml,/WEB-INF/baobaotao-service.xml</param-value>
</context-param>
<!-- 声明自启动的Servlet -->
<servlet>
<servlet-name>springContextLoaderServlet</servlet-name>
<servlet-class>
org.springframework.web.context.ContextLoaderServlet
</servlet-class>
<!-- 启动顺序 -->
<load-on-startup>2</load-on-startup>
</servlet>


  由于WebApplicationContext需要使用日志功能,用户可以将log4J的配置文件放置到类路径WEB-INF/classes,这时Log4J引擎即可顺利启动。如果Log4J配置文件放置在其他位置,用户还必须在web.xml指定Log4J配置文件位置。Spring为启用Log4J引擎提供了两种类食欲启动WebApplicationContext的实现类:Log4jConfigServlet和Log4jConfigListener。

<!-- 指定Log4J配置文件位置 -->
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>/WEB-INF/log4j.properties</param-value>
</context-param>
<!-- 指定Log4J配置文件位置 -->
<servlet>
<servlet-name>log4jConfigServlet</servlet-name>
<servlet-class>org.springframework.web.util.Log4jConfigServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>


  log4j启动顺序在springContextLoaderServlet之前启动。如果使用监听,则需要在之前。

  如果使用标注@Configuration的Java类提供配置信息,则web.xml的配置需要按如下方式配置:

<!-- 通过指定context参数,让Spring使用AnnotationConfigWebApplicationContext而
非XmlWebApplicationContext启动容器 -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>

<!-- 指定标注了@Configuration的配置类,可以使用逗号或空格隔开 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.baobaotao.AppConfig1, com.baobaotao.AppConfig2</param-value>
</context-param>

<!-- 监听器将根据上面配置使用 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>


  ContextLoaderListener如果发现配置了contextClass上下文参数,就会使用参数所指定的WebApplicationContext实现类(即AnnotationConfigWebApplicationContext)初始化容器,该实现类会根据contextConfigLocation上下文参数指定的@Configuration的配置类所提供的Spring配置信息初始化容器。

3.4.3 父子容器

  通过HierarachicalBeanFactory接口,Spring的IOC容器可以建立父子层级关联的容器体系,自容器可以访问父容器中的Bean,但父容器不能访问自容器的Bean。在容器内,Bean的id必须是唯一的,但自容器可以拥有一个和父容器id相同的Bean。

  比如,展现层Bean位于一个子容器中,而业务层和持久层的Bean位于父容器中。这样展现层Bean就可以引用业务层和持久层的Bean,而业务层和持久层的Bean则看不到展现层的Bean。

3.5 Bean的生命周期

  Web容器中的Servlet拥有明确的声明周期,Spring容器中的Bean也拥有相似的生命周期。

3.5.1 BeanFactory中Bean的生命周期

  [b]生命周期图解[/b]

  * Bean自身的方法:如调用Bean构造函数实例化Bean,调用Setter设置Bean的属性值以及通过的init-method和destroy-method所指定的方法;

  * Bean级生命周期接口方法,如BeanNameAware、BeanFactoryAware、InitializingBean和DisposableBean,这些接口方法由Bean类直接实现;

  * 容器级生命周期接口方法:BeanPostProcessor。

  [b]关于Bean生命周期接口的探讨[/b]

  通过实现Spring的Bean生命周期接口对Bean进行额外控制,就让Bean和Spring框架绑定在一起。这和Spring一直推崇的”不对应用程序类作出任何限制”的理念是相博的。

  可以通过的init-method和destroy-method,进行配置进行与Spring框架接口解耦。而BeanPostProcessor接口像插件一样注册到Spring容器中,为容器提供额外功能。

3.5.2 ApplicationContext中Bean的生命周期

  与BeanFactory中的周期类似,但是增加了一个接口,在应用上下文在装载配置文件之后初始化Bean实例之前调用BeanFactoryPostProcessor对配置信息进行加工处理。另外不同之处:ApplicationContext利用Java反射识别配置文件的定义,而BeanFactory需要代码手工调用进行注册。

3.6 小结

  深入分析了IOC的概念,控制反转中的”控制”指接口实现类的选择控制权;而”反转”是指这种选择控制权从调用类转移到外部第三方类或容器的手中。

  BeanFactory、ApplicationContext和WebApplicationContext是Spring框架三个最核心的接口。Resource是一个资源访问接口,实现了和具体资源的解耦,ResourceLoader采用了策略模式,通过不同的前缀自动选择合适的底层资源实现类。

  Spring为Bean提供了周全的声明周期过程,推荐使用配置方式,与Spring进行解耦,进行对Bean生周期的控制。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: