Spring 详解(二):IOC 和DI

1 IOC 理解

Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

  • 谁控制谁,控制什么:传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
  • 为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

用一句话来讲解什么是IOC:IOC中文名称是控制反转。主要目的是为了实现程序的解耦,由程序员主动实例化对象的过程转交给Spring容器。IOC将本来是由程序员创建对象以及对象的管理和销毁的控制权交给了Spring IOC容器中,Spring IOC将这一块操作从应用程序中解耦出来,降低程序的耦合程度,简化程序设计。

2. DI 理解

Dependency Injection,即“依赖注入”。DI是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。当一个类(A)需要依赖另一个类对象(B)时,我们把另一个对象赋值给一个对象的过程。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

  • 谁依赖于谁:当然是应用程序依赖于IoC容器;
  • 为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;
  • 谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
  • 注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

用一句话来讲解什么是依赖注入:当我们一个类需要依赖于另一个对象,我们把另一个对象实例化后注入到这个对象的过程,我们就称为DI。由于Spring IOC掌控对象的创建、管理以及销毁工作,如果一个类需要依赖另外一个对象,Spring IOC容器需要将另外一个对象注入到应用程序某个对象中来,应用程序需要依赖某个外部对象。所有依赖注入提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。

3. Spring Bean装配

在Spring中,对象无需自己查找或者创建与其所关联的其他对象。相反容器负责把需要相互协作的对象引用赋予各个对象。创建应用对象之间协作关系的行为通常被称为装配(wiring),这也是依赖注入(DI)的本质。

3.1 Spring Bean装配的可选方案

当描述bean如何装配时,Spring具有非常大的灵活性,它提供了三种主要的装配机制:

  • 在XML中进行显示配置
  • 在Java中进行显示配置
  • 隐式的Bean发现机制和自动装配

我们需要尽可能使用在Java中进行显示配置和Bean自动装配机制,极少推荐使用XML中进行显示配置,因为XML中配置需要大量的XML配置工作。

3.2 隐式的Bean发现机制和自动装配

Spring从两个角度来实现自动化装配:

  • 组件扫描(component scanning):Spring会发现应用上下文中所创建的Bean
  • 组件装配(autowiring):Spring自动满足Bean之间的依赖

实现自动化装配有两种方式:(组件扫描默认是不启动的)

1)通过XML启用组件扫描

首先在mvc-config.xml中启动组件扫描功能,并用base-package属性指定扫描范围

<context:component-scan base-package="com.hust.edu"/>

再通过在需要注解的class上添加@Controller、@Service、@Repository、@Component等注解,比如:

@Controller
public class UserController {// ......}

2)通过@ComponentScan注解启用组件扫描

首先在class上使用@ComponentScan启用组件扫描,例如:

