Spring IOC容器简介

2020-04-09  本文已影响0人  LENN123
前言

Spring的核心是一个IOC(控制反转)容器,IOC的含义与DI(依赖注入)等同,但是DI显然更好理解,也就是说当我一个类依赖另一个类的实例时,我们不需要主动的去创建这个实例,而是请求IOC容器帮我们把这个特定实例注入进来。为什么我们需要一个IOC容器呢?实际上是为了降低代码的耦合性,也就是解耦。

IOC是如何帮助我们降低耦合性的?

从实际出发,我们设计一个场景,假设一个People类,其实例表示一个人。同时设计一个Fruit接口,定义了一个与其关联的行为eat(),表示吃这个动作。只要实现了这个接口的水果,就可以被食用。

public interface Fruit {
    public void eat();
}
public class Apple implements Fruit {
    @Override
    public void eat() {
        System.out.println("苹果,直接吃。");
    }
}
public class Orange implements Fruit{
    @Override
    public void eat() {
        System.out.println("橘子,剥皮吃。");
    }
}

因为People既可以吃apple 也可以吃 orange,所以我们可以给People类设计两个方法eatOrangeeatApple,分别表示吃苹果和吃橘子这两个行为。

public class People {
    private String name;
    private int age;

    public People(String name, int age) {
        this.name = name;
        this.age  = age;
    }
    public void eatApple(){
        Apple apple = new Apple();
        apple.eat();
    }
    public void eatOrange(){
        Orange orange = new Orange();
        orange.eat();
    }
}

直到目前为止都很正常,但是现在假设我们又多了一种水果Banana, 那我们只要给People类添加一个eatBanana方法就可以了,似乎也不麻烦。但注意到我们这里只有People这一种动物类别,假设我们还有DogCatMonkey等100种不同动物,他们都能够吃Banana这种水果,那我们就要手动的给100个类都添加上eatBanana方法,是不是很麻烦。
为了解决这个问题,我们应当想到的是 People这个类应当设计一个eatFruit方法,其可以根据水果的种类,从一个地方拿到对应的水果,而不是自己去new一个出来。工厂模式似乎可以很好的解决这个问题,让我们建立一个FruitFactory

public class FruitFactory {
    public static Fruit getFruit(String name) {
        if ("apple".compareTo(name)==0) {
            return new Apple();
        } else {
            return new Orange();
        }
    }
}
public class People {
    private String name;
    private int age;

    public People(String name, int age) {
        this.name = name;
        this.age  = age;
    }
    public void eatFruit(String name) {
        Fruit fruit = FruitFactory.getFruit(name);
        fruit.eat();
    }
}

现在如果我们想新增加一个水果种类,只要在FruitFactory中的一处修改就好了。组件之间的关系图如下。

工厂模式

可以看到很关键的一点是原来的People类从一开始的主动new一个水果对象实例,变成了根据水果名从FruitFactory获取对应的水果实例,从创建变成了获取,完成了控制反转。我们知道SpringIOC容器是整个Spring的核心所在,我们可以简单的把它也想像成一个工厂,只不过IOC容器不仅能够创建Fruit类,还能够创建各种各样其他类型的对象,此外,除了创建对象实例本身,它还负责了这些实例的生命周期管理等。

使用IOC容器

虽然现在Spring框架更多的使用注解这种形式来完成依赖注入的工作,但其本质上和基于XML描述文件的原理是不变的。XML文件就像一张图纸,你在这张图纸里给你的XML里描述你要创建的Bean的各个属性,以及一个唯一的id,然后交给Spring,Spring会使用这张图纸按照你的要求实例化各个对象,并且存放在IOC容器里等你来获取并使用它们,我们以之前的People类为例来说明这件事。

public class People {
    private String name;
    private int age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public void eatFruit(String name) {
        Fruit fruit = FruitFactory.getFruit(name);
        fruit.eat();
    }
}

