Spring IoC的XML实现方式

IoC的概念

所谓IoC, 即控制反转, 将对象的创建、对象之间关系的维护交给框架而不是程序员, 传统方式是由程序员主动创建对象并维护其引用关系, 使用Spring框架可以只描述要创建对象的特征, 创建的行为交给框架去处理.

IoC的原理

在Spring中, IoC通常需要一个XML配置文件, 框架通过读取配置信息来创建对象并维护关系, 实现IoC的简单原理大致是下面三个步骤:

  1. 读取配置文件(XML解析);

  2. IoC容器根据解析结果通过反射机制, 创建对象;

  3. 对所创建对象进行依赖注入(调用setter或通过有参构造器注入);

更详细的原理可以参考Spring源码, 这里不再展开, 要注意的是以下几个点:

  • IoC容器底层就是一个对象工厂, 是工厂模式的实现;
  • 要进行依赖注入的属性, 要么有公共的setter的, 要么配置有参构造器注入, 否则将注入失败.

什么是DI?

DI即依赖注入, 指的是将被引用对象的构造和引用对象解耦, 即引用对象直接得到一个完整对象并进行使用, 而不关心这个对象的创建等无关事宜. 体现到方法中就是, 从原本一个方法接受各种参数来创建一个被引用对象, 到该方法直接接受这个对象本身(对象作为参数传递), 实际上没有多复杂的东西, 我们使用Java开发应用程序本身在方法中传递对象就是在不断使用DI的思想, 因为Java的设计原则是: 一切皆对象.

为什么说Spring是一个DI框架? 因为Spring负责创建对象并维护其引用关系. 简单来说, 假设有一个类型A和类型B, 其中A中有一个属性类型是B, 那么只需要经过适当的配置, Spring就可以创建A的实例和B的实例, 并将B的实例注入到A的属性中, 对于应用程序来说, 实现了"要什么给什么"的效果, 而创建过程是透明的, 这就是典型的依赖注入, 当然Spring可以注入的情形可不止自定义类型, 还可以注入简单类型、String和集合等.

IoC做什么?

IoC主要就做一件事, 即Bean管理, 而Bean管理做两件事, 其实前面已经说过了, 即:

  • Spring创建对象;
  • Spring注入属性;

下面介绍一下基于XML如何实现Bean管理.

在编码中, 实现IoC的步骤

0. 编写配置文件

配置文件可以是XML文件、Java配置类(注解开发), 本文记录XML实现, 后文会介绍注解开发.

1. 获取IoC容器

上文说到, IoC容器底层是一个工厂, 为了获取工厂进行对象创建, Spring提供了两个接口:

  • BeanFactory: IoC容器的基本实现, 是Spring内部接口, 主要由Spring内部使用;
  • ApplicationContext: BeanFactory的一个子接口, 增强了功能, 建议开发人员使用.

两个接口的主要区别:

  • BeanFactory: 默认情况下, 加载配置文件时不会创建对象, 获取对象时才创建对象;
  • ApplicationContext: 默认情况下, 加载配置文件时会创建好配置文件中的对象.

以一般使用的ApplicationContext为例, 其常用的实现类有:

  • ClassPathXmlApplicationContext: 通过类路径解析XML配置文件;
  • FileSystemXmlApplicationContext: 通过文件路径解析XML配置文件;
  • AnnotationConfigApplicationContext: 完全注解开发下, 解析配置类.

2. 通过IoC容器, 获取对象

到这一步直接调用IoC容器的方法, 即可获取到对象, 下面做一个演示.

简单演示创建对象

  1. 先声明一个简单Bean, 叫做Employee, 如下:
package com.yjzzjy4.learning.beans;

public class Employee {
    private String name;
    
    public Employee() {}

    // getter && setter...
}
  1. 写一个配置文件Spring-Conf-0.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="emp" class="com.yjzzjy4.learning.beans.Employee"/>
</beans>
  1. 编写一个测试类Test0, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test0 {
    public static void main(String[] args) {
        // 读取配置文件, 获取IoC容器;
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-0.xml");

        // 获取对象;
        Employee employee = context.getBean("employee", Employee.class);

        System.out.println(employee + "\n" + employee.getName());
    }
}
  1. 运行结果如下:

注意事项:

  • <bean>标签的id属性是用作IoC容器的getBean方法的获取对象的唯一标识;
  • <bean>标签的class属性用于指定类的完整路径(带包名).

依赖注入的方式

我们可以通过Spring为创建的对象注入属性值, 其方式有多种, 下面一一进行介绍:

通过有参构造器注入

