Spring IoC控制反转思想 & DI依赖注入(五大注解+一个方法注解)

一、IoC 控制反转

IoC: Inversion of Control (控制反转), 也就是说 Spring 是⼀个"控制反转"的容器.
什么是控制反转呢? 也就是控制权反转. 什么的控制权发⽣了反转? 获得依赖对象的过程被反转了
也就是说, 当需要某个对象时, 传统开发模式中需要⾃⼰通过 new 创建对象, 现在不需要再进⾏创 建, 把创建对象的任务交给容器, 程序中只需要依赖注⼊ (Dependency Injection,DI)就可以了.
这个容器称为:IoC容器. Spring是⼀个IoC容器, 所以有时Spring 也称为Spring 容器.

二、案例 - 造⼀辆⻋

造⼀辆⻋
我们的实现思路是这样的:
先设计轮⼦(Tire),然后根据轮⼦的⼤⼩设计底盘(Bottom),接着根据底盘设计⻋⾝(Framework),最 后根据⻋⾝设计好整个汽⻋(Car)。这⾥就出现了⼀个"依赖"关系:汽⻋依赖⻋⾝,⻋⾝依赖底盘,底 盘依赖轮⼦.

2.1 传统方式 new 创建对象

package com.ameris.iocdemo;

public class Main {

    public static void main(String[] args) {
     //创建车
        Car car = new Car();
        //让车跑起来
        car.run();
    }
}
package com.ameris.iocdemo;

public class Car {
    //车要有车身,依赖于车身
    private Framework framework;

    //车初始化
    public Car() {
        //车子初始化的时候,就要有车身
        this.framework = new Framework();
        System.out.println("car init..");
    }

    public void run() {
        System.out.println("car run..");
    }
}
package com.ameris.iocdemo;

public class Framework {
    //车身依赖于 底盘
    private Bottom bottom;

    //那么在初始化车身的时候 底盘就要创建好

    public Framework() {
        this.bottom = new Bottom();
        System.out.println("framework init..");
    }
}
package com.ameris.iocdemo;

public class Bottom {
    //底盘依赖于 轮胎
    private Tire tire;

    public Bottom() {
        this.tire = new Tire();
        System.out.println("bottom init..");
    }
}
package com.ameris.iocdemo;

public class Tire {
    private int size = 17;
    public Tire() {
        System.out.println("tire init..(size:"+size+")");
    }
}

运行结果:

2.2 用代码模拟 IoC 思想

假设,轮胎的尺寸,不满足我的需求,需要修改,就会产生一系列的问题

一个代码改动,接下来其他的代码也要改,耦合性太高了

 

2.2.1 IoC 控制反转 的思想

相当于,一个制车厂造车,需要车身,底盘、车轮等。厂商不自己造零件,而是外包给别人,他不管你是怎么制造这些零件的,只要提供给厂商就行, 厂商来组装成他要的东西。

 而Spring完成的就是这里

package com.ameris.iocdemo.v2;

public class Main {
    public static void main(String[] args) {
        Tire tire = new Tire(17,"blue");
        Bottom bottom = new Bottom(tire);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
        car.run();
    }
}

package com.ameris.iocdemo.v2;

public class Car {
    //依赖于车身
    private Framework framework;
    //传统方法是 new



    //而 IoC 的方法是,直接拿 对象
    public Car(Framework framework) {
        this.framework = framework;
        System.out.println("car init...");
    }

    public void run() {
        System.out.println("car run...");
    }
}

package com.ameris.iocdemo.v2;

public class Framework {
    private Bottom bottom;

    public Framework(Bottom bottom) {
        this.bottom = bottom;
        System.out.println("framework init...");
    }
}
package com.ameris.iocdemo.v2;

public class Bottom {
    private Tire tire;

    public Bottom(Tire tire) {
        this.tire = tire;
        System.out.println("bottom init...");
    }
}

package com.ameris.iocdemo.v2;

public class Tire {
    private int size;
    private String color;

