zoukankan      html  css  js  c++  java
  • 控制反转

    正如大家熟知的那样,我们的电脑是由各种部件组成的。比如中央处理器,内存,硬盘,网卡,电源,等等。这些部件一起运转,彼此合作,由是电脑跑起来了,我们可以用她写代码,玩游戏,上网,工作,听歌,画画,等等。

    软件开发也是一样的。我们需要定义各种各样的类,每个类都有自己的功能,作用,职责。之后,我们使用这些定义好的类创建各种对象。这些对象相互合作,彼此依赖,由是软件跑起来了,能够按照我们期望的那样工作。

    由此可见,一款软件是由各种对象组成的。对象之间一定是相互合作,彼此依赖的。这种依赖就是我们通常所说的耦合。试想,当我们的软件非常庞大,由成千上万个对象组成时,这些对象通过各种关系紧紧耦合在一起,将使软件多么复杂,多么难以维护,多么容易引进错误。那么,有没有什么办法能够解耦,以求降低对象之间的耦合,进而降低软件开发的复杂度和成本呢?

    一般而言,面向接口编程可在一定程序上降低对象之间的耦合。可这远远不够。为了简化软件开发,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。

    返回目录  下一篇:两种容器 

  • 相关阅读:
    pandas 数据结构基础与转换
    Python 基础常用
    css 横向滚动条webkit-scrollbar
    hive mysql 初始化
    hive 的理解
    hive 踩坑
    hbase 调试各种报错
    hbase shell常用命令
    mysql 性能测试工具 mysqlslap
    【CDH学习之一】CDH简介
  • 原文地址:https://www.cnblogs.com/evanlin/p/12651890.html
Copyright © 2011-2022 走看看