默认情况下, Spring通过无参构造器初始化对象, 若是一个类中没有无参构造器, 且并未配置构造器参数注入属性值, 则会抛出异常, 这种情况不在此演示, 下面我们介绍通过有参构造器注入属性值的方法.

  1. 首先修改Employee的声明, 为其新增有参构造器:
package com.yjzzjy4.learning.beans;

public class Employee {
    private String name;
    
    public Employee() {}
    
    public Employee(String name) {
        this.name = name;
    }

    // getter && setter...
}
  1. 然后修改配置文件, 实现有参构造器注入:
<?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="employee0" class="com.yjzzjy4.learning.beans.Employee"/>
    <bean id="employee1" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="yjzzjy4"/>
    </bean>
</beans>
  1. 修改测试类Test0, 对比生成的两个对象:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test0 {
    public static void main(String[] args) {
        // 读取配置文件, 获取IoC容器;
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-0.xml");

        // 获取对象;
        Employee employee0 = context.getBean("employee0", Employee.class);
        Employee employee1 = context.getBean("employee1", Employee.class);

        System.out.println(employee0 + "\n" + employee0.getName());
        System.out.println(employee1 + "\n" + employee1.getName());
    }
}
  1. 运行结果如下:

注意事项:

  • 可以看到生成了两个不同的对象, employee1是使用有参构造器注入的name属性值, employee0使用无参构造器, 因此name属性是默认值null;
  • <constructor-arg>标签是指定构造器参数的标签, 可以有多个, 其name属性值指定参数名称, 也可以用index指定参数索引(不推荐), value属性值指定简单类型或字符串的值, ref可以指定引用对象的值(通过<bean>id属性值指定, 后面会详细说).

通过setter注入

通过配置, Spring可以先创建对象, 再使用对象的setter进行依赖注入, 例子如下:

  1. 创建一个新的配置文件Spring-Conf-1.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="employee" class="com.yjzzjy4.learning.beans.Employee">
        <property name="name" value="Aaron"/>
    </bean>
</beans>
  1. 新建一个测试类Test1, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        // 读取配置文件, 获取IoC容器;
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-1.xml");

        // 获取对象;
        Employee employee = context.getBean("employee", Employee.class);

        System.out.println(employee + "\n" + employee.getName());
    }
}
  1. 运行结果如下:

注意事项:

  • 要注入的属性必须要有对应的setter, 否则注入失败;
  • <property>属性要嵌入<bean>中使用;
  • <property>的三个属性namevalueref含义和上文所述无异.

通过p名称空间注入

p名称空间其实就是setter注入的一种简化写法, 下面进行演示:

  1. 创建一个新的配置文件Spring-Conf-2.xml, 如下:
<?xml version="1.0" encoding="UTF-8"?>
<!--引入p名称空间-->
<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.xsd">

    <bean id="employee" class="com.yjzzjy4.learning.beans.Employee" p:name="Tom"/>
</beans>
  1. 新建一个测试类Test2, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test2 {
    public static void main(String[] args) {
        // 读取配置文件, 获取IoC容器;
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-2.xml");

        // 获取对象;
        Employee employee = context.getBean("employee", Employee.class);

        System.out.println(employee + "\n" + employee.getName());
    }
}
  1. 运行结果如下:

注意事项:

  • p名称空间可以省略<property>标签, 直接在<bean>标签中以p:开头的属性指定属性注入, 其中值类型的属性可以使用p:propertyName指定值, 引用类型的属性可以用p:propertyName-ref注入其引用的对象;
  • 使用时需要引入p名称空间, 在配置文件中已有注释, 写法是固定的, 无需记忆.

引用类型的注入

上述例子演示的都是简单类型或String字面量的注入, 可以理解为值类型, 那么我们要如何注入一个引用类型的属性呢? 其实也很简单, 下面分几种情况进行说明.

注入外部Bean

  1. 我们先创建一个名为Department的类, 如下:
package com.yjzzjy4.learning.beans;

public class Department {
    private String name;

    public Department() {}

    public Department(String name) {
        this.name = name;
    }

    // getter && setter...
}
  1. 再为Employee对象新增一个对Department的引用属性, 如下:
package com.yjzzjy4.learning.beans;

public class Employee {
    private String name;
    private Department department;

    public Employee() {}

    public Employee(String name) {
        this.name = name;
    }

    // getter && setter...
}
  1. 创建一个新的配置文件Spring-Conf-3.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="employee" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="Tom"/>
        <property name="department" ref="department"/>
    </bean>
    <bean id="department" class="com.yjzzjy4.learning.beans.Department">
        <constructor-arg name="name" value="财务部"/>
    </bean>
