Spring学习笔记(3)——装配Spring Bean详解

装配 Bean 的概述

主要介绍如何将自己开发的Bean装配到Spring IoC容器中。
大部分场景下,都会使用ApplicationContext的具体实现类,因为对应的Spring IoC容器功能相对强大。

在Spring中提供了3中方式进行配置:

  • 在XML文件中显示配置
  • 在Java的接口和类中实现配置
  • 隐式Bean的发现机制和自动装配原则
方式选择的原则
  1. 最优先:通过隐式Bean的发现机制和自动装配的原则
    减少程序开发者的决定权,简单又不失灵活。
  2. 其次:Java接口和类中实现配置
    避免XML配置的泛滥,也更为容易。
  3. 最后:XML方式配置
    简单易懂。典型场景:使用第三方类,无法修改里面的代码,通过XML方式配置使用。

通过 XML 配置装配 Bean

需要引入对应的XML模式文件(XSD),这些文件会定义配置Spring Bean的一些元素。

在IDEA中,通过new->XML Configuration File->Spring Config创建。

一个简单的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">

</beans>

它只是一个格式文件,引入了一个beans的定义,映入了xsd文件,它是一个根元素,这样它所定义的元素将可以定义对应的Spring Bean。

装配简易值
<bean id="c" class="pojo.Category">
	<property name="name" value="测试" />
</bean>
  • id属性是Spring能找到当前Bean的一个依赖的编号,不是必需的属性,唯一性由容器负责检查。
  • name属性可以定义bean元素的名称,能以逗号或空格隔开起多个别名,且可以使用很多特殊字符。不是必须的属性。
  • 如果id和name属性都没有声明的话,那么Spring将会采用**“全限定名#{number}”**的格式生成编号。例如上面,如果没有声明id="c"的话,那么Spring为期生成的编号就是pojo.Category#0,当它第二次声明没有id属性的Bean时,编号就是pojo.Category#1,以此类推。
  • class属性显然就是一个类的全限定名
  • property元素是定义类的属性,其中的name属性定义的是属性的名称,而value是它的值。
    注入自定义类:
<!-- 配置 srouce 原料 -->
<bean name="source" class="pojo.Source">
    <property name="fruit" value="橙子"/>
    <property name="sugar" value="多糖"/>
    <property name="size" value="超大杯"/>
</bean>

<bean name="juickMaker" class="pojo.JuiceMaker">
    <!-- 注入上面配置的id为srouce的Srouce对象 -->
    <property name="source" ref="source"/>
</bean>

这里先定义了一个name为source的Bean,然后再制造器中通过ref属性去引用对应的Bean,而source正是之前定义的Bean的name,这样就可以项目引用了。

注入对象:使用ref属性

装配集合

如果需要装配复杂的集合,比如Set、Map、List、Array和Properties等。如下例:

package pojo;

import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

public class ComplexAssembly {
    
    private Long id;
    private List<String> list;
    private Map<String, String> map;
    private Properties properties;
    private Set<String> set;
    private String[] array;

    /* setter and getter */
}

装配配置:

<bean id="complexAssembly" class="pojo.ComplexAssembly">
    <!-- 装配Long类型的id -->
    <property name="id" value="1"/>
    
    <!-- 装配List类型的list -->
    <property name="list">
        <list>
            <value>value-list-1</value>
            <value>value-list-2</value>
            <value>value-list-3</value>
        </list>
    </property>
    
    <!-- 装配Map类型的map -->
    <property name="map">
        <map>
            <entry key="key1" value="value-key-1"/>
            <entry key="key2" value="value-key-2"/>
            <entry key="key3" value="value-key-2"/>
        </map>
    </property>
    
    <!-- 装配Properties类型的properties -->
    <property name="properties">
        <props>
            <prop key="prop1">value-prop-1</prop>
            <prop key="prop2">value-prop-2</prop>
            <prop key="prop3">value-prop-3</prop>
        </props>
    </property>
    
    <!-- 装配Set类型的set -->
    <property name="set">
        <set>
            <value>value-set-1</value>
            <value>value-set-2</value>
            <value>value-set-3</value>
        </set>
    </property>
    
    <!-- 装配String[]类型的array -->
    <property name="array">
        <array>
            <value>value-array-1</value>
            <value>value-array-2</value>
            <value>value-array-3</value>
        </array>
    </property>
