如何玩转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注解的方法能够实现:

  1. 该方法返回的对象会被注入到Spring的容器中。
  2. 调用该方法返回的对象是个单例,每次都返回同样的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的形式。

你可能感兴趣的:(如何玩转Spring Java Config)