依赖注入学习笔记

Ioc概述

Ioc(Inversion Of Control)翻译为控制反转

首先明确没有反转的控制为"主动控制"

  1. 主动控制:我们在之前编写的所有代码都是主动控制,意思是程序运行过程中需要什么对象就实例化什么对象
  2. 控制反转:使用控制反转思想编写的代码是将所有程序会使用到的对象都保存到外部容器,需要时从容器中获取

创建Spring项目

创建Spring项目

在这里插入图片描述

使用Idea创建Maven项目然后添加Spring的依赖即可

修改之后当前项目的pom.xml文件如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>SpringTest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <!-- 设置 JDK 版本为 1.8 -->
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
        <!-- 设置编码为 UTF-8 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    </properties>
    <dependencies>
        <!-- Spring Context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
</project>

下面开始创建控制反转的HelloWorld程序

控制反转的第一个程序

我们先创建一个包含学生属性的类

public class Stu {
    
    private Integer id;
    private String name;
    
    // get\set\toString 略
}

下面我们要学习怎么将一个对象保存在Spring容器中以实现控制反转

新建一个类Config

// 这个注解标记后表示当前类是用于配置Spring的配置类
@Configuration
public class Config {

    // 这个配置类中,可以将对象保存到Spring容器
    // 这个@Bean注解表示将下面方法的返回值保存到Spring容器中
    // 这个对象在Spring容器中的名称就是这个方法的名称
    @Bean
    public Stu stu(){
        Stu stu=new Stu();
        stu.setId(1);
        stu.setName("孙悟空");
        return stu;
    }
}

在main方法中使用Spring容器中的对象

public class DemoTest {

    public static void main(String[] args) {
        // 初始化Spring容器
        AnnotationConfigApplicationContext acac=
          new AnnotationConfigApplicationContext(Config.class);
        // 获得Spring 容器中保存的对象
        //  getBean([对象的名称\id],[对象的类型.class])
        Stu stu=acac.getBean("stu",Stu.class);
        System.out.println(stu);

    }

}

使用@Bean注解来保存对象到Spring容器是Spring框架提供的主要方法之一

JUnit测试运行Spring

如果用main方法进行程序的测试,每次测试都要新建一个类编写main方法

类会比较多,而且每次main方法中都有acac对象的实例化和关闭,代码有冗余

我们使用JUnit进行单元测试就可以改善上面的缺点

首先添加JUnit4的依赖

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
</dependency>

在test的java文件夹下创建demo包

包中创建TestDemo类,测试代码如下

public class TestDemo {

    // 一个测试类中可以编写多个可以运行的测试方法

    @Test
    public void run1(){
        System.out.println("run1");
    }

    @Test
    public void run2(){
        System.out.println("run2");
    }

    // 声明ACAC对象,当做测试类的属性,能够在所有方法中使用
    AnnotationConfigApplicationContext acac;

    // @Before注解标记的方法会在当前测试类@Test方法运行之前自动运行
    @Before
    public void init(){
        acac=new AnnotationConfigApplicationContext(Config.class);
    }

    // @After注解标记的方法会在当前测试类@Test方法运行之后自动运行
    @After
    public void destroy(){
        acac.close();
    }

    @Test
    public void getStu(){
        Stu stu=acac.getBean("stu",Stu.class);
        System.out.println(stu);
    }
    
}

组件扫描保存对象到Spring容器

上面我们使用的是@Bean注解的方法将对象保存到Spring容器

下面我们学习另一中方法将对象保存到Spring容器

新建一个包san

包中新建一个类GuanYu

代码如下

// @Component就是组件扫描保存到Spring容器的关键注解之一
// Component是组件的意思,当前类被扫描时,会自动实例化一个对象,
// 保存到Spring容器,对象的id(名称)为当前类名首字母小写的单词guanYu

@Component
public class GuanYu {
    private String name="关羽";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void fight(){
        System.out.println(name+"在战斗!");
    }

}

组件扫描也离不开Config类

创建ConfigSan类代码如下

@Configuration
// 组件扫描(ComponentScan)的注解
// 这个注解需要指定一个包,实例化ACAC对象解析这个配置文件时ConfigSan.class时
// 会扫描这个指定的包中的所有类,只要这些类中有标记为@Component注解的,
// 都会实例化对象保存到Spring容器
@ComponentScan("san")
public class ConfigSan {
}

测试获得组件扫描方式保存到Spring容器中的对象

public class TestSan {

    AnnotationConfigApplicationContext acac;

    @Before
    public void init(){
        acac=new AnnotationConfigApplicationContext(
                                         ConfigSan.class);
    }
    @After
    public void destroy(){
        acac.close();
    }
    @Test
    public void getGuan(){
        GuanYu guanYu=acac.getBean("guanYu",GuanYu.class);
        guanYu.fight();
    }
}