</bean>

集合包含的是复杂对象,则使用ref属性进行引用。

命名空间装配
  1. c-命名空间(constructor)
    在 Spring 3.0 中引入,是在XML中更为简洁地描述构造器参数的方式,要使用它需要在XML的顶部声明其模式:
xmlns:c="http://springframework.org/schema/c"

假设有这个一个类:

package pojo;

public class Student {

   int id;
   String name;

   public Student(int id, String name) {
       this.id = id;
       this.name = name;
   }
   // setter and getter
}

在c-命名空间和模式声明之后,我们就可以使用它来声明构造器参数了:

<!-- 引入 c-命名空间之前 -->
<bean name="student1" class="pojo.Student">
   <constructor-arg name="id" value="1" />
   <constructor-arg name="name" value="学生1"/>
</bean>

<!-- 引入 c-命名空间之后 -->
<bean name="student2" class="pojo.Student"
     c:id="2" c:name="学生2"/>

c-命名空间属性名以"c:"开头,也就是命名空间的前缀。接下来就是要装配的构造器参数名,在此之后如果要注入的参数是对象的话要跟上“-ref”(如c:card-ref="idCard1",则对card这个构造器参数注入之前配置的名为idCard1的bean)。
显然,使用c-命名空间属性要比使用元素精简,并且会直接引用构造器之中参数的名词,这有利于我们使用的安全性。
另一种替代方式:

<bean name="student2" class="pojo.Student"
      c:_0="3" c:_1="学生3"/>

将参数的名词替换成了“0”和“1”,即参数的索引,因为在XML中不允许数字作为属性的第一个字符,因此必须要添加一个下划线作为前缀。

  1. p-命名空间(property)
    c-命名空间通过构造器注入的方式来配置bean,p-命名空间则是用setter的注入方式来配置bean,同样,需要引入声明:
    xmlns:p="http://www.springframework.org/schema/p
    然后我们就可以通过p-命名空间来设置属性:
<!-- 引入p-命名空间之前 -->
<bean name="student1" class="pojo.Student">
    <property name="id" value="1" />
    <property name="name" value="学生1"/>
</bean>

<!-- 引入p-命名空间之后 -->
<bean name="student2" class="pojo.Student" 
      p:id="2" p:name="学生2"/>

我们需要先删掉Student类中的构造函数,不然XML约束会提示我们配置<constructor-arg>元素。

同样的,如果属性需要注入其他Bean的话也可以在后面跟上-ref
3. util-命名空间
工具类的命名空间,可以简化集合类元素的配置,同样的我们需要引入其声明:
xmlns:util="http://www.springframework.org/schema/util"
引入前后的变化:

<!-- 引入util-命名空间之前 -->
<property name="list">
    <list>
        <ref bean="bean1"/>
        <ref bean="bean2"/>
    </list>
</property>

<!-- 引入util-命名空间之后 -->
<util:list id="list">
    <ref bean="bean1"/>
    <ref bean="bean2"/>
</util:list>

下表提供了util-命名空间的所有元素:
util-命名空间的所有元素

引入其他配置文件

在实际开发中,随着应用程序规模的增加,系统中元素配置的数量也会大大增加,导致applicationContext.xml配置文件变得非常臃肿难以维护。
解决方案:使用<import>元素导入其他配置文件
新建bean.xml文件,写好基础约束,把applicationContext.xml文件中配置的元素复制进去。在applicationContext.xml文件中替换为:
<import resource="bean.xml" />

通过注解装配Bean

