装配 Bean 的概述
主要介绍如何将自己开发的Bean装配到Spring IoC容器中。
大部分场景下,都会使用ApplicationContext的具体实现类,因为对应的Spring IoC容器功能相对强大。
在Spring中提供了3中方式进行配置:
- 在XML文件中显示配置
- 在Java的接口和类中实现配置
- 隐式Bean的发现机制和自动装配原则
方式选择的原则
- 最优先:通过隐式Bean的发现机制和自动装配的原则
减少程序开发者的决定权,简单又不失灵活。 - 其次:Java接口和类中实现配置
避免XML配置的泛滥,也更为容易。 - 最后: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属性进行引用。
命名空间装配
- 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中不允许数字作为属性的第一个字符,因此必须要添加一个下划线作为前缀。
- 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-命名空间的所有元素:
引入其他配置文件
在实际开发中,随着应用程序规模的增加,系统中元素配置的数量也会大大增加,导致applicationContext.xml配置文件变得非常臃肿难以维护。
解决方案:使用<import>元素导入其他配置文件。
新建bean.xml文件,写好基础约束,把applicationContext.xml文件中配置的元素复制进去。在applicationContext.xml文件中替换为:
<import resource="bean.xml" />
通过注解装配Bean
更多时候已经不再推荐使用XML的方式去装配Bean,而采用注解(annotation)的方式。
- 优势:
- 可以减少XML的配置
- 功能更加强大,技能实现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然后将其注入。
- 先在Package【service】下创建一个StudentService接口:
package service; public interface StudentService { public void printStudentInfo(); }
- 为上面的接口创建一个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容器就会寻找资源,然后将其注入。 - 编写测试类:
// 第一步:修改 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资源。
-
解决方案:
- 自己创建一个新的类来拓展包里的类,然后在新类上使用
@Component
注解,但这样很low。 - 使用
@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个:- name:是一个字符串数组,允许配置多个BeanName
- autowire:标志是否是一个引用的Bean对象,默认值是Autowire.NO
- initMethod:自定义初始化方法
- 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中则使用“#”。