基于Java的显式配置
我们知道基于XML的显式配置就是采用XML描述Bean的创建信息,告诉Spring容器具体需要创建哪些Bean。自然而然的,基于Java的显式配置则是采用Java这种编程语言描述Bean的创建信息,告诉Spring容器具体需要创建哪些Bean。至于如何描述;让我们趁热打铁,紧接前文,看看同样的项目能用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其Id默认是配置方法的方法名。当然,我们也能设置@Bean注解的name属性,指定我们想要的Id
因此,AppConfig配置类实现的produceMusic方法带有@Bean(name="music")注解,能被Spring容器发现之后进行调用,创建一个类型为Music,id为music的Bean;实现的producePlayer方法带有@Bean(name="player")注解,能被Spring容器发现之后进行调用,创建一个类型为Player,id为player的Bean
于是,AppConfig配置类完成了,可以把它交给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应用上下文实例的时候传入了AppConfig.class。这样,Spring应用上下文就能加载AppConfig配置类进行Bean的创建了。之后,我们使用Spring应用上下文里的Bean顺利实现了音乐播放器。
可是,完成实现之后我们却开始困惑了。
前文曾经提及,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注解之后并不会直接加载配置类,而是基于配置类生成一个代理类,基于带有@Bean注解的配置方法生成代理方法。因此,每次调用produceMusic方法创建music对象时并不是直接调用produceMusic方法创建music对象,而是调用代理类里的代理方法进行创建。
代理方法创建Bean之前会先判断一下即将创建的Bean Spring应用上下文里是不是已经有了。如果已经有了,则直接返回Spring应用上下文里的Bean,不再创建;如果还没有,则调用配置方法进行创建。由是我们的困惑解开了,Spring应用上下文加载配置类之后创建的Bean默认还是单例的。
那么,producePlayer方法创建的player对象和producePlayer_2方法创建的player对象也是同一个吗?
当然不是。producePlayer方法和producePlayer_2方法是两个不同的方法,不同的方法运行不同的代码创建的Bean当然是不同的。至于代理,则是Java这门编程语言的一个高级特性,超出本书的讨论范围。如果大家对此不太了解又想深入学习的话,建议大家阅读一下关于代理的Java书籍或文章。这里不作介绍。
至此,基于Java的显式配置的基础知识介绍完了。和基于XML的显式配置相比,基于Java的显式配置至少具有两大优点。首先,基于Java的显式配置是类型安全的。这意味着编译代码即能暴露一些写错了的配置信息。比如某个类名写错了,编译一下就能知道。而XML只是文本文件,诸如这样的问题只在运行的时候才能发现,编译的时候是发现不了的。其次,基于Java的显式配置功能更加强大。只要我们愿意,我们可往配置类里写任何代码进行对象的创建。虽然并不建议。
更加令人惊喜的是,除了显式配置,Spring还提供了自动配置这样一种更加让人着迷的配置方式。至于自动配置有多迷人,我们将在下章揭开它的面纱。欢迎大家继续阅读,谢谢大家!