正如大家熟知的那样,我们的电脑是由各种部件组成的。比如中央处理器,内存,硬盘,网卡,电源,等等。这些部件一起运转,彼此合作,由是电脑跑起来了,我们可以用她写代码,玩游戏,上网,工作,听歌,画画,等等。
软件开发也是一样的。我们需要定义各种各样的类,每个类都有自己的功能,作用,职责。之后,我们使用这些定义好的类创建各种对象。这些对象相互合作,彼此依赖,由是软件跑起来了,能够按照我们期望的那样工作。
由此可见,一款软件是由各种对象组成的。对象之间一定是相互合作,彼此依赖的。这种依赖就是我们通常所说的耦合。试想,当我们的软件非常庞大,由成千上万个对象组成时,这些对象通过各种关系紧紧耦合在一起,将使软件多么复杂,多么难以维护,多么容易引进错误。那么,有没有什么办法能够解耦,以求降低对象之间的耦合,进而降低软件开发的复杂度和成本呢?
一般而言,面向接口编程可在一定程序上降低对象之间的耦合。可这远远不够。为了简化软件开发,Spring引入两大核心组件,控制反转 (Inversion of Control,IoC)和面向切向编程(Aspect-Oriented Programming,AOP)。通过组合运用Spring的控制反转和面向切面编程,外加Java的面向接口编程,对象之间的耦合是可以大大降低的。对象之间的耦合降低了,软件开发的复杂度和成本也就随之降低了。
当然,要想彻底消除对象之间的耦合是不可能的。毕竟一旦各个对象彼此独立,没有关系了,也就意味着软件不复存在了。试问一旦中央处理器存取不了内存的数据,电脑还跑得动吗?
现在,让我们静下心来,好好看看控制反转是什么。至于面向切向编程,我们将在往后的章节进行介绍。
通过先前Hello World的例子,我们知道有两种方式能够创建对象。一种方式是传统的,我们自己创建对象;一种方式是Spring的,我们只需提供配置文件,告诉Spring应用上下文我们需要创建哪些对象,再由Spring应用上下文根据配置文件提供的信息创建相应的对象即可。
可能大家已经察觉,这种Spring的方式把创建对象的控制权让了出来,转而交给Spring应用上下文。也就是说,创建对象的方式反转了,由我们自己创建转向由Spring应用上下文创建了。于是,一个全新的概念出来了,那就是控制反转。又由于对象之间的依赖是由Spring应用上下文注入的。因此,控制反转也叫依赖注入(Dependency Injection,DI)。
概念既已清楚,就让我们通过实现一个小项目,一边写代码,一边学习控制反转的基础知识。这个小项目是一个简单的音乐播放器,能够播放,暂停和停止音乐。为此,请打开IntelliJ IDEA,新建Maven项目music-player,并向pom文件添加项目所需的依赖:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" 3 http://maven.apache.org/POM/4.0.0 4 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 5 6 <modelVersion>4.0.0</modelVersion> 7 <groupId>com.learning</groupId> 8 <artifactId>music-player</artifactId> 9 <version>1.0-SNAPSHOT</version> 10 11 <properties> 12 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 13 <maven.compiler.source>11</maven.compiler.source> 14 <maven.compiler.target>11</maven.compiler.target> 15 </properties> 16 17 <dependencies> 18 <dependency> 19 <groupId>org.springframework</groupId> 20 <artifactId>spring-core</artifactId> 21 <version>5.2.2.RELEASE</version> 22 </dependency> 23 <dependency> 24 <groupId>org.springframework</groupId> 25 <artifactId>spring-beans</artifactId> 26 <version>5.2.2.RELEASE</version> 27 </dependency> 28 <dependency> 29 <groupId>org.springframework</groupId> 30 <artifactId>spring-context</artifactId> 31 <version>5.2.2.RELEASE</version> 32 </dependency> 33 </dependencies> 34 </project>
项目已经搭好了,应该怎么实现呢?
俗话说:“巧妇难为无米之炊。”音乐播放器就是用来播放音乐的。因此,我们需要音乐。音乐多种多样,可以是古典音乐,乡村音乐,摇滚音乐;也可以是萨克斯,琵琶,二胡。因此,我们最好定义一个音乐接口,让各种类型的音乐实现她。如下所示:
1 package com.learning; 2 3 public interface Music { 4 String name(); 5 }
这个接口非常简单,只有一个name方法,用于获取音乐名。现在,让我们基于这个接口,实现一个古典音乐类:
1 package com.learning; 2 3 public class ClassicMusic implements Music { 4 @Override 5 public String name() { 6 return "古典音乐"; 7 } 8 }
音乐有了,音乐播放器呢?请看以下定义:
1 package com.learning; 2 3 public class Player { 4 private String name; 5 private Music music; 6 7 public Player() { 8 } 9 10 public void play() { 11 System.out.println("音乐播放器" + this.name + "正在播放" + music.name()); 12 } 13 14 public void stop() { 15 System.out.println("音乐播放器" + this.name + "停止播放" + music.name()); 16 } 17 18 public void pause() { 19 System.out.println("音乐播放器" + this.name + "暂停播放" + music.name()); 20 } 21 22 public String getName() { 23 return name; 24 } 25 26 public void setName(String name) { 27 this.name = name; 28 } 29 30 public Music getMusic() { 31 return music; 32 } 33 34 public void setMusic(Music music) { 35 this.music = music; 36 } 37 }
现在,构建项目所需的类全部定义好了。我们有了音乐,也有了音乐播放器。那么,如何把她们装配(Wiring)起来,让音乐播放器播放音乐呢?
这时,我们需要一个配置文件配置一些信息,告诉Spring应用上下文需要创建什么对象,对象之间有什么关系,如何把她们装配起来,等等。为此,请于resources目录添加app-config.xml配置文件如下:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> 5 6 <!--在这里填写配置信息--> 7 8 </beans>
正如大家所见,这是一个XML文件。里面有个<beans>根元素。<beans>根元素列出一些XML模式文件(.xsd),定义了配置文件可以怎么配置。比如配置文件拥有哪些元素,这些元素拥有哪些属性,元素和属性各有什么作用,等等。上面的配置文件列出的http://www.springframework.org/schema/beans/spring-beans.xsd模式文件定义了Bean可以怎么配置。
看到这里,也许大家多少有些困惑,Bean是什么东西呀?其实,Bean就是对象。这些对象由Spring应用上下文创建,装配和管理,并且彼此合作,相互依赖,共同构成我们的应用程序。
那么,Bean应该怎么配置呢?是的,通过<bean>元素。现在,让我们使用<bean>元素配置音乐类和音乐播放器类,告诉Spring应用上下文如何创建她们的实例。如下代码所示:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> 5 <bean id="player" class="com.learning.Player"/> 6 <bean id="classicMusic" class="com.learning.ClassicMusic"/> 7 </beans>
可以看到,<bean>元素拥有id和class两个重要属性。id用于唯一标识一个Bean。这样,Spring应用上下文就能通过id找到并使用Bean了。class用于表明Bean的类型,其值为类的全名。这里,我们定义了两个Bean,一个Bean是Player类型的,一个Bean是ClassicMusic类型的。Spring应用上下文读取配置文件之后,能够通过反射技术调用类的默认构造函数创建相应的Bean。注意,默认情况下,Spring应用上下文创建的Bean是单例的,也就是一个Spring应用上下文有且只有一个实例。当然,我们也可以修改配置文件,告诉Spring应用上下文创建非单例的Bean。这个话题将在往后的章节进行讨论。现在,我们只需知道默认情况下,Spring应用上下文创建的Bean是单例的就可以了。
可能大家已经察觉,这里虽然定义了两个Bean,可是她们并没有被初始化。也就是说,音乐播放器既没名称,也不知道应该播放什么音乐。一切空空如也。那么,应该如何把名称,音乐这些依赖注入音乐播放器之中,以此完成Bean的装配和初始化,让音乐播放器能够播放音乐呢?是的,可以通过属性注入,把音乐播放器需要的名称和音乐注入进去,完成Bean的装配和初始化。这样,音乐播放器就能播放音乐了。如下代码所示:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> 5 <bean id="player" class="com.learning.Player"> 6 <property name="name" value="天籁"/> 7 <property name="music" ref="classicMusic"/> 8 </bean> 9 <bean id="classicMusic" class="com.learning.ClassicMusic"/> 10 </beans>
可以看到,属性注入是通过<property>元素配置的。<property>元素拥有name,value和ref三个重要属性。name属性是Bean的属性名。value和ref属性是Bean的属性值。其中,value是值类型的,ref是引用类型的。这些配置信息用于告诉Spring应用上下文,创建player这个Bean之后,把name的值初始化为“天籁”这个字符串,把music的值初始化为classicMusic这个Bean。于是,Bean的装配和初始化完成了,音乐播放器可以播放音乐了。同时,这也意味着配置文件写好了,可以把她交给Spring应用上下文了。为此,请新建Main类,实现main方法如下:
1 package com.learning; 2 3 import org.springframework.context.support.ClassPathXmlApplicationContext; 4 5 public class Main { 6 public static void main(String[] args) { 7 try (var context = new ClassPathXmlApplicationContext("app-config.xml")) { 8 var player = context.getBean("player", Player.class); 9 player.play(); 10 player.pause(); 11 player.stop(); 12 } 13 } 14 }
运行程序,输出如下:
可以看到,上面的代码把配置文件app-config.xml交给ClassPathXmlApplicationContext。ClassPathXmlApplicationContext是Spring应用上下文的其中一种实现。她能够基于类路径查找一个或多个XML配置文件,找到之后加载XML配置文件完成Bean的创建,装配和初始化。这样,我们需要的Bean就存在于Spring应用上下文之中,由Spring应用上下文管理着了。
现在,假如我们的项目除了app-config.xml,还需另外一个XML配置文件another-config.xml,且该文件位于music-player/src/main/resources/com/learning;这时,我们应该怎样创建ClassPathXmlApplicationContext的实例呢?请看以下示例:
1 package com.learning; 2 3 import org.springframework.context.support.ClassPathXmlApplicationContext; 4 5 public class Main { 6 public static void main(String[] args) { 7 try (var applicationContext = new ClassPathXmlApplicationContext("app-config.xml", "com/learning/another-config.xml")) { 8 var player = applicationContext.getBean("player", Player.class); 9 player.play(); 10 player.pause(); 11 player.stop(); 12 } 22 } 23 }
正如大家所见,ClassPathXmlApplicationContext现在能够加载app-config.xml和another-config.xml两个配置文件了。当然,如果项目需要更多的配置文件,我们只需依照这种方式把她们传给Spring应用上下文就可以了。只是,这里的配置文件使用的是具体的文件名。那么,通配符呢?是不是也支持?当然支持。以下是可以使用的三种通配符:
1. 通配符?用于匹配一个字符。比如,指定路径为com/learning/anothe?-config.xml,则com/learning/another-config.xml,com/learning/anothes-config.xml,com/learning/anothet-config.xml,诸如这样的文件都能找到。
2. 通配符*用于匹配一个或多个字符。比如,指定路径为com/learning/*.xml,则位于com/learning目录的所有.xml文件都能找到。
3. 通配符**用于匹配一个或多个目录。比如,指定路径com/**/another-config.xml,则位于com目录的所有another-config.xml文件都能找到。
看到这里,也许大家会想,假如我们指定了多个配置文件,不同配置文件又存在相同的Bean定义,这样不会冲突吗?确实不会。这种情况出现的时候,定义在后面的Bean会覆盖定义在前面的。
另外,除了ClassPathXmlApplicationContext,FileSystemXmlApplicationContext也是一种常用的Spring应用上下文实现。这种实现能够基于文件系统查找一个或多个XML配置文件,再从找到的配置文件里加载Spring应用上下文。如下代码所示:
1 package com.learning; 2 3 import org.springframework.context.support.ClassPathXmlApplicationContext; 4 5 public class Main { 6 public static void main(String[] args) { 7 try(var context = new FileSystemXmlApplicationContext("target/classes/app-config.xml")) { 8 var player = context.getBean("player", Player.class); 9 player.play(); 10 player.pause(); 11 player.stop(); 12 } 13 } 14 }
可以看到,我们提供给FileSystemXmlApplicationContext的是一个相对于工作目录的路径。假如我们想用绝对路径,则需在指定的路径加上“file:”前缀。如下所示:
package com.learning; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { try(var context = new FileSystemXmlApplicationContext("file:D:/music-player/target/classes/app-config.xml")) { var player = context.getBean("player", Player.class); player.play(); player.pause(); player.stop(); } } }
至此,关于Spring应用上下文的介绍就完了。现在,让我们回到music-player这个项目,看看她是怎样获取Spring应用上下文里的Bean的。
可以看到,我们使用Spring应用上下文的getBean方法获取Bean。getBean方法有两个参数。第一个参数是Bean的id,第二个参数是Bean的类型。这里,我们调用getBean方法,从Spring应用上下文里获取id为player,类型为Player的Bean。之后调用Bean的play,pause和stop方法,播放,暂停和停止音乐。于是,一个简单的音乐播放器完成了。
通过这个小项目,我们可以看到,ClassicMusic,Player这些类的定义既没继承任何Spring相关的类,也没实现任何Spring相关的接口。她们非常简单,就是一些简单老式Java对象(Plain Old Java Object,POJO),和Spring没有任何关系。我们只是提供一个配置文件给Spring,Spring就让一切发生了。整个过程是那样的简单,简洁,没有任何侵入。我们的代码没有因为Spring的引入而混乱,我们的项目没有因为Spring的引入而沉冗,一切是那样的轻量。这也是Spring简化企业级应用软件开发的其中一个方面,更是Spring充满魅力的其中一个原因。
看到这里,可能大家会问,除了上面那种属性注入的方式之外,有没有其她方式也能装配和初始化Bean呢?当然有的,那就是构造函数注入和工厂方法注入。工厂方法注入又分为静态工厂方法注入和实例工厂方法注入。
构造函数注入
很多时候,我们希望一步到位,通过构造函数完成Bean的创建,装配和初始化。这时,构造函数注入就派上用场了。按照构造函数注入的方式,Spring应用上下文采用反射技术创建Bean的时候,会调用相应的带有参数的构造函数,把Bean所需的依赖注入进去,完成Bean的创建,装配和初始化。为此,我们需要修改Player类,添加以下构造函数:
1 public Player(String name, Music music) { 2 this.name = name; 3 this.music = music; 4 }
相应的,也得修改app-config.xml配置文件如下:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> 5 <bean id="player" class="com.learning.Player"> 6 <constructor-arg type="java.lang.String" value="天籁"/> 7 <constructor-arg type="com.learning.Music" ref="classicMusic"/> 8 </bean> 9 <bean id="classicMusic" class="com.learning.ClassicMusic"/> 10 </beans>
可以看到,构造函数注入是通过<constructor-arg>元素配置的。<constructor-arg>元素拥有type,value和ref三个重要属性。type属性是构造函数的参数类型,其值为类的全名。value和ref属性是构造函数的参数值。其中,value是值类型的,ref是引用类型的。这些配置信息用于告诉Spring应用上下文,创建player这个Bean的时候,为类型为String的参数注入“天籁”这个字符串,为类型为Music的参数注入classicMusic这个Bean。Spring应用上下文读取这些信息之后,就能调用相应的构造函数,完成Bean的创建,装配和初始化。
现在,运行一下程序。可以看到,结果和先前的是一样的。
注意,运行程序的时候,如果我们把<constructor-arg>元素的type属性去掉,程序照样也能跑起来。为什么呢?因为Spring默认根据参数的类型匹配参数,也就是什么类型的值赋给什么类型的参数。所以有type属性和没type属性其实是一样的。这时大家也许会问,如果构造函数有些参数的类型是一样的话,匹配难道不会出错吗?确实会的。那该怎么办?
这时,我们可以使用<constructor-arg>元素的index属性。index属性表示构造函数的参数索引,也就是第几个参数。其中,第一个参数的值是0,第二个参数的值是1,第三个参数的值是2,以此类推。这样就能解决匹配出错的问题了。
此外,我们也可以使用<constructor-arg>元素的name属性。name属性表示构造函数的参数名。构造函数每个参数的参数名都是不一样的,因此不会存在匹配出错的问题。只是,如果使用name属性的话,就需在编译代码的时候将调试标志保存在类代码中。如此,当我们优化构建过程,将调试标志移除的时候,这种方式就无法正常工作了。因此,大家了解一下就行,不建议使用。
静态工厂方法注入
总有那么一些时候,我们的类存在一些静态方法,这些静态方法能够创建对象并返回。由于这些方法是静态的,专门用来创建对象。因此,这些方法称为静态工厂方法。那么,Spring应该怎样调用静态工厂方法创建Bean呢?这就需要用到静态工厂方法注入了。通过静态工厂方法注入,Spring应用上下文采用反射技术创建Bean的时候,会调用类的静态工厂方法,把Bean所需的依赖通过方法参数注入进去,完成Bean的创建,装配和初始化。为此,我们需要修改Player类,添加以下静态工厂方法:
public static Player factory(String name, Music music) { return new Player(name, music); }
相应的,也得修改app-config.xml配置文件如下:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> 5 <bean id="player" class="com.learning.Player" factory-method="factory"> 6 <constructor-arg type="java.lang.String" value="天籁"/> 7 <constructor-arg type="com.learning.Music" ref="classicMusic"/> 8 </bean> 9 <bean id="classicMusic" class="com.learning.ClassicMusic"/> 10 </beans>
可以看到,静态工厂方法注入和构造函数注入的差别不大。唯一的区别就是<bean>元素改变了,多了一个factory-method属性用来指定类的静态工厂方法名。
现在,运行一下程序,你会发现结果和先前的是一样的。
实例工厂方法注入
有些时候,我们的类存在一些实例方法,这些实例方法能够创建对象并返回。由于这些方法是实例方法,并且可以用来创建对象。因此,这些方法称为实例工厂方法。那么,Spring应该怎样调用实例工厂方法创建Bean呢?这就需要用到实例工厂方法注入了。通过实例工厂方法注入,Spring应用上下文采用反射技术创建Bean的时候,会调用对象的工厂方法,把Bean所需的依赖通过方法参数注入进去,完成Bean的创建,装配和初始化。为此,我们需要修改Player类,添加以下实例工厂方法:
public Player produce(String name, Music music) { return new Player(name, music); }
相应的,也得修改app-config.xml配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="player-1" class="com.learning.Player"> </bean> <bean id="player-2" factory-bean="player" factory-method="produce"> <constructor-arg value="天籁" /> <constructor-arg ref="classicMusic"/> </bean> <bean id="classicMusic" class="com.learning.ClassicMusic"/> </beans>
可以看到,实例工厂方法从一个已经存在的Bean里调用工厂方法,完成Bean的创建,装配和初始化。因此,这里配置了两个Bean。第一个Bean只是一个简单的Bean,第二个Bean通过factory-bean属性指向第一个Bean。这样,Spring应用上下文就知道应该调用第一个Bean的某个工厂方法完成Bean的创建,装配和初始化。因此,第二个Bean还多了个factory-method属性用来指定一个来自第一个Bean的工厂方法名。至于其它的,就和构造函数注入的方式基本一样,这里不再赘述。
现在,运行一下程序,你会发现结果和先前的是一样的。
总结
通过本章的学习,我们知道Spring的核心组件控制反制改变了对象的创建方式,由过去的自己创建转为提供配置文件给Spring应用上下文,让Spring应用上下文根据配置文件提供的信息创建,以此降低对象之间的耦合,降低软件开发的复杂度和成本。
此外,我们还学习了两种常用的Spring应用上下文实现,ClassPathXmlApplicationContext和FileSystemXmlApplicationContext。其中,ClassPathXmlApplicationContext能够基于类路径,FileSystemXmlApplicationContext能够基于文件系统查找一个或多个XML配置文件,找到之后加载XML配置文件完成Bean的创建,装配和初始化。这样,我们需要的Bean就存在于Spring应用上下文之中,由Spring应用上下文管理着了。之后,我们可以调用Spring应用上下文的getBean方法获取和使用Bean。
最后,我们还学习了配置文件,知道<bean>元素可以用来配置Bean。也知道可以采用属性注入,构造函数注入,静态工厂方法注入,实例工厂方法注入这四种方式装配和初始化Bean。