关于为什么要装配 bean 可以先阅读我之前写的两篇:
关于为什么,这里简述下:Spring 容器负责创建应用程序中的 bean 并通过 DI 来协调这些对象对象之间的关系。但是你需要告诉 Spring 要创建哪些 bean,并且这些 bean 需要装配在哪里。
Spring 提供了三种装配 bean 的方式:
- 隐式的 bean 发现机制和自动装配
- 在 Java 中进行显式装配
- 在 XML 中进行显式装配
接下来就分别介绍这三种装配方式,而且在实际项目中吗,我们不是只使用一种方式,而是配合使用,那么我们就需要他们都有什么特点。
1.自动化装配 bean
自动化装配在实际项目中是最常见的,因为如果我们一个个为 bean 手动装配,那确实太麻烦了。所以我们会自动扫描包中所有 bean 并且将其装配,其他有特殊需求的我们在显示装配。
Spring 从两个角度去实现自动化装配:
- 组件扫描:Spring 会自动发现应用上下文中所创建的 bean
- 自动装配:Spring 会自动满足 bean 之间的依赖
这两者需要配合才能达到自动化装配的目的,组件扫描去发现包中的 bean,自动装配则是相当于常见的 new 操作。
1.1 创建可被发现的 bean
先定义一个似乎没什么卵用的接口,接口主要为了降低耦合。
package apple;
public interface AppleDevices {
void useIcloud();
}
接着创建实现类
package apple;
import org.springframework.stereotype.Component;
@Component
public class IPhone implements AppleDevices {
String message = "iCloud Synchronization Completed";
@Override
public void useIcloud() {
System.out.println("iPhone " + message);
}
}
注意,我们在类上加了 @Component
注解,这个注解表明IPhone
这个类已经被作为组件类了,并且告知 Spring 要为这个类创建 bean。但Spring 不会默认就去扫描组件,而是需要我们自己去开启,这里我们先使用 Java 代码去完成开启组件扫描,很简单。
package apple;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class SpringBeanConfig {
}
@Configuration
注解等会再说明,但其实看语义也能猜到,这里关键的是 @ComponentScan
注解,看语义也能知道,它就是开启了组件扫描,如果就这样配置的话,它默认是扫描 SpringBeanConfig 这个类同包类中的 bean。
开启组件扫描还有另一种方式,使用 XML 配置。
在 spring 的配置文件中加入
<context:component-scan base-package="apple"/>
这里我们先使用 java 代码配置的方式。
测试一下是否成功,测试代码如下:
import apple.AppleDevices;
import apple.SpringBeanConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static junit.framework.TestCase.assertNotNull;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringBeanConfig.class)
public class SpringBeanTest {
@Autowired
private AppleDevices appleDevices;
@Test
public void iPhoneIsExistTest(){
assertNotNull(appleDevices);
}
}
@RunWith(SpringJUnit4ClassRunner.class)
是为了在测试开始的时候自动创建 Spring 的上下文,@ContextConfiguration(classes = SpringBeanConfig.class)
则是为了告诉测试类加载哪边的配置。@Autowired
则是为了将 AppleDevices 注入,实际注入的是 AppleDevices 的实现类 IPhone,所以上面的代码其实有个问题,因为我们现在的代码中只有一个 AppleDevices 的实现类,如果有两个或者更多,很明显这样是不对的,idea 也会报错。
这个问题等会我们会去解决。
assertNotNull(appleDevices)
是使用了断言,这是 junit 自带的工具类,使用 jdk 自带的断言则是写成这样:assert appleDevices != null
,效果是一样的。
1.2 为组件扫描的 bean 命名
在上面我们遇到一个问题,就是有两个类 IPhone 和 IPad 都继承了 AppleDevices,当我们在AppleDevices 上加 @Autowired
注解后,spring 并不知道我们要注入的到底是哪个,这时候我们可以给 bean 命名,让 spring 知道我们要注入哪个实现类。
如果我们没有自己去命名,Spring 会给应用上下文中所有的 bean 一个迷人的 ID,ID 的规则就是把类名的第一个字母小写,其他不变,比如 IPhone 类的 ID 就是 iPhone , IPad 类的 ID 就是 iPad。如果要自己去命名,可以这样写。
package apple;
import org.springframework.stereotype.Component;
@Component("smartPhone")
public class IPhone implements AppleDevices {
...
}
这里我们将 IPhone 这个 bean 在 Spring 上下文中重命名为 smartPhone,这时候我们想要注入这个 bean 的时候可以这样写。
@Autowired()
@Qualifier("smartPhone")
private AppleDevices appleDevices;
@Qualifier
这个注解就告诉了 spring 我要注入的是 AppleDevices 的哪个实现类。
@Named
这里还要提到一下的就是这个 @Named
,这是 Java 依赖注入规范中提供的注解来为 bean 设置 Id,它和Component
基本相同,但 Component
在 spring 中更加语义化一点,因为我们是要让他成为了一个组件供我们使用,而不仅仅是命名。而且在 spring 框架中使用 named 还需要额外导包,太麻烦了。还不如用 spring 提供的Component。
1.3 设置组件扫描的基础包
@ComponentScan
现在默认是扫描配置类的同包下的类,我们也可以为其设置要扫描哪个包。
现在把配置包扫描的类 SpringBeanConfig
放到另一个文件夹下面。里面添加 @ComponentScan
的属性,扫描的包为 apple 包下的。测试依然通过。
package config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("apple")
public class SpringBeanConfig {
}
也可以写成这样 @ComponentScan(basePackages = "apple")
,而且 basePackages 是个复数,很明显我们可以同时写多个包,比如 :@ComponentScan(basePackages = {"apple", "google"})
。
除了指定包,还可以直接指定类:@ComponentScan(basePackageClasses = {IPhone.class, IPad.class})
。这样更不容易出差错,但一个个添加太麻烦了。
除此之外,我们也可以在 set 的时候注入,如下就把 AppleDevices 注入进来了:
@Autowired
public void setAppleDevices(AppleDevices appleDevices) {
this.appleDevices = appleDevices;
}
或者在构造函数的时候注入,如下:
@Autowired
public User(AppleDevices appleDevices) {
this.appleDevices = appleDevices;
}
但实际中我发现如果不加注解也能把 AppleDevices 注入进来,为了看的清楚点,我把这个类的代码贴上来
package apple;
import org.springframework.stereotype.Component;
@Component
public class User {
private AppleDevices C;
public User(AppleDevices appleDevices) {
this.appleDevices = appleDevices;
}
public void use() {
appleDevices.useICloud();
}
}
按理说,我们并没有给appleDevices赋值,但实际测试中它确实已经注入进来了,而上面的 set 就不行,这个问题等我解决了再更新,暂时反正都加上就行了。
更新于半天之后。。。
我理解是这样的:
测试类中会注入 User,如下:
@Autowired
private User user;
在 spring 为User 注入的时候,可能就是这样:
@Autowired
private User user = new User(new IPhone());
这个@Autowired 是作用于下面整句话的,所以 IPhone 类也一起被注入进来的。这也是为什么 set 不能少了@Autowired 的原因,因为User 在注入的时候是调用的无参构造,不会吧 IPhone 带进来,所以 IPhone 必须在 set 的时候注入进来,所以必须要注解。
这问题纠结了我一下午,这是我自己理解的,也不一定对,如果觉得有问题的欢迎指出,我们一起讨论
补充:别人给的解释,可能更易懂准确一点:在spring项目启动的时候会去扫描那些注解对应的类,初始化这些类的对象,如果发现类里面有依赖的对象也是spring管理的类,就会先去初始化那些依赖的类的对象,注入到当前的类
还有一点,@AutoWired
的属性:required。默认 required 是 true,如果没有注入进来,会抛异常,如果设为 false,spring 在尝试装配失败会让这个 bean 处于未装配的状态,实际对象也就是 null。
@Inject 功能和 @AutoWired 类似,只不过和 @Named 一样,来源于 Java 依赖注入规范。
2.通过有 Java 代码装配 bean
很多时候组件扫描和自动装配都能帮我们解决问题,但如果想把一些第三方类库里的组件装配到应用中,就必须显式的去装配 bean 了。
这是一个不带注解的正常 java 类:
package apple;
public class IPhone{
public void useICloud() {
System.out.println("iPhone iCloud Synchronization Completed");
}
}
很明显,我们去掉了之前的@Component
注解,配置文件中的包扫描注解 @ComponentScan
也去掉,因为我们暂时不需要是扫描发现应用中的 bean,而是手动去装配 bean。我们需要需要在配置文件中声明 bean,如下:
package apple;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringBeanConfig {
@Bean
public IPhone IPhone() {
return new IPhone();
}
}
@Bean
注解会告诉 Spring 这个方法将会返回一个对象,并且把这个对象注册为 Spring 应用上下文中 Bean。@Bean
注解后面也可以加 name 属性来为这个 bean 命名。
这时候,测试代码依然是通过的:
import apple.IPhone;
import apple.SpringBeanConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static junit.framework.TestCase.assertNotNull;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringBeanConfig.class)
public class SpringBeanTest {
@Autowired
private IPhone iPhone;
@Test
public void iPhoneIsExistTest(){
assertNotNull(iPhone);
}
}
3.通过 XML 装配 bean
虽然 xml 配置是最麻烦的,但在项目中是最常见的,但在 springboot 的项目中已经见不到它了。xml 装配可以与前两者配合使用。
3.1 声明一个简单的 bean
这时候我们的 IPhone 类还是不变,没有加任何注解:
package apple;
public class IPhone{
public void useICloud() {
System.out.println("iPhone iCloud Synchronization Completed");
}
}
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="iPhone" class="apple.IPhone"></bean>
</beans>
配置文件中首先得有个配置规范,也是根命名空间,然后声明一个 bean,通过 class 属性来指定,并给他一个 ID,没有没有定义 ID,那么 ID 会默认为 apple.IPhone#0
,这在 ide 中可能会报错,但实际测试是通过的,可能因为用的人实在太少了。
所以我们一般都会给它一个 ID,这里为iPhone 。当 Spring 发现这个 bean 元素时,会调用 IPhone 的默认构造器来创建 bean。xml 还有一个缺点就是不能保证 class 属性中的类一定存在,但 java config 中基本不会存在这个问题。
3.2 借助构造器注入初始化 bean
3.2.1 将引用注入构造器
也就是说 bean 的构造函数中有一个引用类型的变量,如下:
package apple;
public class User {
private AppleDevices appleDevices;
public User(AppleDevices appleDevices) {
this.appleDevices = appleDevices;
}
public void use() {
appleDevices.useICloud();
}
}
User的构造函数中引用了AppleDevices(IPhone 实现的接口),这时候我们在 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="iPhone" class="apple.IPhone"></bean>
<bean id="user" class="apple.User">
<constructor-arg ref="iPhone"></constructor-arg>
</bean>
</beans>
需要加一个constructor-arg子节点,ref 后面的值是引用已经申明的 bean。
在有了 c-命名空间和模式声明后,我们还可以这样写:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
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="iPhone" class="apple.IPhone"></bean>
<bean id="user" class="apple.User" c:appleDevices-ref="iPhone"/>
</beans>
c:appleDevices-ref="iPhone"
分解一下,c 是命名空间前缀,appleDevices 是构造器中的参数名,必须一样,ref 代表引用。如果觉得写参数名不好,还可以这样写:
<bean id="user" class="apple.User" c:_0-ref="iPhone"/>
_0 表示第一个参数,如果有更多的参数,可以写成 _1, _2
如果只有一个参数,可以直接把0省略,写这样:
<bean id="user" class="apple.User" c:_-ref="iPhone"/>
ide 会报错,但实际是没问题的。
3.2.2 将字面量注入到构造器中
先把 IPhone 类修改一下,加入一个变量:
package apple;
public class IPhone implements AppleDevices{
private String name;
public IPhone(String name) {
this.name = name;
}
public void useICloud() {
System.out.println(name +":iCloud Synchronization Completed");
}
}
xml 配置文件也要随之修改:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
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="iPhone" class="apple.IPhone">
<constructor-arg value="iphone"/>
</bean>
</beans>
与引用不同的是,ref 被换成 value,这可能是值传递引用传递导致的,这里先不讨论。
当然字面量注入也有 c 命名空间的写法:
// 使用构造器参数名称
<bean id="iPhone" class="apple.IPhone" c:name="iphone"/>
// 使用构造器参数的位置
<bean id="iPhone" class="apple.IPhone" c:_0="iphone"/>
上面两种写法都可以,同样,如果只有一个参数,可以写成这样:
<bean id="iPhone" class="apple.IPhone" c:_="iphone"/>
3.2.3 将集合注入到构造器中
这时候 c 命名空间就不行了,只能使用 constructor-arg
。
把 IPhone 类再改造下:
package apple;
import java.util.List;
public class IPhone implements AppleDevices{
private String name;
private String os;
private List<String> photos;
public IPhone(String name,String os, List<String> photos) {
this.name = name;
this.os = os;
this.photos = photos;
}
public void useICloud() {
System.out.println(name + "-" + os +":iCloud Synchronization Completed");
}
}
加入 os 这个属性我仅仅是为了看下属性的顺序,实践证明,构造函数中的变量是什么顺序,在 xml 配置的时候就应该是什么顺序,必须严格遵守。
我们在这个类中加入了一个集合属性,xml 配置中就应该这样写:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
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="iPhone" class="apple.IPhone">
<constructor-arg value="iphone"/>
<constructor-arg value="ios12"/>
<constructor-arg>
<list>
<value>1.jpg</value>
<value>2.jpg</value>
<value>3.jpg</value>
<value>4.jpg</value>
</list>
</constructor-arg>
</bean>
</beans>
如果是 set 集合就把 list 改成 set。
3.3 设置属性
上面一节都是采用构造器注入,但我们还可以使用属性注入。一般来说,如果是强依赖采用构造器注入,如果可选性的依赖使用属性注入。
首先需要在类中加 set 方法
package apple;
public class User {
private AppleDevices appleDevices;
public void setAppleDevices(AppleDevices appleDevices) {
this.appleDevices = appleDevices;
}
public void use() {
appleDevices.useICloud();
}
}
xml 配置也不再使用 constructor-arg ,而是 property。
<bean id="user" class="apple.User">
<property name="appleDevices" ref="iPhone"/>
</bean>
前面有 c 命名空间,这里使用的是 p 命名空间,c 是 constructor-arg 的首字母,p 是 property 的首字母,很好记。
<bean id="user" class="apple.User" p:appleDevices-ref="iPhone"/>
上面是引用,字面量则是:
<bean id="iPhone" class="apple.IPhone">
<property name="name" value="iphone"/>
<property name="os" value="123"/>
<property name="photos">
<list>
<value>1.jpg</value>
<value>2.jpg</value>
<value>3.jpg</value>
<value>4.jpg</value>
</list>
</property>
</bean>
可以指定 name,然后 value 赋值,list 和上面类似。p 命名和 c 命名类似,就不多说了,没有 ref 就是字面量。还有个 util 命名空间可以装配集合,可以这样写:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
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
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<bean id="iPhone" class="apple.IPhone" p:name="ip" p:os="ios" p:photos-ref="photoList"/>
<util:list id="photoList">
<value>1.jpg</value>
<value>2.jpg</value>
<value>3.jpg</value>
<value>4.jpg</value>
</util:list>
<bean id="user" class="apple.User" p:appleDevices-ref="iPhone"/>
</beans>
测试也是可以的。注意声明命名空间。
4. 如何混合使用这三种方式
上面三种方式我们可以在实际项目中混合使用,而不是说我们只能同时使用一种,所以怎么选择还是得看实际情况。
关于第二种 Java config,如果我们全写在一个 Java config 文件中,太冗余了,我们可以在每个 package 下面都写一个自己的,然后使用 @Import
注解来导入到总的那个 Java config 中。如果是 xml 配置的话就使用 @ImportResource
注解来导入到总的那个 Java config 中。
package config;
import apple.AppleConfig;
import apple.IPhone;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
@Configuration
@Import(AppleConfig.class)
@ImportResource("classpath:apple/spring.xml")
public class SpringBeanConfig {
@Bean
public IPhone IPhone() {
return new IPhone();
}
}
相应的,我们可以使用下面方式吧 java config 或者 xml 配置引入同一个 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">
<!-- 引入 xml 配置 -->
<import resource="spring-base.xml"/>
<!--引入 java config-->
<bean class="config.SpringBeanConfig"/>
</beans>
总结
Spring 容器是 Spring 框架的核心,而容器中装的就是这些 bean,因此如何把这些 bean 注入也变得十分关键。一般我们会采用自动化配置为主加上显示配置为辅,而显示配置的两种方式,基于 Java 的配置比基于 xml 的配置更加强大、类型安全和易于重构,所以在以后我会尽量选择基于 Java 的配置,当然具体还要根据实际来选择。