    public Tire(int size,String color) {
        this.size = size;
        System.out.print("tire init...");
        System.out.print(" size:" + size);
        System.out.print(" color:" + color);
        System.out.println();
    }
}

 在在tire这个类里加上了color属性,但是并不用去修改其他依赖于 tire 的类,只需要在main方法里加一个参数就好了。

相比传统的方式,在方法内部不停的new对象。

IoC思想,直接去要对象,在方法传参的时候就拿到这个对象,直接使用。

耦合性低了,即使做各种修改,也不会特别影响到其他的代码。

三、DI 依赖注入

3.1 属性注入

spring IoC/DI

对象的管理:

1.存对象 (把对象交给spring管理) @Component 等 五大注解和一个方法注解

2.取对象(依赖注入)  @Autowired

拿之前写的 书架系统做案例

修改一下

修改完发现,可以正常获取到书架的书籍

从这个例子中能看出来,IoC的好处

第⼀,资源集 中管理,实现资源的可配置和易管理。
第⼆,降低了使⽤资源双⽅的依赖程度,也就是我们说的耦合 度。

3.2 构造方法注入

3.2.1 没有写构造函数,spring会自动使用Java提供的无参的构造函数

package com.ameris.iocdemo.controller;

import com.ameris.iocdemo.config.UserConfig;
import com.ameris.iocdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {
    //无属性,没有写构造方法
    //自动生成无参构造方法
    public void sayHi(){
        System.out.println("hello,UserController...");

    }
}

所以运行spring时没有出错

3.2.2 写了一个构造函数 ,spring 就会使用当前这个写好的构造函数

当写了一个构造函数时,spring 就会使用当前这个写好的构造函数,

package com.ameris.iocdemo.controller;

import com.ameris.iocdemo.config.UserConfig;
import com.ameris.iocdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    private UserService userService;


    /**
     * 构造函数注入
     */

    public UserController(UserService userService) {
        this.userService = userService;
    }


    public void sayHi(){
        System.out.println("hello,UserController...");
        userService.sayHi();
    }
}

所以spring没报错

3.2.3 写了多个构造函数写,默认会使用无参的构造函数,需要使用@Autowired指定构造方法,否则会报错

//当写了多个构造函数时,写了多个构造函数,默认会使用无参的构造函数,如果没有这个无参构造函数,spring会报错
// spring 不知道使用哪一个构造函数,就会报错
package com.ameris.iocdemo.controller;

import com.ameris.iocdemo.config.UserConfig;
import com.ameris.iocdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    private UserService userService;
    private UserConfig userConfig;

    /**
     * 构造函数注入
     */

//    //当写了多个构造函数时,写了多个构造函数,默认会使用无参的构造函数,如果没有这个无参构造函数,spring会报错
//    // spring 不知道使用哪一个构造函数,就会报错
//
   // 手动写了其他构造函数之后,养成好习惯 把默认无参的构造函数也写上 ,避免其他组件报错
    public UserController() {
    }

    public UserController(UserService userService) {
        this.userService = userService;
    }

    public UserController(UserService userService, UserConfig userConfig) {
        this.userService = userService;
        this.userConfig = userConfig;
    }



    public void sayHi(){
        System.out.println("hello,UserController...");
        userService.sayHi();
        userConfig.sayHi();
    }
}

如果没有这个无参构造函数,spring会报错

加上 @Autowired 就好了

3.3  set方法注入

package com.ameris.iocdemo.controller;

import com.ameris.iocdemo.config.UserConfig;
import com.ameris.iocdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {


    /**
     * set 方法注入
     */
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void sayHi(){
        System.out.println("hello,UserController...");
        userService.sayHi();

    }
}

3.4 三个注入的区别

可能上面的几个注入,给人感觉最简单的是 属性注入,但在spring中 属性注入不被推荐。特别是专业版的idea ,属性注入时,注解下面会有黄色的小波浪线(就是不推荐使用的意思)

