美文网首页
Spring IOC容器简介

Spring IOC容器简介

作者: LENN123 | 来源:发表于2020-04-09 21:41 被阅读0次
    前言

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

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

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

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

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

    • People类
    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

    • FruitFactory类
    public class FruitFactory {
        public static Fruit getFruit(String name) {
            if ("apple".compareTo(name)==0) {
                return new Apple();
            } else {
                return new Orange();
            }
        }
    }
    
    • 新的People类
    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类为例来说明这件事。

    • 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)文件

    • applicationContext.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与集合类型的注入
    • null
      当我们需要对一个值赋予空值null的时候,可以采用标签<null/>
        <bean id="people" class="entity.People">
            <constructor-arg value="26" index="1"></constructor-arg>
            <constructor-arg index="0">
                <null/>
            </constructor-arg>
        </bean>
    
    • ref引用类型
      有时候一个bean的内部可能会引用另外一个bean,这时候就需要ref引用类型,ref的值是被引用的beanid
      我们先设计一个Car类,brand表示它的品牌。
    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
    
    • 集合类型的注入
      java里诸如listsetmaparray等都属于集合类型,Spring也为它们提供了对应的注入方式,分别对应于xml中的<list><set><map><array>标签,我们先创建一个CollectionDemo类用于演示。
    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的大量时间。利用注解的方式完成依赖注入有两步

    • 在想要被注入到IOC容器里的类上添加相应的注解。
    • 配置注解扫描器,指明要扫描注解的目录。

    现在假设我们想利用注解的方式,将我们之前的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
    

    相关文章

      网友评论

          本文标题:Spring IOC容器简介

          本文链接:https://www.haomeiwen.com/subject/fvicmhtx.html