组件扫描和@Bean方式的区别

1.组件扫描配置更简单

2.@Bean是可以为对象属性赋值之后再保存到Spring容器的

3.@Bean可以配置多个不同的对象保存到Spring容器(例如Worker的w1和w2)

组件扫描注意事项

1.特殊的类名导致特殊的名称保存到Spring容器

​ 正常情况下

​ Stu -> stu

​ Worker -> worker

​ GuanYu -> guanYu

​ 这样的规律在类名特殊时会不同

​ 当连续的两个大写字母开头的类名

​ VIPStu -> VIPStu

​ 保存到Spring容器的对象名称就是类名

2.Spring框架支持多种注解表示组件的含义

Spring提供了下面注解和@Component有相同作用

  • @Controller 用于标记控制层组件
  • @Service 用于标记业务逻辑层组件
  • @Repository 用于标记数据访问层组件

上面三个注解也都可以表示将当前类实例化后保存到Spring容器

只是由于单词的含义不同,在开发看到时,方便知道它的作用,增加代码可读性

3.自定义组件id(名称)

@Component("erye")

public class GuanYu {
    //.....  代码略
}

如果程序中需要将当前组件扫描保存的对象自定义名称

就需要在@Component(或相同功能的注解后)编写(),其中使用字符串来自定义当前对象保存到Spring容器的名称

Bean(对象)的作用域

这里的Bean指的就是Spring容器中的对象

作用域英文:Scope

作用域用来设置当前Spring容器中对象的管理策略

这个管理策略主要有两种

1.singleton(单例)

2.prototype(原型)

单例策略

单例策略是Spring框架的默认作用域策略

上面章节中Stu\Work\GuanYu类保存到Spring容器都是单例的

下面我们在已有的测试类中进行测试,验证"单例"的特性

首先既然是"单例",表示从测试类中获得的对象无论获取多少次都是同一个对象

@Test
public void single(){
    // 用于测试单例的代码
    Stu s1=acac.getBean("stu",Stu.class);
    Stu s2=acac.getBean("stu",Stu.class);

    System.out.println(s1==s2);

    s1.setName("齐天大圣");
    System.out.println(s2);

}

代码中先编写了它们的判等代码,输出结果为true

表示他们是同一个引用

下面s1对象修改了name属性的值

输出s2,s2对象的name属性也随s1的修改而变化了,又一次印证了它们是同一个对象

单例的优势

1.节省内存

2.方便管理,对当前对象进行修改,影响所有对象的属性

3.在需要获得多个对象时,不能使用单例,否则无法达到目的

原型策略

下面我们来介绍一下原型策略的使用

我们可以在@Bean注解下或@Component注解下编写

@Scope("prototype")

做完上面的配置在运行下面的测试代码

@Test
public void prototype(){
    // 测试原型作用域
    // 每从Spring容器中获得一次对象,都将获得一个新实例化的对象
    Stu s3=acac.getBean("stu",Stu.class);
    Stu s4=acac.getBean("stu",Stu.class);

    System.out.println(s3==s4);

    s3.setName("孙行者");
    System.out.println(s3);
    System.out.println(s4);
}

会发现s3==s4为false

修改了s3的属性,只影响s3对象自己,不会影响s4,这些现象和上面单例完全不同

也就是说在原型作用域下,每次从Spring容器中获得对象都会获得新实例化的对象

原型策略优势

1.程序中需要使用一个模板生成多个对象时,就是用原型模式

2.但是原型策略会非常浪费内存空间

在没有必要的情况下推荐使用单例,当确实需要一个类型的多个对象时使用原型

惰性初始化

惰性初始化又称"懒加载"

什么是惰性初始化

所谓惰性初始化就是在程序需要使用这个对象时,Spring容器在实例化它

如果程序运行过程中没有需要它,Spring就不实例化它了

为什么需要惰性初始化

在程序运行过程中,有些对象本来就是可能使用到,可能使用不到的

针对这种对象,我们设置为惰性初始化可能减少内存的占用

Spring容器中默认设置并不是惰性的,也就说默认情况下,所有单例作用域的对象都会在程序运行前就实例化好,除非设置了惰性初始化,也就是惰性初始化是针对单例作用对象的

惰性初始化也不是没有缺点的,如果程序中所有对象都设置为惰性初始化,也就是在运行过程中,所有对象都需要先实例化在使用,那么会明显拖慢运行速度

默认情况下的对象实例化时机

Stu的配置类Config恢复成初始的单例作用域

在Stu类中添加了一个无参构造方法

public Stu(){
    System.out.println("Stu实例化");
}

在TestDemo测试类的@Before方法中添加了输出