更多时候已经不再推荐使用XML的方式去装配Bean,而采用注解(annotation)的方式。

  • 优势:
    1. 可以减少XML的配置
    2. 功能更加强大,技能实现XML的功能,也提供了自动装配的功能。采用了自动装配后,开发人员所需要做的决断就少了,更有利于程序的开发。这就是**“约定优于配置”**的开发原则。

在Spring中,它提供了两种方式来让Spring IoC容器发现bean:

  • 组件扫描:通过定义资源的方式,让Spring IoC容器扫描对应的包,从而把bean装配进来。
  • 自动装配:通过注解定义,使得一些依赖关系可以通过注解完成。
    把之前的Studeng类改一下:
package pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component(value = "student1")
public class Student {

    @Value("1")
    int id;
    @Value("student_name_1")
    String name;

    // getter and setter
}
  • @Component注解:表示Spring IoC会把这个类扫描成一个 bean 实例,而其中的value属性表示这个类在Spring中的id ,相当于在XML中定义的 Bean 的id:<bean id="student1" class="pojo.Student" />,也可以简写成@Component("student1"),甚至直接写成@Component,对于不写的,Spring IoC 容器默认以类名来命名作为id(只不过首字母小写)配置到容器中。
  • @Value注解:表示值得注入,跟在XML中写value属性一样。
    这样我们就声明好了我们要创建的一个Bean,就像在XML中写了这样一段:
<bean name="student1" class="pojo.Student">
    <property name="id" value="1" />
    <property name="name" value="student_name_1"/>
</bean>

但是现在我们声明了这个类,并不能进行任何的测试,因为Spring IoC并不知道这个Bean的存在,这个时候我们可以使用一个StudentConfig类去告诉Spring IoC:

package pojo;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class StudentConfig {
}

需要说明的是:

  • 该类和Student类位于同一包名
  • @ComponnetScan注解:代表进行扫描,默认是扫描当前包的路径,扫描所有带有@Component注解的POJO。
    这样我们就可以通过Spring定义好的Spring IoC容器的实现类——AnnotationConfigApplicationContext去生成IoC容器了:
ApplicationContext context = new AnnotationConfigApplicationContext(StudentConfig.class);
Student student = (Student) context.getBean("student1", Student.class);
student.printInformation();

弊端

  • 对于@ComponentScan注解,它只是扫描所在包的Java类,但更多时候我们希望是可以扫描我们指定的类
  • 上面的例子只是注入了一些简单的值,通过@Value注解并不能注入对象

@ComponentScan注解存在两个配置项:

  • basePackages:它是由base和package两个单词组成的,而package还用了复数,意味着它可以配置一个Java包的数组,Spring会根据它的配置扫描对应的包和子包,将配置好的Bean装配进来。
  • basePackageClasses:它由base、package和class三个单词组成,采用复数,意味着它可以配置多个类,Spring会根据配置的类所在的包,为包和子包进行扫描装配对应配置的Bean。

重构StudentConfig类来验证上面两个配置项:

package pojo;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackages = "pojo")
public class StudentConfig {
}

//  —————————————————— 【 宇宙超级无敌分割线】—————————————————— 
package pojo;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackageClasses = pojo.Student.class)
public class StudentConfig {
}
  • 【basePackages】和【basePackageClasses】如何选择?
    【basePackage】可读性更好,可优先选用。但在修改包名后不会有错误提示,而【basePackageClasses】会有错误提示。
自动装配——@Autowired