</beans>
  1. 新建一个测试类Test3, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test3 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-3.xml");
        Employee employee = context.getBean("employee", Employee.class);

        System.out.println(employee + "\n" + employee.getName());
        System.out.println(employee.getDepartment().getName());
    }
}

运行结果如下:

注意事项:

  • 在配置文件中配置了两个<bean>, 其中employee依赖于department;
  • 使用<property>ref属性指明要注入的对象的id, 凡是用来指定要注入的属性的标签, 都可以用ref属性, 如<constructor-arg>标签;
  • 在测试类中, 虽然没有通过IoC容器主动取出department对象, 但还是可以看到它作为属性被注入employee中了, 这说明Spring会自动管理对象间的依赖关系, 体现了DI的思想.

注入内部Bean

刚才department的<bean>标签是写在整个employee的<bean>标签外面, 属于同级关系, 故称作外部Bean, 内部Bean就是在属性需要用到引用对象的时候, 直接创建一个<bean>节点.

  1. 创建一个新的配置文件Spring-Conf-4.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="employee" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="Jackson"/>
        <property name="department">
            <!--嵌套一个内部<bean>-->
            <bean class="com.yjzzjy4.learning.beans.Department">
                <constructor-arg name="name" value="财务部"/>
            </bean>
        </property>
    </bean>
</beans>
  1. 新建一个测试类Test4, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test4 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-4.xml");
        Employee employee = context.getBean("employee", Employee.class);

        System.out.println(employee + "\n" + employee.getName());
        System.out.println(employee.getDepartment().getName());
    }
}

运行结果如下:

注意事项:

  • 内部Bean的id属性是多余的, 即使设置也没法通过IoC容器来获取内部Bean的对象, 会抛出"NoSuchBeanDefinitionException"异常. 因为内部Bean是作为一个部分嵌套在另一个Bean中的, 无法作为完整的Bean被单独创建;
  • 不仅可以在<property>标签中嵌套<bean>标签, 实际上任何一个需要注入对象的地方都可以使用, 比如可以在<constructor-arg>中嵌套(前提是要有对应的构造器参数).

级联赋值

所谓级联赋值, 可以简单描述为以下过程:

  • 类A依赖于类B, 且a、b分别是类A、B的一个实例;
  • 为b的属性c赋值, 然后将b注入到a中, 则a.b.c也相当于获得了一个新的值.

很明显通过之前注入外部Bean的配置, 我们已经完成了一次级联赋值, 现在介绍另一种配置方式.

  1. 创建一个新的配置文件Spring-Conf-5.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="employee" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="Tom"/>
        <property name="department" ref="department"/>
        <!--级联赋值-->
        <property name="department.name" value="行政部"/>
    </bean>
    <bean id="department" class="com.yjzzjy4.learning.beans.Department">
        <constructor-arg name="name" value="财务部"/>
    </bean>
</beans>
  1. 新建一个测试类Test5, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test5 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-5.xml");
        Employee employee = context.getBean("employee", Employee.class);

        System.out.println(employee + "\n" + employee.getName());
        System.out.println(employee.getDepartment().getName());
    }
}

运行结果如下:

注意事项:

  • <property>name属性值是属性名.属性名的形式, 可以对Bean中的复杂类型属性的对象的属性进行赋值, 前提有以下两点:
  1. 复杂类型属性的对象不能为null. 在本例中即employee中的department对象不能为null, 要么通过注入外部Bean, 要么通过注入内部Bean来保证其不为null, 否则就会抛出一个原理类似空指针异常的错误: “NullValueInNestedPathException”.

  2. Bean中对要赋值的对象必须要有getter, 要赋值的对象的目标属性必须要有setter. 因为Spring要先获取该对象, 再为其属性设置值, 本例中其原理可以大致抽象表示为:

    employee.getDepartment().setName("行政部");
    
  • 理论上, 级联赋值可以超过两级, 只要满足上述赋值的条件即可;

  • 级联赋值也可以注入对象, 使用ref属性即可, 或者注入内部Bean.

注入集合(Array、List、Set)

在开发中, 集合(或者说容器)是非常常用的一种组件, Java为我们提供了一套完善的集合框架, 而Spring自然是对这些集合作为属性的注入有着很好的支持, 下面我们演示一下.

  1. 员工和部门是一个典型的一对多关系, 考虑到数据层面的引用(比如级联查询等), 我们通常会在Department类中声明一个Employee的容器, 修改Department的声明如下:
package com.yjzzjy4.learning.beans;

