经过前文的学习,我们知道基于XML的显式配置就是采用XML显式配置Spring容器。自然而然的,基于Java的显式配置则是采用Java这种编程语言显式配置Spring容器。至于能用Java怎么配置,让我们趁热打铁,紧接前文,看看同样的项目能用基于Java的显式配置怎么实现,进而学习基于Java的显式配置的基础知识。为此,请打开前文实现的music-player项目,新建AppConfig类,修改如下:
1 package com.dream; 2 3 import org.springframework.context.annotation.*; 4 5 @Configuration 6 public class AppConfig { 7 }
乍眼一瞧,大家肯定得犯嘀咕:“咋写了个啥也没有的类咧!”可是当我们静下心来仔细瞧上一瞧,就会惊人地发现这个荒芜的类上有个神秘的@Configuration注解。
这是怎么回事呢?
原来,@Configuration是Spring提供的一个注解。这个注解可以把某个类标为配置类,使之具有向Spring容器提供配置信息的能力。由此可见,AppConfig既不是一个啥也没有的类,也不是一个普通的类,而是一个具有@Configuration注解的配置类。只因我们尚未往类里添加配置信息,所以空落落的。既然这样,就让我们敲些代码充实一下该类。如下所示:
1 package com.dream; 2 3 import org.springframework.context.annotation.*; 4 5 @Configuration 6 public class AppConfig { 7 @Bean(name = "music") 8 public Music produceMusic() { 9 var music = new Music(); 10 music.setMusicName("执着"); 11 return music; 12 } 13 14 @Bean(name = "player") 15 public Player producePlayer() { 16 var music = this.produceMusic(); 17 var player = new Player(); 18 player.setMusic(music); 19 return player; 20 } 21 }
代码非常简单,就实现了两个方法:一个方法能够创建Music对象;一个方法能够创建Player对象。需要特别留意的是,这两个方法无一例外,都带有一个神秘的@Bean注解。
这是怎么回事呢?
原来,@Bean也是Spring提供的一个注解。这个注解可以把某个方法标为配置方法,使之能被Spring应用上下文发现之后进行调用,从而创建Bean。@Bean注解有个常用的name属性,用于指定Bean的id。其值默认是与之相关的方法的方法名。
于是我们知道了,Spring应用上下文加载AppConfig配置类之后,发现produceMusic方法带有@Bean(name="music")注解。于是调用produceMusic方法创建一个类型为Music,id为music的Bean。发现producePlayer方法带有@Bean(name="player")注解,于是调用producePlayer方法创建一个类型为Player,id为player的Bean。至于Spring应用上下文怎样加载配置类,请看以下代码:
1 package com.dream; 2 3 import org.springframework.context.annotation.*; 4 5 public class Main { 6 public static void main(String[] args) { 7 try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) { 8 var player = context.getBean("player", Player.class); 9 player.play(); 10 player.pause(); 11 } 12 } 13 }
这里用到AnnotationConfigApplicationContext。AnnotationConfigApplicationContext是Spring实现的另外一种Spring应用上下文,能够加载配置类,调用配置方法创建Bean。其构造函数签名如下:
public AnnotationConfigApplicationContext(Class<?>... componentClasses)
这个构建函数接受一个Class<?>... 类型的参数,用于告诉Spring应用上下文能从哪些配置类里加载配置信息。于是,我们创建AnnotationConfigApplicationContext对象的时候传入AppConfig.class,告诉AnnotationConfigApplicationContext加载AppConfig配置类,调用AppConfig配置类里的配置方法创建Bean。
于是,Spring应用上下文加载配置类之后顺利完成了Bean的创建。这些Bean存在Spring应用上下文中,由Spring应用上下文管理着。我们只需调用getBean方法获取id为player,类型为Player的Bean。随后调用Bean的play方法播放音乐,pause方法暂停音乐即可实现音乐播放器。现在运行一下程序,输出如下:
程序是顺利跑起来了,同时我们也开始困惑了。前文曾经提及,Spring应用上下文创建的Bean默认是单例的。可是,Spring应用上下文调用配置方法创建Bean时,不是调用一次方法就创建一个对象,调用两次方法就创建两个对象吗?如此,Spring容器创建的Bean怎么可能还是单例的呢?
为了解开这个迷题,让我们修改一下AppConfig类,添加producePlayer_2方法如下:
1 package com.dream; 2 3 import org.springframework.context.annotation.*; 4 5 @Configuration 6 public class AppConfig { 7 @Bean(name = "music") 8 public Music produceMusic() { 9 var music = new Music(); 10 music.setMusicName("执着"); 11 return music; 12 } 13 14 @Bean(name = "player") 15 public Player producePlayer() { 16 var music = this.produceMusic(); 17 var player = new Player(); 18 player.setMusic(music); 19 return player; 20 } 21 22 @Bean(name="player_2") 23 public Player producePlayer_2() { 24 Music music = this.produceMusic(); 25 Player player = new Player(); 26 player.setMusic(music); 27 return player; 28 } 29 }
现在,producePlayer和producePlayer_2方法都能创建player对象,并且创建player对象时都会调用produceMusic方法创建music对象进行音乐的注入。于是问题来了,producePlayer方法调用produceMusic方法创建的music对象和producePlayer_2方法调用produceMusic方法创建的music对象是同一个吗?
当然是的。实际上,Spring应用上下文瞧见@Configuration注解之后并不会直接加载AppConfig配置类,而是基于AppConfig配置类生成一个代理类;之后又把带有@Bean注解的配置方法生成代理方法。因此,每次调用produceMusic方法创建music对象的时候,并不是直接调用produceMusic方法创建music对象,而是调用Spring应用上下文生成的代理类里的代理方法进行创建。代理方法创建Bean之前会先判断一下即将创建的对象Spring应用上下文里是不是已经有了。如果已经有了,则直接返回Spring应用上下文里的对象,不再创建。如果Spring应用上下文里还没有这个对象,则调用配置方法进行创建。由是我们的困惑解开了,Spring应用上下文加载配置类之后创建的Bean默认还是单例的。
那么,producePlayer方法创建的player对象和producePlayer_2方法创建的player对象也是同一个对象吗?
当然不是。为什么呢?因为这里定义了两个方法,不同的方法运行不同的代码创建的Bean当然是不同的。至于代理,则是Java这门编程语言的一个高级特性,超出本书的讨论范围。如果大家对此不太了解又想深入学习的话,建议大家阅读一下关于代理的Java书籍或文章。这里不作介绍。
至此,基于Java的显式配置的基础知识介绍完了。前文曾经提及,除了显式配置,Spring还提供了自动配置。令人兴奋的是,自动配置还是一种远比显式配置更为迷人的配置方式。至于自动配置有多迷人,我们将在下一章进行介绍。欢迎大家继续阅读,谢谢大家!