两个弊端之一的没有办法注入对象,可以通过自动装配解决。
自动装配技术是一种由Spring自己发现对应的Bean,自动完成装配工作的方式,它会应用到一个十分常用的注解@Autowired,这个时候Spring会根据类型去寻找定义的Bean然后将其注入

  1. 先在Package【service】下创建一个StudentService接口:
    package service;
    public interface StudentService {
        public void printStudentInfo();
    }
    
  2. 为上面的接口创建一个StudentServiceImpl实现类:
    package service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import pojo.Student;
    
    @Component("studentService")
    public class StudentServiceImp implements StudentService {
    
        @Autowired
        private Student student = null;
    
         // getter and setter
    
        public void printStudentInfo() {
            System.out.println("学生的 id 为:" + student.getName());
            System.out.println("学生的 name 为:" + student.getName());
        }
    }
    
    该实现类实现了接口的printStudentInfo()方法,这里的@Autowired注解,表示在Spring IoC定位所有的Bean后,这个字段需要按类型注入,这样IoC容器就会寻找资源,然后将其注入。
  3. 编写测试类:
    // 第一步:修改 StudentConfig 类,告诉 Spring IoC 在哪里去扫描它:
    package pojo;
    
    import org.springframework.context.annotation.ComponentScan;
    
    @ComponentScan(basePackages = {"pojo", "service"})
    public class StudentConfig {
    }
    
    // 或者也可以在 XML 文件中声明去哪里做扫描
    <context:component-scan base-package="pojo" />
    <context:component-scan base-package="service" />
    
    // 第二步:编写测试类:
    package test;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import pojo.StudentConfig;
    import service.StudentService;
    import service.StudentServiceImp;
    
    public class TestSpring {
    
        public static void main(String[] args) {
            // 通过注解的方式初始化 Spring IoC 容器
            ApplicationContext context = new AnnotationConfigApplicationContext(StudentConfig.class);
            StudentService studentService = context.getBean("studentService", StudentServiceImp.class);
            studentService.printStudentInfo();
        }
    }
    

@Autowired注解表示在Spring IoC定位所有的Bean后,再根据类型寻找资源,然后将其注入。
过程:定义Bean =》初始化Bean(扫描)=》根据属性需要从Spring IoC容器中搜寻满足要求的Bean =》满足要求则注入。\

  • 问题:IoC容器可能会寻找失败,此时会抛出异常(默认情况下,Spring IoC容器会认为一定要找到对应的Bean来注入到这个字段,但有时候并不是一定需要,比如日志)
  • 解决:通过配置项required来改变,比如@Autowired(required = false)
    @Autowired注解不仅仅能配置在属性智商,还允许方法配置,常见的Bean的setter方法也可以使用它来完成注入,总之一切需要Spring IoC去寻找Bean资源的地方都可以用到,例如:
/* 包名和imp ort */
public class JuiceMaker {
    ......
    @Autowired
    public void setSource(Source source) {
        this.source = source;
    }
}

在大部分的配置中都推荐使用这样的自动注入来完成,这是Spring IoC帮助我们自动装配完成的,这样使得配置大幅度减少,满足约定优于配置的原则,增强了程序的健壮性。

自动装配的歧义性(@Primary和@Qualifier)

上例中使用@Autowired注解来自动注入一个Source类型的Bean资源,但如果我们现在有两个Source类型的资源,Spring IoC 就会不知所措,不知道究竟该引入哪一个Bean:

<bean name="source1" class="pojo.Source">
    <property name="fruit" value="橙子"/>
    <property name="sugar" value="多糖"/>
    <property name="size" value="超大杯"/>
</bean>
<bean name="source2" class="pojo.Source">
    <property name="fruit" value="橙子"/>
    <property name="sugar" value="少糖"/>
    <property name="size" value="小杯"/>
</bean>

联想到Spring IoC最底层的容器接口——BeanFactory的定义,它存在一个按照类型获取Bean的方法,显然通过Source.class作为参数无法判断使用哪个类实例进行返回,这就是自动装配的歧义性。
为了消除歧义性,Spring提供了两个注解:

  • @Primary注解:代表首要的,当Spring IoC检测到有多个相同类型的Bean资源的时候,会优先注入使用该注解的类。
  • 问题:该注解只是解决了首要的问题,但是并没有选择性的问题。
  • @Qualifier注解:除了按类型查找Bean,Spring IoC容器最底层的接口BeanFactory还提供了按名字查找的方法,如果按照名字来查找和注入不就能消除歧义性了吗?
  • 使用方法:指定注入名称为“source1”的Bean资源:
    /* 包名和import */
    public class JuiceMaker {
        ......
        @Autowired
        @Qualifier("source1")
        public void setSource(Source source) {
            this.source = source;
        }
    }
    