import java.util.Set;

public class Department {
    private String name;
    private Set<Employee> employees;

    public Department() {}

    public Department(String name) {
        this.name = name;
    }

    // getter && setter...
}

  1. 创建一个新的配置文件Spring-Conf-6.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="employee" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="Susan"/>
        <property name="department" ref="department"/>
    </bean>

    <bean id="department" class="com.yjzzjy4.learning.beans.Department">
        <constructor-arg name="name" value="财务部"/>
        <property name="employees">
            <set>
                <!--内部Bean-->
                <bean class="com.yjzzjy4.learning.beans.Employee">
                    <constructor-arg name="name" value="Sara"/>
                    <property name="department" ref="department"/>
                </bean>
                <bean class="com.yjzzjy4.learning.beans.Employee">
                    <constructor-arg name="name" value="Aaron"/>
                    <property name="department" ref="department"/>
                </bean>

                <!--引用外部Bean-->
                <ref bean="employee"/>
            </set>
        </property>
    </bean>
</beans>
  1. 新建一个测试类Test6, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Department;
import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test6 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-6.xml");
        Department department = context.getBean("department", Department.class);

        System.out.println(department.getName());

        for(Employee e : department.getEmployees()) {
            System.out.println(e.getDepartment().getName() + " " + e.getName());
        }
    }
}

运行结果如下:

注意事项:

  • <set>是用于注入集合的标签, 关于这个标签, 说明如下:

集合, 容器也. 既然是容器, 一定是要往里面装东西的, 可以装对象, 也可以装简单类型的值(最常见的就是字符串直接量), 针对不同的使用场景, 有不同的写法, 如:

  • 值类型的集合:
<set>
    <value></value>
    <value></value>
    ...
    <value></value>
</set>
  • 引用类型的集合(内部Bean写法):
<set>
    <bean></bean>
    <bean></bean>
    ...
    <bean></bean>
</set>
  • 引用类型的集合(外部Bean写法):
<set>
    <ref bean="xxx"></ref>
    <ref bean="yyy"></ref>
    ...
    <ref bean="zzz"></ref>
</set>

当然内部Bean和外部Bean也可以混用, 本例已经演示了混用的情况.

  • 其他集合类型如ArrayList等用法也都如Set, 只是最外层标签从<set>改为<array><list>而已, 这里就不再赘述, 用时举一反三即可;

  • Map由于是K-V结构的容器, 用法会稍有不同, 接下来会介绍如何注入Map.

注入Map

  1. 为了方便, 我们直接新建一个Bean来进行演示, MapInjection声明如下:
package com.yjzzjy4.learning.beans;

import java.util.Map;

public class MapInjection {
    private Map<String, String> map;

    public MapInjection() {}

    // getter && setter...
}
  1. 创建一个新的配置文件Spring-Conf-7.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="mapInjection" class="com.yjzzjy4.learning.beans.MapInjection">
        <property name="map">
            <map>
                <entry key="k0" value="v0"/>
                <entry key="k1" value="v1"/>
                <entry key="k2" value="v2"/>
            </map>
        </property>
    </bean>
</beans>
  1. 新建一个测试类Test7, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.MapInjection;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.Map;

public class Test7 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-7.xml");
        MapInjection mapInjection = context.getBean("mapInjection", MapInjection.class);

        for(Map.Entry<String, String> entry : mapInjection.getMap().entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}

运行结果如下:

注意事项:

  • <map>应该嵌套<entry>标签, 且<entry>是无值标签, 一般写成<entry/>;
  • <entry>有四个常用属性: keyvaluekey-refvalue-ref, 用于指定键、值, 且键、值都可以是引用类型, 指定时用id属性值标识.

将集合提取为公共组件

之前注入集合属性的时候, 都是在<property>中嵌套对应的集合标签, 那么能不能将集合提取到<bean>之外, 与其平级, 成为一个单独的组件呢? 因为相同类型的集合可能有非常多个, 这样就不用每次需要写一个内嵌集合了, 提高复用率. 答案当然是可以的, 借助util名称空间的帮助即可, 下面进行演示.

  1. 创建一个新的配置文件Spring-Conf-8.xml, 如下:
