Bean的管理方式

228 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第14天,点击查看活动详情

前面我们已经简单地使用了Spring框架,成功地使用配置文件将对象放入了IOC容器,接下来我们补充一些其它内容。

其它注入方式

现在已知的三种注入方式分别是构造方法注入、setter方法注入和接口注入,其实,Spring还提供了一种属性的注入方式,那就是p名称空间,看例子:

<bean id="user" class="com.wwj.spring.demo.User"
      p:name="zs" p:age="20" p:sex="1" p:pet-ref="pet"/>

使用p名称空间需要在xml文件开头添加Schema定义:

xmlns:p="http://www.springframework.org/schema/p"

使用方法非常简单,只要符合p:属性名格式即可。

还有一些非常特殊的场景,比如给某个属性注入一个null值,该如何做呢?

<bean id="user" class="com.wwj.spring.demo.User">
  <constructor-arg name="name" value="null"/>
  <constructor-arg name="sex" value="null"/>
  <constructor-arg name="age" value="null"/>
</bean>

很显然这样是行不通的,首先后面两个null会报错,因为null字符串不能转为Integer类型,而第一个null虽然不报错,但它也违背了我们的需求,它实质上是一个null字符串,而不是一个null值,所以要是想注入一个null值,我们需要用到<null/>标签,如下:

<bean id="user" class="com.wwj.spring.demo.User">
  <constructor-arg name="name">
    <null/>
  </constructor-arg>
  <constructor-arg name="sex">
    <null/>
  </constructor-arg>
  <constructor-arg name="age">
    <null/>
  </constructor-arg>
</bean>

又比如,我想注入一个<张三>字符串到name属性中,很显然,因为<>是xml文件中定义的符号,所以肯定是会出现问题的:

我们需要对其进行转义:

<bean id="user" class="com.wwj.spring.demo.User">
  <constructor-arg name="name">
    <value><![CDATA[<张三>]]></value>
  </constructor-arg>
  <constructor-arg name="sex" value="1"/>
  <constructor-arg name="age" value="20"/>
</bean>

使用<![CDATA[属性值]]的方式即可传入特殊字符。

级联注入

Sping同时支持级联注入,就是在一个对象中若是还有对象类型的属性,则可以级联注入的方式对属性进行赋值,如:

public class User {

    private String name;
    private Integer age;
    private Pet pet;
}

则级联方式注入写法如下:

<bean id="pet" class="com.wwj.spring.demo.Pet"/>

<bean id="user" class="com.wwj.spring.demo.User">
  <property name="name" value="zs"/>
  <property name="age" value="22"/>
  <property name="pet" ref="pet"/>
  <property name="pet.name" value="xg"/>
  <property name="pet.age" value="5"/>
</bean>

注入集合类型

这里我们再介绍一种特殊的注入类型,它就是集合,因为集合的种类很多,有List、Set、Map等等,但是它们的注入方式是类似的,所以直接贴出代码了,首先是Bean类:

public class User {

    private String[] strings;
    private List<String> list;
    private Set<String> set;
    private Map<String, String> map;
    private Properties properties;
}

然后是配置:

<bean id="user" class="com.wwj.spring.demo.User">
  <property name="strings">
    <array>
      <value>1</value>
      <value>2</value>
      <value>3</value>
    </array>
  </property>
  <property name="list">
    <list>
      <value>1</value>
      <value>2</value>
      <value>3</value>
    </list>
  </property>
  <property name="set">
    <set>
      <value>1</value>
      <value>2</value>
      <value>3</value>
    </set>
  </property>
  <property name="map">
    <map>
      <entry key="1" value="1"/>
      <entry key="2" value="2"/>
      <entry key="3" value="3"/>
    </map>
  </property>
  <property name="properties">
    <props>
      <prop key="1">1</prop>
      <prop key="2">2</prop>
      <prop key="3">3</prop>
    </props>
  </property>
</bean>

虽然每一种类型都有对应的标签,比如数组使用array标签注入,List使用list标签注入,而Set使用set标签注入,但事实上,我们完全可以随意使用这些标签进行注入,比如:

<bean id="user" class="com.wwj.spring.demo.User">
  <property name="strings">
    <list>
      <value>1</value>
      <value>2</value>
      <value>3</value>
    </list>
  </property>
  <property name="list">
    <array>
      <value>1</value>
      <value>2</value>
      <value>3</value>
    </array>
  </property>
</bean>

在数组中使用list标签,和在list中使用array标签是完全没有问题的,但为了规范起见,最好还是使用类型对应的标签吧。

工厂Bean

现在我们已经大概了解Spring是如何创建我们的对象的, Spring会从配置文件中读取配置,并根据配置反射创建出对象放入IOC容器中,Spring为了增强扩展性, 提供了一种方式使得我们可以在创建对象之前进行定制化,实现方式是让被创建的类实现FactoryBean接口,如下:

public class User implements FactoryBean {

    private String name;
    private Integer age;

    @Override
    public Object getObject() throws Exception {
        Pet pet = new Pet("sg", 5);
        return pet;
    }

    @Override
    public Class<?> getObjectType() {
        return Pet.class;
    }
}

这一接口给程序提供了大大的灵活性,当我们在配置文件中配置一个User对象时:

<bean id="user" class="com.wwj.spring.demo.User"/>

Spring将会反射创建这个User对象,但Spring发现这个对象实现了FactoryBean接口,就会去调用对象中的getObject方法,所以最终创建的对象是由这个getObject方法决定的,FactoryBean是Spring框架的重要组成部分,感兴趣的同学可以翻阅源码了解它的作用。

Bean的作用域

在Spring中,Bean是有作用域的,而且一共有5种作用域,分别如下:

  1. singleton:在IOC容器中仅存在一个Bean实例,Bean以单例的形式存在
  2. prototype:每次从容器获取Bean时,都会创建一个新的对象返回
  3. request:每次http请求都会创建一个新的对象
  4. session:同一个Session域共享一个Bean实例
  5. application:同一个application域共享一个Bean实例

而默认Spring中的Bean是单例的,也就是说,我们如果没有对Bean的作用域进行任何配置,则它就是单例的。

若是想配置Bean的作用域,也非常简单,只需设置scope属性值即可:

<bean id="user" class="com.wwj.spring.demo.User" scope="prototype">
  <property name="name" value="zs"/>
  <property name="age" value="22"/>
</bean>

我们不妨来测试一下:

public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
    User user = context.getBean("user", User.class);
    User user2 = context.getBean("user", User.class);
    System.out.println(user == user2);
}

若Bean的作用域为prototype,则每次从容器获取Bean时,都会创建一个新的对象,所以结果一定为false:

false

而如果将作用域改为singleton或者不作配置,则结果为true,其它三种作用域使用不多,知道怎么配置即可。

Bean的生命周期

我们已经了解到,Spring是一个高度可扩展的框架,所以Spring在很多地方都提供了扩展点,例如Bean的生命周期,如同人的生命一样,Bean也会经历从出生到消亡的过程,如下:

public class User {

    private String name;
    private Integer age;

    public User() {
        System.out.println("构造方法被调用");
    }

    public void init() {
        System.out.println("初始化方法被调用");
    }

    public void destroy() {
        System.out.println("销毁方法被调用");
    }
}

在User类中有一个init方法和destroy方法,它们分别会在对应的生命周期过程中被Spring调用,不过现在Spring还不认识这两个方法,需要我们进行配置:

<bean id="user" class="com.wwj.spring.demo.User" init-method="init" destroy-method="destroy">
  <property name="name" value="zs"/>
  <property name="age" value="22"/>
</bean>

我们可以来测试一下生命周期方法的调用顺序:

public static void main(String[] args) throws Exception {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
    User user = context.getBean("user", User.class);
}

运行结果:

构造方法被调用
初始化方法被调用

destory方法只有容器关闭时才会调用,即:容器被关闭时才会去销毁对象,如下:

public static void main(String[] args) throws Exception {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
    User user = context.getBean("user", User.class);
    context.close();
}

运行结果:

构造方法被调用
初始化方法被调用
销毁方法被调用

这里需要注意的一点是,构造方法是在初始化方法之前调用的。