使用@Bean装配Bean
  • 问题:以上都是通过@Component注解来装配Bean,并且只能注解在类上,当你需要引用第三方包的(jar文件),而且旺旺没有这些包的源码,这时候将无法为这些包的类加入@Conponent注解,让它们变成开发环境中的Bean资源。

  • 解决方案:

    1. 自己创建一个新的类来拓展包里的类,然后在新类上使用@Component注解,但这样很low
    2. 使用@Bean注解,注解到方法之上,使其成为Spring中返回对象为Spring的Bean资源。
  • 在Package【pojo】下新建一个用来测试@Bean注解的类:

    package pojo;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class BeanTester {
    
        @Bean(name = "testBean")
        public String test() {
            String str = "测试@Bean注解";
            return str;
        }
    }
    
  • 注意:@Configuration注解相当于XML文件的根元素,必须要,有了它才能解析其中的@Bean注解

  • 然后我们在测试类中编写代码,从Spring IoC容器中获取到这个Bean:

    // 在 pojo 包下扫描
    ApplicationContext context = new AnnotationConfigApplicationContext("pojo");
    // 因为这里获取到的 Bean 就是 String 类型所以直接输出
    System.out.println(context.getBean("testBean"));
    
  • @Bean的配置项共4个:

    1. name:是一个字符串数组,允许配置多个BeanName
    2. autowire:标志是否是一个引用的Bean对象,默认值是Autowire.NO
    3. initMethod:自定义初始化方法
    4. destroyMethod:自定义销毁方法
  • 使用@Bean注解的好处就是能够动态获取一个Bean对象,能够根据环境不同得到不同的Bean对象。

Bean 的作用域

在默认的情况下,Spring IoC容器只会对一个Bean创建一个实例,但有时候我们希望通过Spring IoC容器湖区多个实例,我们可以铜鼓@Scope注解或<bean>元素中的scope属性来设置,例如:

// XML 中设置作用域
<bean id="" class="" scope="prototype" />
// 使用注解设置作用域
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

Spring提供了5种作用域,它会根据情况来决断是否生成新的对象:

作用域类别描述
singleton(单例)在Spring IoC容器中仅存在一个Bean实例 (默认的scope)
prototype(多例)每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时 ,相当于执行new XxxBean():不会在容器启动时创建对象
request(请求)用于web开发,将Bean放入request范围,request.setAttribute(“xxx”),在同一个request获得同一个Bean
session(会话)用于web开发,将Bean放入Session范围,在同一个Session获得同一个Bean
globalSession(全局会话)一般用于Porlet应用环境,分布式系统存在全局session概念(单点登录),如果不是porlet环境,globalSession等同于Session
Spring 表达式语言简要说明

Spring还提供了更灵活的注入方式——Spring表达式(Spring EL)。实际上Spring EL 远比以上注入方式都要强大,它拥有很多功能:

  • 使用Bean的id来引用Bean
  • 调用指定对象的方法和访问对象的属性
  • 进行计算
  • 提供正则表达式进行匹配
  • 集合配置
    简单例子:
package pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component("elBean")
public class ElBean {
    // 通过 beanName 获取 bean,然后注入 
    @Value("#{role}")
    private Role role;
    
    // 获取 bean 的属性 id
    @Value("#{role.id}")
    private Long id;
    
    // 调用 bean 的 getNote 方法
    @Value("#{role.getNote().toString()}")
    private String note;
    /* getter and setter */
}

与属性文件中读取使用的“$”不同,在Spring EL中则使用“#”。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值