@ComponentScan
public class AppConfig {// ......}

此外:@ComponentScan(basePackages=“conf”)等同于@ComponentScan(“conf”),然后通过在需要注解的class上添加@Controller、@Service、@Repository、@Component等注解,例如:

对于@ComponentScan可以通过basePackages或者basePackageClasses指定扫描范围,等同于XML注解中的base-package属性;如果不指定扫描范围,则默认扫描当前类所在包以及子包的所有类。当然Craig Walls建议使用basePackageClasses,因为如果代码重构的话这种方式会立马发现错误,下面是basePackageClasses的使用方式(指定基础扫描类):

@ComponentScan(basePackageClasses={
		UserController.class
		// ......
})
public class AppConfig {}

使用@Autowired可以为bean实现自动装配,@Autowired可以使用在构造函数、Setter方法、普通方法和成员变量上。比如下面的用法:


// 用法一:
@Autowired
MessageSend messageSend;
// 用法二:(构造函数也一样,主要是函数参数的依赖)
@Autowired
public void setMessageSend(MessageSend messageSend) {
	this.messageSend = messageSend;
}

设置@Autowired(required=false)时,Spring尝试执行自动装配,但是如果没有匹配的bean则忽略,但是这种情况故意出现空指针异常NullPointerException。@Autowired注解可以使用@Inject替换,@component可以使用@Named注解替换,后者是源于Java依赖注入规范。

3.3. 通过Java代码装配

大部分的场景自动化装配Bean是满足要求的,但是在一些特殊场景下自动化装配Bean是无法满足要求的,比如说要将第三方的组件装配到自己的应用中,因为没有方法将@component或者@Autowired注解放置在它们的类上。但是你仍然可以采用显示装配方式:Java代码装配和XML配置。

首先需要创建配置类,创建配置类的关键是使用@Configuration注解来表明这个类是一个配置类,该类包涵Spring上下文中如何创建Bean的细节。

声明一个简单Bean:在Java的配置类中编写一个带有@Bean注解的方法,比如下面:

@Bean
public UserServiceImpl userService(){
	return new UserServiceImpl();
}

默认情况下Bean的ID是方法名,也可以指定Bean的ID:@Bean(name=“userService”),如果有依赖可以使用下面这些的方式来实现:

@Bean
public UserServiceImpl userService(){
	return new UserServiceImpl(userDao());
}
@Bean
public UserDaoImpl userDao(){
	return new UserDaoImpl();
}

上面看起来是调用UserDaoImpl(),其实在配置类中Spring会拦截对这个方法的引用,并返回该方法所创建的bean,而不是每次都对其进行实际调用。当然下面这种方式也是可以的,userService()方法需要userDao作为参数,Spring创建Bean的时候会自动装配一个UserDaoImpl到方法中(我猜测应该和@Autowired意思差不多,当Spring Context下只有一个UserDaoImpl就可以通过匹配原则进行装配),这种方式是被推荐的,如果UserDaoImpl不是在本配置类下配置,任然可以正常使用(比如XML默认的组件扫描等)。

@Bean
public UserServiceImpl userService(UserDaoImpl userDao){
	return new UserServiceImpl(userDao);
}

上面我们通过构造函数的方式实现依赖注入(DI),当然我们也可以用一种更好的方式来实现依赖注入,就是用Setter方法注入,如下所示:


@Bean
public UserServiceImpl userService(UserDaoImpl userDao){
	UserServiceImpl userService = new UserServiceImpl();
	userService.setDao(userDao);
	return userService;
}

3.4 通过XML装配Bean

XML装配Bean的方式,虽然已经不推荐了,但是还是我们最早使用的一种Bean装配的方式,还是需要学习的(很多古老的项目还在用,我目前的公司也是)。下面是一个简单Bean的声明:

<!--index表示获取到对象的标识 clsss创建哪个对象-->
    <bean id="peo" class="com.hust.edu.pojo.People">
        <!--ref表示引用另一个bean vaule表示基本数据类型或者String-->
        <constructor-arg index="1" type="int" value="1"></constructor-arg>
        <constructor-arg index="0" type="java.lang.String" value="AAA"></constructor-arg>
    </bean>

通过构造器创建Bean

  • 通过无参数构造创建:默认情况
  • 有参数构造创建:需要明确配置

在applicationContext.xml中设置调用哪个构造方法创建对象

  1. 如果设定的条件匹配多个构造方法执行最后的构造方法
  2. index:参数的索引,从0开始
  3. name:参数名字
  4. type:类型(区分关键字和非封装类 int 和 Integer)
public class People {
    private int id;
    private String name;

    public People(int id, String name) {
        this.id = id;
        this.name = name;
        System.out.println("有参数构造方法1 name:"+name+" id:"+id);
    }

    public People(String name, int id) {
        this.id = id;
        this.name = name;
        System.out.println("有参数构造方法2 name:"+name+" id:"+id);
    }

    public People() {
        System.out.println("执行默认的构造方法");
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        System.out.println("setter id: "+ id);
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("setter name: "+ name);
        this.name = name;
    }