但是其他注入方式就不会这样

实际上各个注入各有特点

属性注⼊
优点: 简洁,使⽤⽅便;
缺点:
        ▪ 只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指 针异常)
        ▪ 不能注⼊⼀个Final修饰的属性
构造函数注⼊(Spring 4.X推荐)
优点:
        ▪ 可以注⼊final修饰的属性
        ▪ 注⼊的对象不会被修改
        ▪ 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法 是在类加载阶段就会执⾏的⽅法.
        ▪ 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的
缺点:
        ▪ 注⼊多个对象时, 代码会⽐较繁琐
Setter注⼊(Spring 3.X推荐)
优点: ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊
缺点:
        ▪ 不能注⼊⼀个Final修饰的属性
        ▪ 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险.

3.4.1 属性注入 不能使用 final,但是构造函数可以

加上 final 之后,代码立马爆红

分析原因:

final 的特点:

1)被 final 关键字修饰,需要手动初始化。

手动创建这个对象的方法是 new 实例化一个对象,这就与 @Autowired 的本意 冲突了。

        @Autowired 就是为了把交给spring管理的对象,取出来用(依赖注入),就是为了简便 new这个过程。不初始化,违背 关键字 final 的用法,初始化了和@Autowired 直接矛盾了。既然都初始化了,还 @Autowired 干嘛,多此一举。

        那直接初始化成null 可以吗?语法上没问题,没有报错。但是我既然赋值给 null ,就等于没有创建 这个对象,逻辑上 和 @Autowired 还是冲突了,给这个注解要用这个对象,赋值null,等于没有这个对象了。

2)如果不初始化,在构造方法中进行赋值

3.4.2 构造方法注入的对象不会被修改,而setter方法注入的对象可能会被修改

原因很简单,构造方法是对象的初始化,从实例化对象之后就固定了。最重要的一点是,构造方法不可能被调用。

而setter方法,本身就是一个提供给别人调用的方法,对 对象的属性值 进行修改,所以 setter方法注入的对象 有被修改的风险

四、IoC的详细用法

五大注解: @Controller , @Service ,@Component ,@Repository ,@Configuration

方法注解: @Bean

        从功能上说,除了 @Controller ,其他的效果都是一样的。因为@Controller除了具备让Spring 管理的功能外,还是接口的入口,必须为@Controller

        而之所以还需要用不同的注解,是为了通过注解来区分代码,方便管理

程序的应用分层

4.1 @Controller (控制器存储)

首先写了一个UserController类,并加上了类注解 @Controller

把这个类 交给 spring 管理 

再来看启动类 加上了@SpringBootApplication 这个注解

对启动类代码进行修改,观察spring 管理的类

package com.ameris.iocdemo;

import com.ameris.iocdemo.controller.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;


@SpringBootApplication //这个是spring的启动类注解
public class IocDemoApplication {

    public static void main(String[] args) {
        //.run方法就是执行spring,让它运行起来
        //并且这个方法是由返回值的,这个返回值是 spring上下文
        ApplicationContext context = SpringApplication.run(IocDemoApplication.class, args);
        //可以通过上下文拿到 spring 管理的对象
        UserController bean = context.getBean(UserController.class);//拿到 UserController
        bean.sayHi();//使用UserController的方法
    }

}

使用getBean从spring容器中获取对象

这里是根据 类型 获取bean

打断点调试,发现执行到这里了 

说明@Controller 真的有管理这个对象

4.2 @Service (服务存储)

这里通过 Bean 的名称去获取 Bean

成功执行代码

4.3 @Component (组件存储)

这里通过 Bean 的名字和对象的类型 来获取 Bean

4.4  @Repository(仓库存储)

4.5 @Configuration (配置存储)

 4.6 @Bean 将方法中的对象交给spring进行管理

发现有问题:没有找到UserInfo这个类型的bean

奇怪,明明在方法前面加上了 @Bean 