<?xml version="1.0" encoding="UTF-8"?>
<!--引入util名称空间-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/util
       http://www.springframework.org/schema/util/spring-util.xsd">

    <bean id="employee" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="Susan"/>
        <property name="department" ref="department"/>
    </bean>

    <bean id="department" class="com.yjzzjy4.learning.beans.Department">
        <constructor-arg name="name" value="财务部"/>
        <property name="employees" ref="employees"/>
    </bean>

    <!--提取set集合-->
    <util:set id="employees">
        <!--引用外部Bean-->
        <ref bean="employee"/>

        <!--内部Bean-->
        <bean class="com.yjzzjy4.learning.beans.Employee">
            <constructor-arg name="name" value="Aaron"/>
            <property name="department" ref="department"/>
        </bean>
        <bean class="com.yjzzjy4.learning.beans.Employee">
            <constructor-arg name="name" value="Sara"/>
            <property name="department" ref="department"/>
        </bean>
    </util:set>
</beans>
  1. 新建一个测试类Test8, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Department;
import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test8 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-8.xml");
        Department department = context.getBean("department", Department.class);

        System.out.println(department.getName());

        for(Employee e : department.getEmployees()) {
            System.out.println(e.getDepartment().getName() + " " + e.getName());
        }
    }
}

运行结果如下:

注意事项:

  • 除了Array以外的集合都可以提取出来, util命名空间提供了<util:list><util:set><util:map>等标签, 通过<util:constant>还可以定义常量;
  • util名称空间的引入是固定写法, 无需记忆.

FactoryBean

Spring中有两种Bean, 一种是普通Bean, 一种是FactoryBean, 这两种Bean最大的区别在于:

普通Bean在注入时只能注入其自身类型的对象, FactoryBean一般注入其他类型的对象.

FactoryBean其实是一个接口, 实现该接口的Bean就是一个FactoryBean, 我们看一眼API:

简单翻译一下重点:

  • 实现了FactoryBean接口的Bean不能被当作普通Bean使用;
  • FactoryBean是一种编程约定, 实现类不应该依赖注解方式注入或者其他反射机制(创建对象);
  • (IoC)容器只为FactoryBean的生命周期负责, 而不为其创建的对象负责.

我个人主观的理解:

  • FactoryBean是用来提供其它Bean实例的工厂, 并不是提供自身的实例, 且一个FactoryBean对应一个Bean, 其getObject方法应返回它创建的Bean的实例;
  • 不要用注解方式或者反射方式创建对象, FactoryBean中的方法getObjectgetObjectType可能在启动过程提前调用, 甚至所有后置处理器之前, 由于其不确定性, 使用注解或反射可能会带来意想不到的后果, 如果需要用其他Bean, 实现BeanFactoryAware接口, 通过编程方式获取;
  • 容器不会主动调用被创建的对象的销毁方法(如Closeable.close()), 因此, (如有需要)一个FactoryBean应该实现DisposableBean接口, 并将所有与销毁相关的操作通过代理的方式实现;

总之, 这是一个在Spring框架内部被大量使用, 开发者也可以用来自定义组件的一个接口, 至于如何自定义组件, 由于笔者初来乍到, 水平有限, 以后用上就会知道的, 现在先让我们看看其方法声明:

一共就三个方法, 用法一目了然, 其他就不多说了, 注意一下isSingleton这个方法, 可以用来控制生成的对象是否是单例, 下面做一个简单演示:

  1. 新建一个FactoryBean实现类FactoryBeanImpl:
package com.yjzzjy4.learning.beans;

import org.springframework.beans.factory.FactoryBean;

public class FactoryBeanImpl implements FactoryBean<Employee> {
    @Override
    public boolean isSingleton() {
        return false;
    }

    @Override
    public Employee getObject() throws Exception {
        return new Employee("Johnathon");
    }

    @Override
    public Class<?> getObjectType() {
        return Employee.class;
    }
}
  1. 创建一个新的配置文件Spring-Conf-9.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">

    <!--这里的class写的是FactoryBean的类型-->
    <bean id="factoryBeanImpl" class="com.yjzzjy4.learning.beans.FactoryBeanImpl"/>
</beans>
  1. 新建一个测试类Test9, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test9 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-9.xml");

        // 请注意, 这里获取的对象类型是Employee;
        Employee employee = context.getBean("factoryBeanImpl", Employee.class);
        System.out.println(employee + "\n" + employee.getName());

        // 再获取一个对象, 观察区别;
        employee = context.getBean("factoryBeanImpl", Employee.class);
        System.out.println(employee + "\n" + employee.getName());
    }
}

运行结果如下:

注意事项:

  • 很容易可以看出, 我们获取的对象其实是FactoryBeangetObject创建的那个对象, 但是在配置文件中声明的时候, 需要指定<bean>class属性为FactoryBean实现类的类型;
  • 由于设置了isSingleton的返回值为false, 因此获取的对象是不同对象, 看输出即可看出.

Bean的作用范围