    @Override
    public String toString() {
        return "People{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

 <!--index表示获取到对象的标识 clsss创建哪个对象-->
    <bean id="peo" class="com.hust.edu.pojo.People">
        <!--ref表示引用另一个bean vaule表示基本数据类型或者String-->
        <constructor-arg index="1" type="int" value="1"></constructor-arg>
        <constructor-arg index="0" type="java.lang.String" value="AAA"></constructor-arg>
    </bean>

通过实例工厂方法创建Bean

工厂设计模式:帮助创建对象,一个工厂可以参数多个对象。

实例工厂:需要创建工厂,才能创建对象。

实现步骤:

  1. 必须要有一个实例工厂
  2. 在applicationContext.xml 中配置工厂对象和需要创建的对象
public class PeopleFactory {
    public People createPeople(String type){
        switch (type){
            case "A":
                return new PeopleA();
            case "B":
                return new PeopleB();
            default:
                return null;
        }
    }
}

public class PeopleA extends People{
    @Override
    public String toString() {
        System.out.println("A");
        return super.toString();
    }
}

public class PeopleB extends People{
    @Override
    public String toString() {
        System.out.println("B");
        return super.toString();
    }
}

 <bean id="factory" class="com.hust.edu.constructor.factory.PeopleFactory"></bean>
    <bean id="peo1" factory-bean="factory" factory-method="createPeople" >
        <constructor-arg name="type" value="A"/>
    </bean>
    <bean id="peo2" factory-bean="factory" factory-method="createPeople" >
        <constructor-arg name="type" value="B"/>
    </bean>

通过静态工厂创建Bean

静态工厂:不需要创建工厂, 可以快速创建对象

实现步骤:

  1. 必须需要有一个静态工厂,在方法上添加static
  2. 在applicationContext.xml 中创建对象
public class PeopleStaticFactory {
    public static People getPeopleAInstance(){
        return new PeopleA();
    }

    public static People getPeopleBInstance(){
        return new PeopleB();
    }
}

<bean id="peo3" class="com.hust.edu.constructor.factory.PeopleStaticFactory" factory-method="getPeopleAInstance"></bean>

<bean id="peo4" class="com.hust.edu.constructor.factory.PeopleStaticFactory" factory-method="getPeopleBInstance"></bean>

设置属性

  1. 如果属性是基本数据类型或者String类型,注入就比较简单
  2. 如果属性是Set、List、Map、Array, 注入不同的数据类型
 <bean id="peo" class="com.hust.edu.pojo.People">
        <property name="id" value="1"></property>
        <property name="name" value="zhangsan"></property>
    </bean>

    <bean id="peoSet" class="com.hust.edu.pojo.PeopleSet">
        <property name="id" value="1"></property>
        <property name="name" value="zhangsan"></property>
        <property name="set" >
            <set>
                <value>1</value>
                <value>2</value>
                <value>3</value>
                <value>4</value>
            </set>
        </property>
    </bean>

    <bean id="peoList" class="com.hust.edu.pojo.PeopleList">
        <property name="id" value="1"></property>
        <property name="name" value="zhangsan"></property>
        <property name="list" >
            <set>
                <value>1</value>
                <value>2</value>
                <value>3</value>
                <value>4</value>
            </set>
        </property>
    </bean>

    <bean id="peoArray" class="com.hust.edu.pojo.PeopleArray">
        <property name="id" value="1"></property>
        <property name="name" value="zhangsan"></property>
        <property name="strings" >
            <array>
                <value>1</value>
                <value>2</value>
                <value>3</value>
                <value>4</value>
            </array>
        </property>
    </bean>


    <bean id="peoMap" class="com.hust.edu.pojo.PeopleMap">
        <property name="id" value="1"></property>
        <property name="name" value="zhangsan"></property>
        <property name="map" >
           <map>
               <entry key="a" value="123"></entry>
               <entry key="b" value="456"></entry>
               <entry key="c" value="789"></entry>
           </map>
        </property>
    </bean>

4 处理自动装配的歧义性

虽然在实际编写代码中,很少有情况会遇到Bean装配的歧义性,更多的情况是给定的类只有一个实现,这样自动装配就会很好的实现。但是当发生歧义性的时候,Spring提供了多种的可选解决方案。比如People父类,有三个实现类分别是PeopleA、PeopleB和PeopleC。那么在自动装配的时候具体是装配哪个类呢?所以Spring必须提供表选自动装配的Bean。

public class PeopleAutowired {
    @Autowired
    private People people;

    @Override
    public String toString() {
        return "PeopleAutowired{" +
                "people=" + people +
                '}';
    }
}

@Component
public class PeopleA extends People {
    @Override
    public String toString() {
        System.out.println("A");
        return super.toString();
    }
}

@Component
public class PeopleB extends People {
    @Override
    public String toString() {
        System.out.println("B");
        return super.toString();
    }
}

@Component
public class PeopleC extends People {
    @Override
    public String toString() {
        System.out.println("C");
        return super.toString();
    }
}

4.1 表示首选的Bean

注意,对于多可选择项,只能有一个可以加上@Primary。通过@Primary注解来标识首选Bean。

@Component
@Primary
public class PeopleA extends People {
    @Override
    public String toString() {
        System.out.println("A");
        return super.toString();
    }
}

4.2 限定自动装配的Bean

@Qualifier(“peopleA”)指向的是扫描组件时创建的Bean,并且这个Bean是IceCream类的实例。事实上如果所有的Bean都没有自己指定一个限定符(Qualifier),则会有一个默认的限定符(与Bean ID相同),我们可以在Bean的类上添加@Qualifier注解来自定义限定符,如下所示:

@Component
@Primary
@Qualifier("peopleA")
public class PeopleA extends People {
    @Override
    public String toString() {
        System.out.println("A");
        return super.toString();
    }
}

@Component
public class PeopleAutowired {

    @Autowired
    @Qualifier("peopleB")
    private People people;

    @Override
    public String toString() {
        return "PeopleAutowired{" +
                "people=" + people +
                '}';
    }
}

5. Bean作用域

默认情况下,Spring应用上下文中所有的Bean都是单例模式。在大多数情况下单例模式都是非常理想的方案。但是如果,你要注入或者装配的Bean是易变的,他们会有一些特有的状态。这种情况下单例模式就会容易被污染。Spring为此定义了很多作用域,可以基于这些作用域创建Bean,包括:

  1. 单例(Singleton):在整个应用中,只创建Bean的一个实例
  2. 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的Bean实例。这个相当于new的操作
  3. 会话(Session):在Web应用中,为每个会话创建一个Bean实例。对于同一个接口的请求,如果使用不同的浏览器,将会得到不同的实例(Session不同)
  4. 请求(Request):在Web应用中,为每个请求创建一个Bean实例

@Component
@Scope("prototype")
public class Car {
	// 。。。。。。
}
// ——————或者——————
@Bean
@Scope("prototype")
public LinuxConfig getLinux() {
	LinuxConfig config = new LinuxConfig();
	return config;
}

5.1 使用会话和请求作用域

我们常用@Scope来定义Bean的作用域。如用户的购物车信息,如果将购物车类声明为单例(Singleton),那么每个用户都向同一个购物车中添加商品,这样势必会造成混乱;你也许会想到使用原型模式声明购物车,但这样同一用户在不同请求时,所获得的购物车信息是不同的,这也是一种混乱。如下所示:

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,proxyMode = ScopedProxyMode.INTERFACES)
public class ShoppingCart {
}

在这里我们要注意一下,属性proxyMode。这个属性解决了将会话或者请求作用域的Bean注入到单例Bean中所遇到的问题。假设我们要将Cart bean注入到单例StoreService bean的Setter方法中:

StoreService是一个单例bean,会在Spring应用上下文加载的时候创建。 当它创建的时候, Spring会试图将Cart bean注入到setCart()方法中。 但是Cart bean是会话作用域的, 此时并不存在。 直到某个用户进入系统,创建了会话之后,才会出现Cart实例。系统中将会有多个Cart实例: 每个用户一个。 我们并不想让Spring注入某个固定的Cart实例到StoreService中。 我们希望的是当StoreService处理购物车功能时, 它所使用的Cart实例恰好是当前会话所对应的那一个。Spring并不会将实际的Cart bean注入到StoreService中,Spring会注入一个到Cart bean的代理。这个代理会暴露与Cart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用Cart的方法时, 代理会对其进行懒解析并将调用委托给会话作用域内真正的Cart bean。

在这里插入图片描述

proxyMode属性被设置成了ScopedProxyMode.INTERFACES, 这表明这个代理要实现Cart接口,并将调用委托给实现bean。如果Cart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果Cart是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。

6.运行时值注入

Bean的属性注入的时候有时候硬编码是可以的,但是有时候我们希望避免硬编码值,而是想让这些在运行时再确定。为了实现这些功能,Spring提供了两种在运行时的求值方式:

  • 属性占位符 #{}
  • Spring表示式语言(SpEL)${}

6.1 属性占位符

   <!-- 加载配置文件,支持注解的方法 -->
    <bean id="prop" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <array>

            </array>
        </property>
    </bean>

    <!-- 加载配置文件,支持xml的方式-->
    <bean class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
        <property name="locations">
            <array>

            </array>
        </property>
    </bean>

通过配置PreferencesPlaceholderConfigurer和PropertiesFactoryBean用来在Java代码中可以使用属性占位符来方位外部资源属性。

6.2 Spring表示式语言(SpEL)

Spring表达式语言(Spring Expression Language),它能够以一种强大的和简洁的方式将值装配到Bean属性和构造参数中,这个过程中所使用到的表达式会在运行时计算得到。

SPEL有很多特性:

  • 使用Bean ID来引用Bean
  • 调用方法和访问对象的属性
  • 对值进行算式、关系和逻辑运算
  • 正在表达式匹配
  • 集合操作

SPEL的使用方法

  1. 表达字面值
  2. 引用Bean、属性和方法
  3. 在表达式中使用类型
  4. SPEL运算符
  5. 计算正则表达式
  6. 计算集合

猜你喜欢

转载自blog.csdn.net/qq_21125183/article/details/86251137