Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系,我们需要做的就是告诉Spring要创建哪些bean以及这些bean的依赖情况。
Spring提供了三种主要的装配机制,选择哪种方案很大程度上就是个人喜好的问题,你尽可以选择自己最喜欢的方式。
1.隐式的bean发现机制和自动装配
Spring从两个角度来实现自动化装配:
- 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。
- 自动装配(autowiring):Spring自动满足bean之间的依赖。
首先,定义一个接口:
package soundsystem;
public interface CompactDisc {
void play{};
}
以及,CompactDisc实现类SgtPeppers:
package soundsystem;
import org.springframework.stereotype.Component;
@Component("LonelyHeartsClub") // or @Named
public class SgtPeppers implements CompactDisc {
private String title = "Sgt. Pepper's Lonely Hearts Club Band";
private String artist = "The Beatles";
public void play() {
System.out.println("Playing " + title + " by " + artist);
}
}
需要注意的是,SgtPeppers类使用了@Component注解,这个简单的注解表明该类会作为组件类,并告知Spring要为这个类创建一个id为LonelyHeartsClub的bean。不过,组件扫描默认是不启用的。我们还需要显式配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。
package soundsystem;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class CDPlayerConfig {
}
CDPlayerConfig使用了@Configuration注解,表明这是一个spring的配置类,而@ComponentScan注解则能够在Spring中启用组件扫描功能。如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包。因为CDPlayerConfig类位于soundsystem包中,因此Spring将会扫描这个包以及这个包下的所有子包,查找带有@Component注解的类。这样的话,就能发现CompactDisc,并且会在Spring中自动为其创建一个bean。如果你想要将config类放入一个独立的包中以方便管理,也可以为@ComponentScan传入你想要作为扫描基础包的包名,如:
@ComponentScan("org.xxx.packagename")
或者
@ComponentScan(basePackages={"org.xxx.packagename","org.xxx.otherpackagename"})
来指定多个基础包。
为了以后方便重构(有可能改变包名),那么可以采用更加安全的方式
@ComponentScan(basePackageClasses={abc.class, 123.class})
不再使用String类型的名称来指定包,为basePackageClasses属性所设置的数组中包含了类。这些类所在的包将会作为组件扫描的基础包。
如果你更倾向于使用XML来启用组件扫描的话,那么可以使用Spring context命名空间的<context:component-scan>元素。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="package" />
显然,在实际的项目开发中,bean之间的关系不会像上文所列举的那样简单,不同的组件往往相互依赖来完成复杂的任务,所以,我们需要了解一下Spring自动化配置的另外一方面内容,那就是自动装配。
通过自动装配,将一个CD注入到CDPlayer之中:
package soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component // or @Named
public class CDPlayer implements MediaPlayer {
private CompactDisc cd;
@Autowired // or @Inject
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
public void play() {
cd.play();
}
}
构造器上添加了@Autowired注解,这表明当Spring创建CDPlayer bean的时候,会通过这个构造器来进行实例化并且会传入一个实现了CompactDisc的bean。
@Autowired注解不仅能够用在构造器上,还能用在类的任何方法上。比如说,如果CDPlayer有一个insertDisc()方法,那么可以采用如下的注解形式进行自动装配:
@Autowired
public insertDisc(CompactDisc cd) {
this.cd = cd;
}
如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出异常。为了避免异常的出现,你可以将@Autowired的required属性设置为false:
@Autowired(required=false)
public insertDisc(CompactDisc cd) {
this.cd = cd;
}
将required属性设置为false时,Spring会尝试执行自动装配,但是如果没有匹配的bean的话,Spring将会让这个bean处于未装配的状态。但是,把required属性设置为false时,你需要谨慎对待。如果在你的代码中没有进行null检查的话,这个处于未装配状态的属性有可能会出现NullPointerException。
2.通过Java代码装配bean
大部分情况下组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通时,比如说,你想要将第三方库中
的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component和@Autowired注解的,因此就不能使用自动化装配的方案了。
让我们修改一下CDPlayerConfig类:
package soundsystem;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CDPlayerConfig {
}
我们去掉了@ComponentScan注解,意味着spring容器不再自动扫描我们带有@Component(或@Named)注解的类了,下面我们将在CDPlayerConfig类中显式地配置之前创建的类。
我们在config类中加入以下代码:
package soundsystem;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CDPlayerConfig {
@Bean
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
}
@Bean注解会告诉Spring这个方法将会返回一个对象,该对象要注册为Spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻辑。bean的名字默认为方法名,如有需要,可以使用如下方法修改:
@Bean(name="beanId")
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
使用java配置类进行装配同样要考虑依赖注入的问题,下面就是一种声明CDPlayer的可行方案:
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}
看起来,cdPlayer所依赖的CompactDisc是通过调用sgtPeppers()得到的,但情况并非完全如此。因为sgtPeppers()方法上添加了@Bean注解,Spring将会拦截所有对它的调用,并确保直接返回该方法所创建的bean,而不是每次都对其进行实际的调用,也就是说sgtPeppers仍然是单例的。
但是上面的写法有一个不足,那就是如果sgtPeppers()方法没有定义在CDPlayerConfig中,而是定义在另一个config或xml配置文件中,cdPlayer方法是无法调用的,所以,这种方式并不合理,来看另一种方法:
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc ) {
return new CDPlayer(compactDisc );
}
在这里,cdPlayer()方法请求一个CompactDisc作为参数。当Spring调用cdPlayer()创建CDPlayerbean的时候,它会自动装配(不管以何种方式)一个CompactDisc到配置方法之中。然后,方法体就可以按照合适的方式来使用它。借助这种技术,cdPlayer()方法也能够将CompactDisc注入到CDPlayer的构造器中,而且不用明确引用CompactDisc的@Bean方法。
再次强调一遍,带有@Bean注解的方法可以采用任何必要的Java功能来产生bean实例。
3.通过XML装配bean
// TODO
4.解决不同配置方式之间的依赖关系
现在,我们已经知道cdPlayer bean需要依赖于compactDisc bean,假如我们的cdPlayer bean依然由CDPlayerConfig来装配,而compactDisc bean则改由CDConfig来装配:
package soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CDConfig {
@Bean
public CompactDisc compactDisc() {
return new SgtPeppers();
}
}
一种方法就是在CDPlayerConfig中使用@Import注解导入CDConfig:
package soundsystem;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Bean;
@Configuration
@Import(CDConfig.class)
public class CDPlayerConfig {
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc ) {
return new CDPlayer(compactDisc );
}
}
或者采用一个更好的办法,也就是不在CDPlayerConfig中使用@Import,而是创建一个更高级别的SoundSystemConfig,在这个类中使用@Import将两个配置类组合在一起:
package soundsystem;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
@Configuration
@Import({CDPlayerConfig.class, CDConfig.class})
public class SoundSystemConfig {
}
同样的,假如还有一部分bean定义在bean-config.xml中,则需要用@ImportResource注解导入:
package soundsystem;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
@Configuration
@Import({CDPlayerConfig.class, CDConfig.class})
@ImportResource("classpath:bean-config.xml")
public class SoundSystemConfig {
}