上个例子中我们看到, 设置FactoryBeanisSingleton方法返回值可以决定创建的对象是否是单例, 对于普通的Bean, Spring可以通过<bean>scope属性进行设置, 常用值有:

  • singleton: 单例, Spring的默认值, 每次从容器中获取的都是同一个对象.
  • prototype: 多例, 每次获取都创建一个新的对象;
  • session: 用于web编程, 在一次会话(Session)中有效;
  • request: 用于web编程, 在一次请求(Request)中有效.

其中singletonprototype属性值还有一个区别:

前面说过: 默认情况下BeanFactory到获取对象的时候才创建, ApplicationContext在加载配置文件的时候就创建对象, 而当<bean>标签的scope值为prototype时, ApplicationContext也只等到对象被获取(直接获取或被其他对象依赖)时才会创建.

我们重点关注singletonprototype两个属性值, 举例来说明一下其区别:

  1. 创建一个新的配置文件Spring-Conf-10.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="financeDept" class="com.yjzzjy4.learning.beans.Department">
        <constructor-arg name="name" value="财务部"/>
    </bean>
    <bean id="jessie" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="Jessie"/>
        <property name="department" ref="financeDept"/>
    </bean>
    <bean id="sara" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="Sara"/>
        <property name="department" ref="financeDept"/>
    </bean>

    <!--唯一的多例Bean-->
    <bean id="techniqueDept" class="com.yjzzjy4.learning.beans.Department" scope="prototype">
        <constructor-arg name="name" value="技术部"/>
    </bean>
    <bean id="aaron" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="Aaron"/>
        <property name="department" ref="techniqueDept"/>
    </bean>
    <bean id="johnathon" class="com.yjzzjy4.learning.beans.Employee">
        <constructor-arg name="name" value="Johnathon"/>
        <property name="department" ref="techniqueDept"/>
    </bean>
</beans>
  1. 新建一个测试类Test10, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Department;
import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.HashSet;
import java.util.Set;


public class Test10 {
    private static void outTest(Set<Department> departments, Set<Employee> employees) {
        for(Department department : departments) {
            System.out.println(department + " " + department.getName());
        }

        System.out.println();

        for(Employee employee : employees) {
            System.out.println(employee + " " + employee.getName());
            System.out.println(employee.getDepartment() + " " + employee.getDepartment().getName() + "\n");
        }
    }
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-10.xml");

        Department financeDept = context.getBean("financeDept", Department.class);
        Department techniqueDept = context.getBean("techniqueDept", Department.class);

        Employee jessie = context.getBean("jessie", Employee.class);
        Employee sara = context.getBean("sara", Employee.class);
        Employee aaron = context.getBean("aaron", Employee.class);
        Employee johnathon = context.getBean("johnathon", Employee.class);

        // 创建集合并加入元素;
        Set<Department> departments = new HashSet<>();
        departments.add(financeDept);
        departments.add(techniqueDept);

        Set<Employee> employees = new HashSet<>();
        employees.add(jessie);
        employees.add(sara);
        employees.add(aaron);
        employees.add(johnathon);

        System.out.println("Before: ");
        outTest(departments, employees);

        // 重新获取对象, 并修改属性值;
        financeDept = context.getBean("financeDept", Department.class);
        techniqueDept = context.getBean("techniqueDept", Department.class);
        financeDept.setName("财务部-New");
        techniqueDept.setName("技术部-New");

        // 重新加入集合;
        departments.add(financeDept);
        departments.add(techniqueDept);

        System.out.println("After: ");
        outTest(departments, employees);
    }
}

运行结果如下:

对结果说明如下:

  1. 首先, 程序中创建了4个Employee的对象, 2个Department的对象, 其中一个Department的Bean: techniqueDept是多例, 其余均为单例(默认);
  2. 一开始取出这些对象, 并将其分别置于两个集合中: employees、departments, 类型均为HashSet. 而Set集合有自动去重的功能, 由于没重写这些类的equalshashCode方法, 因此对象判重直接根据其在JVM中分配的内存地址进行;
  3. 输出两个容器中的所有对象的关键值, 可以看到: 依赖于单例Bean注入属性的对象, 其对应属性引用的是同一个对象, 即jessie和sara这两个对象的department属性引用同一个对象, 其地址和departments集合中的那个对象相同, 而aaron和johnathon这两个对象的department属性引用不同对象, 且其地址和departments集合中的那个对象不同;
  4. 再次从容器中取出两个Department的Bean, 并且对其name属性进行修改, 再依次放入departments集合中, 然后再次输出两个集合中的所有对象;
  5. 此时departments中有3个对象, 说明有一个对象在放入集合时被去重了, 很显然是那个financeDept对象, 与前一次结果对比, 不难看出新增了一个配置id为techniqueDept的对象, 且其name属性值被改成了"技术部-New", 容器中原有的那个配置id为financeDept的对象的name属性值被改成了"财务部-New";
  6. 此时employees中对象id没有发生变化, 与前一次结果对比, 只是jessie和sara这两个对象的department.name属性都变成了"财务部-New".