@Before
public void init(){
    acac=new AnnotationConfigApplicationContext(Config.class);
    System.out.println("init方法执行完毕");
}

然后运行一个空的测试方法

@Test
public void lazy(){

}

控制台输出为

Stu实例化
init方法执行完毕

证明acac对象实例化时就将Stu对象进行实例化了

如果想设置Stu为惰性初始化对象就在@Bean或@Component注解添加

@Lazy

注解

再运行空的测试类中的lazy方法控制台输出如下

init方法执行完毕

证明这次运行实例化acac时没有实例化Stu对象

只有测试类中真正使用到Stu对象时,才会进行实例化

@Test
public void lazy(){

    Stu ss=acac.getBean("stu", Stu.class);
    System.out.println(ss);
    //Stu sss=acac.getBean("stu",Stu.class);
}

运行上面代码输入如下

init方法执行完毕
Stu实例化
Stu{id=1, name='孙悟空'}

即使将Stu sss的注释解除也不会再次实例化Stu对象了!

DI 依赖注入

什么是依赖注入

依赖注入(Dependency Injection)

要想明确依赖注入,我们先了解什么是依赖

程序中的依赖指:A类中的方法需要使用B类型对象才能运行,我们就说A类依赖B类

他们是依赖的关系

关羽要战斗需要依赖青龙偃月刀

这样的依赖在程序中要怎么表示呢?

san包中创建DragonBlade类代码如下

@Component
public class DragonBlade {

    private String name="青龙偃月刀";

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}

san包的GuanYu类修改一下

@Component
public class GuanYu {

    private String name="关羽";

    private DragonBlade dragonBlade;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public DragonBlade getDragonBlade() {
        return dragonBlade;
    }

    public void setDragonBlade(DragonBlade dragonBlade) {
        this.dragonBlade = dragonBlade;
    }

    public void fight(){
        System.out.println(name+"使用"+dragonBlade+"战斗!");
    }

}

下面进行测试

要想关羽拿着青龙偃月刀战斗,测试时必须从Spring容器中获得关羽和青龙偃月刀

再设置他们的依赖关系,才能正常运行代码如下

@Test
public void getGuan(){
    DragonBlade blade=acac.getBean(
            "dragonBlade",DragonBlade.class);
    GuanYu guanYu=acac.getBean("guanYu",GuanYu.class);
    guanYu.setDragonBlade(blade);
    guanYu.fight();
}

上面代码的问题

我们自己获得青龙偃月刀,自己获得关羽,还有自己设置它们的依赖关系

这么比较复杂,尤其是依赖对象越来越多时

我们希望在Spring内部就将这些复杂的依赖关系设置好

我们直接获得设置好依赖对象的关羽直接使用即可

这就是依赖注入

所谓依赖注入:在Spring容器内部将对象的依赖关系设置成功的过程

依赖注入(DI)

上次课我们已经讲解了依赖的概念和依赖注入的定义

依赖注入就是将有依赖关系的对象在Spring容器内部设置好依赖关系的操作

依赖注入的实现

和保存到Spring容器一样

实现依赖注入也可以使用@Bean的方式和组件扫描的方式两种

@Bean实现依赖注入

首先创建一个bean包,用于编写代码

包中创建一个SkyLance类

public class SkyLance {

    private String name="方天画戟";
	
    // get\set略
    @Override
    public String toString() {
        return name;
    }
}

再创建一个LvBu类

public class LvBu {

    private String name="吕布";

    private SkyLance skyLance;

    // get\set 略

    public void fight(){
        System.out.println(name+"使用"+skyLance+"战斗");
    }
}

最后在创建一个ConfigBean类,类中使用@Bean实现依赖注入

@Configuration
public class ConfigBean {

    @Bean
    public SkyLance skyLance(){
        return new SkyLance();
    }

    // 在下面方法的参数列表中,我们使用参数表示当前方法需要的类型
    // Spring会自动从Spring容器中选取合适的对象将参数赋值
    // 也就是说这个方法运行时,参数l就是上面保存到Spring容器的SkyLance对象
    @Bean
    public LvBu lvBu(SkyLance l){
        LvBu lb=new LvBu();
        lb.setSkyLance(l);
        return lb;
    }
}

在测试文件夹中创建bean包进行测试

代码如下

@Test
public void bean(){
    //获得吕布
    LvBu lb=acac.getBean("lvBu",LvBu.class);
    lb.fight();
}

现在能够测试成功是因为当前Spring容器中只有唯一的一把方天画戟

如果Spring容器中没有方天画戟对象就会报错

另外如果Spring容器中有两把以上的方天画戟,在我们保存吕布对象的方法参数名称必须和启动一把方天画戟的id匹配,否则运行也报错

配置代码如下

@Configuration
public class ConfigBean {