原因是:虽然方法加上了  @Bean ,但是这个方法存在在 BeanConfig 这个类中,而这个类没有加注释,也就是没有交给 spring 管理,所以spring 运行的时候,就没有管理整个代码。

也就不会注意到里面的方法。

于是修改代码,加上注解 @Configuration

重启spring项目,运行正常了

4.6.1 @Bean 注解是方法注解,并且 @Bean 需要与五大注解搭配使用才能生效。

另外:

注意spring 的扫描路径,是 启动类 所在的路径,注解 必须在扫描路径下 才能生效

        如果spring启动类的位置变化了,到了其他目录下,那么即时加了五大注解,其他的类也可能扫描不到。

例如:当前的启动类 在 iocdemo整个目录下,默认这个扫描这个目录下所有文件

所以,启动类写的代码,能正常执行

如果把启动类移动了,如下:

移动到controller 这个包下

发现 spring只扫描了 controller这个目录的类,其他的都没扫描到

但是如果偏要放在这里,还想要能扫描到其他的类

可以在启动类上面加上@ComponentScan(""),指定spring的扫描路径

4.6.2 @Bean 注解定义的对象,默认名称为 方法名(无论方法名大小写)

一个类下,可能有多个 @Bean,就需要用 Bean名称+类型 去获取了,否则就会异常

报错

修改获取 Bean 的方式

获取成功

4.6.3 @Bean("")自定义 Bean的名称

还可以命名多个bean名称

都能正确获取到

五、@Autowired 存在的问题

1)@Autowired 先通过类型查找bean,没有获取到,抛异常。获取到,如果只有一个bean,正常响应。如果匹配到多个对象时,会查看是否配置了@Qualifier,再通过名称获取 Bean,获取到了就响应,否则报错。

userInfo里使用了两个 @Bean

在 usercontroller2 注入UserInfo,然后调用 UserInfo 的sayHi方法

可以发现,userInfo 代码下面标红了(说明存在问题)

其实在这里就已经提示了问题所在(有多个bean)

启动spring,发现启动就失败了

查看下面的描述:

这个UserInfo类型只需要一个 @Bean ,但是发现了两个 (验证了上面爆红的代码)

spring不知道要使用的是哪个对象

现在 通过修改变量名 为 bean的名字 来取对象

的确是正常取出来了

但是存在很大的隐患

通常:不使用变量名称 来指定 获取某个 bean,而是通过其他的手段来指定bean的名称

        通常情况下,变量 的名称是随便取的,而在程序员看来变量名的修改不影响业务的逻辑处理。

        在团队协作中,变量名很容易被认为只是一个普通的变量名。而实际上,如果这个变量名,是指的 bean。此时有人将变量名修改,整个项目都有可能无法正常启动。 

解决方法: 

1)@Primary

这个注解使用不多

在@Bean 注解注释的方法的位置,写上@Primary,代表 默认@Autowired 获取到的是 该方法下 产生这个对象

加上这个注解后,也没有报红线了

获取到了 userinfo2这个对象

2)@Qulifier ("Bean的名称")

常用       

        在@Autowired 取对象的(注入依赖)位置,注释 @Qulifier ,并在括号里写上要获取的Bean的名称

        即使,被注入依赖的对象的类的中已经有其他方法 被注解了@Primary,但此时 依然以 @Qulifier 为主

3)@Resource (jdk提供)

常用

@Resource (name ="bean的名称")等于 

@Qualifier("bean的名称") + @Autowired

 2)@Autowired 和 @Resource 的区别

1)@Autowired 由spring框架提供

     @Resource 由JDK框架提供

2)@Autowired 先通过名称获取 Bean

        获取到了,正确响应;

        没有获取到,根据类型匹配,如果匹配到多个对象时,@Autowired 取对象时,报错

      @Resource 直接通过 名称获取。

3)@Autowired 没有参数设置

        @Resource 有一些参数设置

(例如:常见参数name ,使用比较多的 @Resource (name ="bean的名称"))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值