注意事项:

  • scope属性默认使用singleton, 则当这个<bean>作为外部Bean注入到其他Bean中时, 可能会引发问题: 修改外部Bean的属性时, 其他Bean的相应属性也会被修改, 因为所谓"注入", 其实就是一个引用的过程(当然作为内部Bean是不存在这个问题的, 但内部Bean的scope属性其实也没有意义). 当项目复杂的时候, 往往可能会在某个地方进行修改而影响到其他地方, 发生意想不到的结果, 因此最好只在一些无状态的Bean中使用singleton, 比如Service层的Bean;
  • Spring中的单例不是线程安全的, 因此在多线程环境下不能认为用默认的单例是可靠的, 还是要自己加上同步方法.

自动装配

以上的例子中, 内部Bean也好外部Bean也好, 对于依赖注入的实现全都是手动指定的, 也就是手动装配. Spring支持自动装配, 即不用显示指定注入的Bean, 通过定义规则即可让Spring自动将符合的Bean进行装配, Spring通过<bean>标签的autowire属性值, 支持两种自动装配的规则:

  • 按名称自动装配: autowire="byName";
  • 按类型自动装配: autowire="byType".
  1. 创建一个新的配置文件Spring-Conf-11.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="aaron" class="com.yjzzjy4.learning.beans.Employee" autowire="byName">
        <constructor-arg name="name" value="Aaron"/>
    </bean>
    <bean id="johnathon" class="com.yjzzjy4.learning.beans.Employee" autowire="byType">
        <constructor-arg name="name" value="Johnathon"/>
    </bean>
    <bean id="department" class="com.yjzzjy4.learning.beans.Department"/>
</beans>
  1. 新建一个测试类Test11, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.Employee;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test11 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-11.xml");

        Employee aaron = context.getBean("aaron", Employee.class);
        System.out.println(aaron + " " + aaron.getName() + " " + aaron.getDepartment());

        Employee johnathon = context.getBean("johnathon", Employee.class);
        System.out.println(johnathon + " " + johnathon.getName() + " " + johnathon.getDepartment());
    }
}

运行结果如下:

注意事项:

  • byName: 只有当id属性值和要装配的Bean的属性名称相同时才会进行装配, 比如在这个例子中, 有id属性值是department的Bean, 且Employee类中有一个属性也是department, 则进行自动装配, 当然不止是名称匹配就可以, 当类型不匹配时, 可能出现装配异常;
  • byType: 按照类型匹配自动装配, 但是配置文件中同一类型的Bean有多个时, 则无法进行自动装配, 因为无法确定使用哪个Bean进行装配.

外部属性文件

我们可以将一些常用的属性提取成属性文件, 再引入到XML配置文件中, 这样可以减少XML配置文件的内容, 提高复用, 修改常用属性的时候也不需要一个一个配置文件进行修改, 只需要修改外部属性文件即可, 最常见的是数据库连接之类的全局属性可以抽取出来, 下面进行演示:

  1. 新建一个属性文件, jdbc.properties:
prop.driverClass=com.mysql.cj.jdbc.Driver
prop.url=jdbc:mysql://localhost:3306/testDb
prop.username=root
prop.password=root
  1. 声明一个Bean, 叫做DataSource, 如下:
package com.yjzzjy4.learning.beans;

public class DataSource {
    private String driver;
    private String url;
    private String username;
    private String password;

    public DataSource() {}
    
    @Override
    public String toString() {
        return "DataSource{" + "driver='" + driver
                + '\'' + ", url='" + url + '\''
                + ", username='" + username
                + '\'' + ", password='"
                + password + '\'' + '}';
    }

    // getter && setter...
}
  1. 创建一个新的配置文件Spring-Conf-12.xml, 注意context名称空间的引入, 如下:
<?xml version="1.0" encoding="UTF-8"?>
<!--引入context名称空间-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--引入外部属性文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!--读取外部属性文件, 完成Bean的属性配置-->
    <bean id="dataSource" class="com.yjzzjy4.learning.beans.DataSource">
        <property name="driver" value="${prop.driverClass}"/>
        <property name="url" value="${prop.url}"/>
        <property name="username" value="${prop.username}"/>
        <property name="password" value="${prop.password}"/>
    </bean>