    @Bean
    public SkyLance skyLance(){
        return new SkyLance();
    }
    @Bean
    public SkyLance lance(){
        return new SkyLance();
    }

    // 下面方法的参数名称必须和上面保存到Spring容器对象的id之一一致
    @Bean
    public LvBu lvBu(SkyLance lance){
        LvBu lb=new LvBu();
        lb.setSkyLance(lance);
        return lb;
    }
}

组件扫描实现依赖注入

我们可以使用张飞和丈八蛇矛的例子来实现组件扫描的依赖注入

先创建comp包,包中创建丈八蛇矛类

@Component
public class SnakeLance {
    private String name="丈八蛇矛";

   	//  .....
    @Override
    public String toString() {
        return name;
    }
}

需要注意@Component注解的编写

再创建张飞类

@Component
public class ZhangFei {

    private String name="张翼德";

    // 类的属性(成员变量)上添加@Autowired注解
    // 表示当前属性会自动从Spring容器中获得相应对象
    @Autowired
    private SnakeLance snakeLance;

    //...

    public void fight(){
        System.out.println(name+"使用"+snakeLance+"战斗");
    }


}

配置类ConfigComp

@Configuration
@ComponentScan("comp")
public class ConfigComp {
}

现在这个状态就可以测试类

创建测试类TestComp

@Test
public void comp(){
    // 获得张飞
    ZhangFei zhangFei=acac.getBean("zhangFei",
                                    ZhangFei.class);
    zhangFei.fight();
}

如果Spring容器中有多个丈八蛇矛类型的对象

例如配置类中新增了@Bean保存了一个丈八蛇矛对象到Spring容器

@Configuration
@ComponentScan("comp")
public class ConfigComp {
    @Bean
    public SnakeLance lance(){
        return new SnakeLance();
    }

}

此时,@Autowired注解标记的成员变量的属性名默认情况下会作为选择对象的依据,会使用id和成员变量名称一致的对象来注入

但是如果id和成员变量名称没有一致的就注入失败,运行报错

这时如果修改成员变量名称会引起一些列的维护,不方便

推荐大家使用@Qualifier注解解决

代码如下

// 类的属性(成员变量)上添加@Autowired注解
// 表示当前属性会自动从Spring容器中获得相应对象
// 如果spring容器中有两个以上的该类型对象,
// 它会自动匹配id和属性名一致的对象
@Autowired
// @Qualifier注解可以指定一个名称,这个名称会匹配Spring容器中的id
// 将匹配的对象赋值给下面的属性,
// 一般用于一个类型Spring容器中多个对象时使用
@Qualifier("lance")
private SnakeLance snakeLance;

IOC\DI解耦

什么是解耦

解耦指:解开耦合

要想理解解开耦合要先知道什么是耦合

所谓耦合:就是当前程序更换组件\对象\零件的难易程度

如果更换对象带来的修改少,也就是容易更换,我们称之为松耦合

如果跟换对象带来的修改多,也就是难易更换,我们称之为紧耦合

程序中,松耦合的程序更容易应对变化,更好扩展与维护,所以我们的程序都追求松耦合

回到主题所谓解耦,就是降低程序的耦合性,使紧耦合的程序变化为松耦合的程序的过程

我们上面编写的程序都是紧耦合的,举例,关羽要战斗只能依赖青龙偃月刀,如果现在想要进行进行解耦,必须将关羽和青龙偃月刀之间的依赖关系进行修改,

解耦的操作将依赖的具体类型修改为一个接口类型

java程序中接口\抽象类都不属于"具体类型",他属性"抽象类型"

新建个包ou

包中新建接口Weapon

代码如下

// 武器接口,关羽类依赖这个接口类型来实现解耦
public interface Weapon {
    
    String getName();
}

然后创建青龙偃月刀和丈八蛇矛类

@Component
public class DragonBlade implements Weapon {

    private String name="青龙偃月刀";

  	//...

    @Override
    public String toString() {
        return name;
    }
}
@Component
public class SnakeLance implements Weapon{

    private String name="丈八蛇矛";

   //...

    @Override
    public String toString() {
        return name;
    }
}

创建关羽类

@Component
public class GuanYu {

    private String name="关云长";

    @Autowired
    // 之前依赖的是具体类型,现在依赖这个类型的接口,以实现解耦
    @Qualifier("snakeLance")
    private Weapon weapon;

    public void fight(){
        System.out.println(name+"使用"+weapon+"战斗");
    }
	//...
}

配置类

@Configuration
@ComponentScan("ou")
public class ConfigOu {
}

最后在java测试包中

@Test
public void run(){
    // 获得关羽
    GuanYu guanYu=acac.getBean("guanYu",
                                        GuanYu.class);
    guanYu.fight();

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值