绘制图纸(编写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 id="people" class="entity.People">
        <property name="name" value="Tom"></property>
        <property name="age" value="26"></property>
    </bean>

</beans>

中间<bean>开头</bean>结尾的部分就是我们队一个bean的详细描述,翻译过来就是给我创建一个bean,它是entity.People类的一个实例,唯一标识符为id='people',并且它的 name属性的值是Tomage属性的值为26

SpringIOC容器管理的对象实例称为Spring bean,这里简称bean

然后我们把这个xml文件交由Spring框架,按照这个xml的文件生成一个对应的IOC容器实例,然后我们就可以按照之前定义的id,来从IOC里直接取用这个对象实例了。这里要注意因为IOC容器是可以管理各种类型的bean的,所以这里返回的是一个Object类型,需要我们进行强制类型转换。

 public static void test(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        People people = (People) applicationContext.getBean("people");
        System.out.println(people.getName() + " " + people.getAge());
    }
Tom 26

Process finished with exit code 0

Spring IOC容器是如何根据XML文件中的描述创建对应的对象实例呢?我们在XML文件里指定了需要创建实例的全类名,那么可以采用反射机制创建类的实例,注意到我们为People类的每一个属性都设置了set方法,那么利用反射机制的时候,只要 <property name="name" value="Tom"></property>name的首字母大写,再在首部拼接上set字段就可以推测出set方法全名,然后调用这个方法设置对应的value就可以了(这里属于Setter注入,还有Constructor注入,即构造器注入)。我们简单模拟一下这个实例化过程。

    public static void testReflect() {
        Object object = null;
        try {
            object  = (People) Class.forName("entity.People").newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        Method m1, m2;
        try {
            m1 = Class.forName("entity.People").getMethod("setName", String.class);
            m1.invoke(object,"Tom");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        try {
            m2 = Class.forName("entity.People").getMethod("setAge", int.class);
            m2.invoke(object, 26);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }


        System.out.println(((People) object).getName() + " " + ((People) object).getAge());
        
    }

整个执行流程如下


依赖注入
注入方式

我们在编写XML文件的时候,给每个Bean预先设置好了值,将这些值赋给对应的属性时,有两种最基本的方式可以选择,一是Setter注入,另一个是Constructor注入。

Setter注入

Setter注入就是利用一个类的set方法来对某个属性进行赋值,比如我们之前演示的 People类,就利用了Setter注入的方式。

    <bean id="people" class="entity.People">
        <property name="name" value="Tom"></property>
        <property name="age" value="26"></property>
    </bean>

再对nameage属性进入赋值的时候,会分别调用以下两个方法

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

让我们给这两个方法各添加一行打印信息,来验证是否真的被调用了。

public void setName(String name) {
        System.out.println("调用了setName方法");
        this.name = name;
    }
public void setAge(int age) {
        System.out.println("调用了setAge方法");
        this.age = age;
    }

运行结果如下,确实调用了这2个set方法

调用了setName方法
调用了setAge方法
Tom 26

Process finished with exit code 0

当我们进行Setter注入的时候,一定要保证这个类存在一个无参构造函数,因为Spring会首先利用无参构造函数创建对象实例,再利用set方法进行属性赋值,注意到如果我们不手动添加任何构造函数,系统会默认给我们添加一个无参构造函数,但是如果你手动添加了任何有参构造函数,系统则不会添加无参构造函数,这时候再调用setter注入就会报错,例如如下代码。

public class People {
    private String name;
    private int age;

    // 覆盖掉默认的无参构造函数
    public People(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("调用了setName方法");
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        System.out.println("调用了setAge方法");
        this.age = age;
    }

    public void eatFruit(String name) {
        Fruit fruit = FruitFactory.getFruit(name);
        fruit.eat();
    }
}

则会给我们抛出一个异常,通知我们Failed to instantiate [entity.People]: No default constructor found,说明找不到默认的无参构造函数

Constructor注入

除了使用set方法,我们也可以通过Constructor来对一个Bean的属性进行赋值,例如

 <bean id="people" class="entity.People">
        <constructor-arg value="jack"></constructor-arg>
        <constructor-arg value="26"></constructor-arg>
 </bean>

指明了constructor的参数,这里就会调用如下的构造器进行注入

 public People(String name, int age) {
     this.name = name;
     this.age = age;
}

执行结果如下

jack 26

Process finished with exit code 0

这里需要注意,我们并没有指定每种参数的类型,这里完全是按照默认的顺序(即xml里第一个constructor-arg对应构造器中传入的第一个参数来赋值),让我们颠倒下xml里两个参数的顺序,看看会怎么样。

    <bean id="people" class="entity.People">
        <constructor-arg value="26"></constructor-arg>
        <constructor-arg value="jack"></constructor-arg>
    </bean>

这时候就会告诉我们发生了错误Could not convert argument value of type [java.lang.String] to required type [int],不能把这个字符串类型的"jack"转换成int型,为了解决这个问题,我们可以明确的告诉Spring每个value对应的属性名具体是什么

    <bean id="people" class="entity.People">
        <constructor-arg name="age" value="26"></constructor-arg>
        <constructor-arg name="name" value="jack"></constructor-arg>
    </bean>

在这里由于2个参数类型不同,也可以通过指明类型来解决

    <bean id="people" class="entity.People">
        <constructor-arg value="26" type="int"></constructor-arg>
        <constructor-arg value="jack" type="java.lang.String"></constructor-arg>
    </bean>

此外,还可以通过标注对应的位置,index0开始

    <bean id="people" class="entity.People">
        <constructor-arg value="26" index="1"></constructor-arg>
        <constructor-arg value="jack" index="0"></constructor-arg>
    </bean>

以上三种方法最推荐第一种,更加清晰。

refnull与集合类型的注入
    <bean id="people" class="entity.People">
        <constructor-arg value="26" index="1"></constructor-arg>
        <constructor-arg index="0">
            <null/>
        </constructor-arg>
    </bean>
public class Car {
    private String brand;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }
}

再在People类里添加一个Car的属性,表示这个人拥有的汽车。并修改一下构造方法。

public class People {
    private String name;
    private String address;
    private Car car;
    private int age;

    public People(){}
    // 覆盖掉默认的无参构造函数
    public People(String name, int age, Car car) {
        this.name = name;
        this.age = age;
        this.car = car;
    }
}

修改xml文件

    <bean id="car" class="entity.Car">
        <property name="brand" value="Benz"></property>
    </bean>

    <bean id="people" class="entity.People">
        <constructor-arg value="26" index="1"></constructor-arg>
        <constructor-arg index="0">
            <null/>
        </constructor-arg>
        <constructor-arg ref="car" index="2"></constructor-arg>
    </bean>

注意people的第三个构造参数,不是value而是ref,表示注入的是对id=car这个bean的引用。
执行测试方法

    public static void testRef(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        People people = (People) applicationContext.getBean("people");
        System.out.println(people.getName() + " " + people.getAge()+ " " + people.getCar().getBrand());
    }

输出结果:

null 26 Benz

Process finished with exit code 0
public class CollectionDemo {
    private List<String> listDemo;
    private Set<String> setDemo;
    private Map<String, String> mapDemo;
    private String[] arrDemo;

    public List<String> getListDemo() {
        return listDemo;
    }

    public void setListDemo(List<String> listDemo) {
        this.listDemo = listDemo;
    }

    public Set<String> getSetDemo() {
        return setDemo;
    }

    public void setSetDemo(Set<String> setDemo) {
        this.setDemo = setDemo;
    }

    public Map<String, String> getMapDemo() {
        return mapDemo;
    }

    public void setMapDemo(Map<String, String> mapDemo) {
        this.mapDemo = mapDemo;
    }

    public String[] getArrDemo() {
        return arrDemo;
    }

    public void setArrDemo(String[] arrDemo) {
        this.arrDemo = arrDemo;
    }
}

其对应的xml配置。

    <bean id="collectionDemo" class="entity.CollectionDemo">
        <property name="arrDemo">
            <array>
                <value>arr-apple</value>
                <value>arr-orange</value>
                <value>arr-banana</value>
            </array>
        </property>
        <property name="listDemo">
            <list>
                <value>list-apple</value>
                <value>list-orange</value>
                <value>list-banana</value>
            </list>
        </property>
        <property name="setDemo">
            <set>
                <value>set-apple</value>
                <value>set-orange</value>
                <value>set-banana</value>
            </set>
        </property>
        <property name="mapDemo">
            <map>
                <entry>
                    <key>
                        <value>key-apple</value>
                    </key>
                    <value>val-apple</value>
                </entry>
                <entry>
                    <key>
                        <value>key-orange</value>
                    </key>
                    <value>val-orange</value>
                </entry>
                <entry>
                    <key>
                        <value>key-banana</value>
                    </key>
                    <value>val-banana</value>
                </entry>
            </map>
        </property>
    </bean>

测试类

    public static void testCollection(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        CollectionDemo collectionDemo = (CollectionDemo) applicationContext.getBean("collectionDemo");
        System.out.println("arr中的元素");
        for (String e : collectionDemo.getArrDemo()) {
            System.out.print(e + "  ");
        }
        System.out.println();
        System.out.println("list中的元素");
        for (String e : collectionDemo.getListDemo()) {
            System.out.print(e + "  ");
        }
        System.out.println();
        System.out.println("set中的元素");
        for (String e : collectionDemo.getSetDemo()) {
            System.out.print(e + "  ");
        }
        System.out.println();
        System.out.println("map中的元素");
        for (String e : collectionDemo.getMapDemo().keySet()) {
            System.out.println(e + "  " + collectionDemo.getMapDemo().get(e));
        }
        System.out.println();
    }

输出结果

arr中的元素
arr-apple  arr-orange  arr-banana  
list中的元素
list-apple  list-orange  list-banana  
set中的元素
set-apple  set-orange  set-banana  
map中的元素
key-apple  val-apple
key-orange  val-orange
key-banana  val-banana


Process finished with exit code 0
Autowire 自动装配

Spring还提供了自动装配的功能,就是说如果一个Bean中的一个属性没有手动的对其进行赋值,开启了自动装配选项后,就会在当前的容器里找到一个id值与这个属性名相同的Bean,用这个Bean对这个属性赋值。注意Autowire只适用于ref类型。
用之前的People类举例

    <bean id="people" class="entity.People" autowire="byName">
        <property name="name" value="Tom"></property>
        <property name="age" value="26"></property>
        <!--<constructor-arg ref="car" index="2"></constructor-arg>-->
    </bean>

我们在这个bean上指定了autowire="byName",告诉Spring,我这里有一个属性car没有赋值,你去找找当前有没有一个id=carbean,如果正好有,就把它赋值给我这个ref类型的名叫car的属性,没有就算了。这里的byName其实是根据id来搜寻。

利用注解的方式完成依赖注入

利用注解的方式完成依赖注入可以节省编写xml的大量时间。利用注解的方式完成依赖注入有两步

现在假设我们想利用注解的方式,将我们之前的People类注入到IOC容器中去,首先在People类上添加注解如下

@Component("people")
public class People {
    @Value("Ben")
    private String name;
    @Value("China")
    private String address;
    @Autowired
    private Car car;
    @Value("34")
    private int age;
}

第一行的@Component("people")告诉Spring框架,这是一个我想注入到IOC容器中的Bean,它的idpeople,以后可以通过这个id找到它。每个属性上的@Value("xxxx"),表示这是一个把这个值赋给注解下面的属性。
@Autowired注解下的是一个Car类型的引用。这里采用了之前说的自动装配的方式,去IOC容器里找有没有id=carbean,有的话就赋值给Peoplecar属性。实际上使用上述的注解就相当写了如下的xml

    <bean id="people" class="entity.People" autowire="byName">
        <property name="name" value="Bem"></property>
        <property name="age" value="34"></property>
        <property name="address" value="china"></property>
    </bean>

再让我们配置扫描器,告诉Spring去哪里扫描以获取对应的Bean

    <context:component-scan base-package="entity"></context:component-scan>

因为People类在entity目录下,所以这里base-package="entity"
最后,测试一下是否注入成功

    public static void testAnnotationDI(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        People people = (People) applicationContext.getBean("people");
        System.out.println(people.getName() + " " + people.getAge() + " " + people.getAddress() + " " + people.getCar().getBrand());
    }

执行结果,发现注入成功。

Ben 34 China Benz

Process finished with exit code 0
上一篇下一篇

猜你喜欢

热点阅读