</beans>
  1. 新建一个测试类Test12, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.DataSource;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test12 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-12.xml");

        DataSource ds = context.getBean("dataSource", DataSource.class);
        System.out.println(ds);
    }
}

运行结果如下:

注意事项:

  • 属性名称一般建议以prop开头, 以免混淆;
  • 引入context名称空间才可以引入外部属性文件, 使用<context:property-placeholder>标签的location属性, 注意属性值可以写file:xxx也可以写classpath:xxx, 取决于你想通过文件路径还是类路径定位属性文件;
  • 引入属性时, 要使用Spring表达式, 把要引入的属性名写在${}中;
  • 引入context名称空间的写法是固定的, 无需记忆.

Bean的生命周期

Spring中Bean的生命周期, 一共有7个部分, 如下:

  1. 通过构造器创建Bean实例(默认无参构造器);

  2. 通过setter为Bean的属性设置值或引用;

  3. 将Bean的实例传递给后置处理器的方法(postProcessBeforeInitialization)进行处理;

  4. 调用Bean的初始化方法(需要手动配置该方法);

  5. 将Bean的实例传递给后置处理器的方法(postProcessAfterInitialization)进行处理;

  6. Bean准备完成, 可以获取并使用了;

  7. 容器关闭的时候, 调用Bean的销毁方法(需要手动配置该方法).

如果配置中没有添加后置处理器, 则Bean的生命周期将不会有2和4两步, 下面进行演示:

  1. 先实现一个后置处理器PostProcessor, 如下:
package com.yjzzjy4.learning.beans;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class PostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessBeforeInitialization");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessAfterInitialization");
        return bean;
    }
}
  1. 再新建一个Bean用以测试, BeanLifeCycle, 如下:
package com.yjzzjy4.learning.beans;

public class BeanLifeCycle {
    private String name;
    
    public BeanLifeCycle() {
        System.out.println("constructor");
    }

    public void initMethod() {
        System.out.println("initMethod");
    }

    public void destroyMethod() {
        System.out.println("destroyMethod");
    }
    
    public void setName(String name) {
        this.name = name;
        System.out.println("setter");
    }
    
    // getter...
}
  1. 创建一个新的配置文件Spring-Conf-13.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="beanLifeCycle" class="com.yjzzjy4.learning.beans.BeanLifeCycle" init-method="initMethod" destroy-method="destroyMethod">
        <property name="name" value="value"/>
    </bean>

    <!--配置后置处理器-->
    <bean id="postProcessor" class="com.yjzzjy4.learning.beans.PostProcessor"/>
</beans>
  1. 新建一个测试类Test13, 如下:
package com.yjzzjy4.learning.test;

import com.yjzzjy4.learning.beans.BeanLifeCycle;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test13 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Conf-13.xml");
        BeanLifeCycle blc = context.getBean("beanLifeCycle", BeanLifeCycle.class);
        System.out.println("got bean instance");
        System.out.println(blc);

        // 关闭容器, 自动调用所有Bean的销毁方法;
        ((ClassPathXmlApplicationContext)context).close();
    }
}

运行结果如下:

注意事项:

  • 实现了BeanPostProcessor的类可以当成后置处理器使用;
  • 后置处理器是全局的, 将会在所有Bean实例化时都添加上, 前提是要先在配置文件中配置;
  • <bean>标签的init-methoddestroy-method属性可以配置初始化方法、销毁方法;
  • 在容器关闭时才会调用Bean的销毁方法, ApplicationContext没有close方法, 其实现类中有;

其他

补充一下注入一些特殊值的细节问题.

  • 注入null值: 很简单, 直接在<property>中嵌套使用<null/>即可, 或者干脆不指定值, 引用类型的属性默认值就是null, 使用<null/>的例子如:
<property name="bookName">
    <null/>
</property>
  • 注入XML中语法不允许直接出现的特殊值, 如: <、>等, 有以下两种做法:
  1. 转义: &lt、&gt等, 比如:
<property name="bookName" value="&lt&lt我的大学&gt&gt"/>
  1. 使用CDATA, 比如:
<property name="bookName">
    <value>
        <![CDATA[<<我的大学>>]]>
    </value>
</property>

以上就是本人对Spring IoC的XML实现方式的一个学习总结, 难免出现疏漏和笔误的地方, 还请多多谅解, 更详细的技术原理、更新更全面的使用规范, 请自行查阅Spring官方文档、API和官网的学习版块.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值