如何玩转Spring Java Config
随着SpringBoot的兴起,Spring所鼓励的配置方式也逐渐由传统的xml的方式在向Java Config的方式来倾斜。
我们今天就来讲讲Java Config配置的一些内容。
文章的内容参考了Defining Bean Dependencies With Java Config in Spring Framework。
1. 内部Bean引用
这种方式也是Spring reference doc中所介绍的方式。
我们定义如下几个类:
public class MyRepository {
public String findString() {
return "some-string";
}
}
public class MyService {
private final MyRepository myRepository;
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
public String generateSomeString() {
return myRepository.findString() + "-from-MyService";
}
}
此时我们如果想将MyRepository注入到MyService中的话,只需要按照如下的内部引用即可:
@Configuration
class MyConfiguration {
@Bean
public MyService myService() {
return new MyService(myRepository());
}
@Bean
public MyRepository myRepository() {
return new MyRepository();
}
}
是不是很简单?除了这种方式,我们还可以引用.property文件中的属性。比如如果我们希望在MyRepository中加入前缀和后缀的话,则需要如下的MyRepository类:
public class MyRepository {
private final String prefix;
private final String suffix;
public MyRepository (String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
public String findString() {
return prefix + "-some-string-" + suffix;
}
}
此时我们的Java Config文件可以按照如下方式进行配置:
@Configuration
class MyConfiguration {
@Bean
public MyService myService() {
return new MyService(myRepository(null, null));
}
@Bean
public MyRepository myRepository(@Value("${repo.prefix}") String prefix,
@Value("${repo.suffix}") String suffix) {
return new MyRepository(prefix, suffix);
}
}
等等?是不是感觉对这样的用法感觉有些奇怪呢。我们明明在构建myService Bean的时候通过构造器注入(constructor injection)传入的参数都是null,那这个配置到底是如何工作的呢?
实际上,被@Configuration注解的类都会被CGLIB代理,从而使得我们被@Bean注解的方法能够实现:
- 该方法返回的对象会被注入到Spring的容器中。
- 调用该方法返回的对象是个单例,每次都返回同样的Bean。
因此,每次我们调用该方法(比如上面例子中参数均为null),无论参数为如何,均能够返回正确的Bean。
当然,需要注意的是,由于使用了CGLIB代理,所以config类和Bean方法不能够是private或者final方法。
还有一点需要说明,@Bean方法不光能用于被@Configuration注解的类中,还能够用于被类似@Component注解的类中。但是当被@Componet注解的时候,该配置类并不会被CGLIB代理,因此@Bean方法每次返回的都是新的实例(不再是单例)。这种模式也被称为轻量模式(Lite mode)。
以下的例子证明了这点:
当我们使用@Configuration时:
@Configuration
public class MyConfiguration {
@Bean
public MyService myService() {
return new MyService(myRepository());
}
@Bean
public MyRepository myRepository() {
return new MyRepository();
}
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfiguration.class);
MyConfiguration myConfiguration = ac.getBean("myConfiguration", MyConfiguration.class);
MyRepository myRepository1 = myConfiguration.myRepository();
MyRepository myRepository2 = myConfiguration.myRepository();
System.out.println("is singleton : " + (myRepository1 == myRepository2));
System.out.println(ac.getBean("myConfiguration").getClass());
}
}
输出:
is singleton : true
class com.spring.javaconf.MyConfiguration$$EnhancerBySpringCGLIB$$eb2da6d3
可见,通过@Configuration注解的类司机都是通过CGLIB进行了增强,使得我们从容器中获得的bean都是单例的形式。
当我们使用@Component的时候:
@Component
public class MyConfiguration {
@Bean
public MyService myService() {
return new MyService(myRepository());
}
@Bean
public MyRepository myRepository() {
return new MyRepository();
}
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfiguration.class);
MyConfiguration myConfiguration = ac.getBean("myConfiguration", MyConfiguration.class);
MyRepository myRepository1 = myConfiguration.myRepository();
MyRepository myRepository2 = myConfiguration.myRepository();
System.out.println("is singleton : " + (myRepository1 == myRepository2));
System.out.println(ac.getBean("myConfiguration").getClass());
}
}
输出
is singleton : false
class com.spring.javaconf.MyConfiguration
可见,此时的实例并没有被代理。而配置类返回的Bean方法也每次返回的不同对象。
这种通过内部Bean(inter-bean reference)的配置方式对于所有bean的配置均处于一个@Configuration中时是非常有效的。但是如果我们希望跨类来进行配置呢?
2. 多个类之间的bean引用
2.1 通过Autowired
在Spring官方文档Referencing beans across @Configuration classes就介绍了这种方式。
比如我们修改我们的测试类如下:
MyConfiguration.class
@Configuration
public class MyConfiguration {
@Autowired
private MyRepository myRepository;
@Bean
public MyService myService() {
return new MyService(myRepository);
}
}
MyConfiguration2.class
@Configuration
public class MyConfiguration2 {
@Bean
public MyRepository myRepository() {
return new MyRepository();
}
}
这种方式也是可以的。
甚至我们可以通过全限定(full-qualified bean reference)的方式来进行引用。
比如:
@Configuration
public class MyConfiguration {
@Autowired
private MyConfiguration2 myConfiguration2;
@Bean
public MyService myService() {
return new MyService(myConfiguration2.myRepository());
}
}
2.2 通过Bean方法参数
如果我们不喜欢将方法调用作为参数,或者我们需要注入的bean是在另一个配置文件中定义的。那这个时候采用内部bean引用可能就不再适用了,此时我们就需要采用方法参数引用。
@Configuration
class MyConfiguration {
@Bean
public MyService myService(MyRepository myRepository) {
return new MyService(myRepository);
}
}
等等,如果我们的容器中包含多个MyRepository的bean呢,那么注入的规则是如何的呢。实际这时候注入的规则和@Autowired是一样的,也就是先按照类型进行注入,在按照名称(也就是此时参数的名称必须与bean的名称相符合)。
@Configuration
class MyConfiguration {
@Bean
public MyRepository myFirstRepository() {
return new MyRepository("first", "repository");
}
//a bean that will be injected by name into myService
@Bean
public MyRepository mySecondRepository() {
return new MyRepository("second", "repository");
}
@Bean
public MyService myService(MyRepository mySecondRepository) {
return new MyService(myRepository);
}
}
如果不希望通过方法参数名称这种形式来进行匹配,那么可以采用@Qualifier注解的方式进行匹配,这种方式将会优先于参数名称匹配。
@Configuration
class MyConfiguration {
//a bean that will be injected by name into myService
@Bean
public MyRepository myFirstRepository() {
return new MyRepository("first", "repository");
}
@Bean
public MyRepository mySecondRepository() {
return new MyRepository("second", "repository");
}
@Bean
public MyService myService(@Qualifier("myFirstRepository") MyRepository someRepository) {
return new MyService(someRepository);
}
}
3. 组合Configuration
如果由多个Configuration文件,我们通过@Import命令可以很容易的将几个配置文件组合在一起。
比如:
@Configuration
@Import({MyConfiguration2.class})
public class MyConfiguration {
@Bean
public MyService myService(MyRepository mySecondRepository) {
return new MyService(mySecondRepository);
}
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfiguration.class);
MyService myService = ac.getBean("myService", MyService.class);
System.out.println("result : " + myService.generateSomeString());
}
}
public class MyConfiguration2 {
@Bean
public MyRepository myFirstRepository() {
return new MyRepository("first", "repository");
}
//a bean that will be injected by name into myService
@Bean
public MyRepository mySecondRepository() {
return new MyRepository("second", "repository");
}
}
可以看到,第二个配置文件即使我们没有使用@Configuration注解,也能够通过@Import注解将其配置注入。
4. 总结
在上面,我们介绍了各种通过Java配置的方式,那么我们在实际的应用中应该如何使用呢,通常来讲,啊这个需要依赖与情景与个人或团队的喜好。
比较常用的选择有:
- 当我们在一个配置文件中互相引用的时候,适用于使用内部bean的引用。否则我们采用传递方法参数的方式。
- 当我们有两个配置类在一个上下文中的时候,我们可以采用方法参数或者autowire的形式。
- 当我们有两个或更多的配置类在不同的上下文的时候,我们可以使用Import将其加入到同一个上下文中,并通过方法参数或者autowired的形式。