【JavaEE进阶】——一万字带你深刻理解Spring IoC&DI

目录

🚩Spring是什么

🎈什么是容器?

🎈什么是 IoC?

📝传统开发思路

📝IOC思想

📝IoC 优势

🎈DI 介绍

🚩IoC 详解

🎈Bean的存储

🔴五大类注解

📝@Controller(控制器存储)

👩🏻‍💻获取bean对象的方式

📝@Service(服务存储)

📝@Repository(仓库存储)

📝@Component(组件存储)

📝@Configuration(配置存储)

🍭类注解之间的关系

🔴⽅法注解 @Bean

🔴Bean传递参数

🎈SpringBoot特点

🚩DI 详解

🎈属性注⼊

🎈构造方法注入

🎈Setter注入

🔴@Autowired存在问题

📝解决方法一 属性名和bean名称相同

📝解决方法二 @Primary

📝解决方法三 @Qualifier

📝解决方法四 @Resource

🔴五大注解修改名以及Bean修改名

📝Bean修改名

📝类注解修改


我们学习了springBoot和spring mvc的开发,完成了基本的功能开发了,但是真正的spring,spring mvc以及spring boot之间又有什么关系呢?


🚩Spring是什么

spring是一个开源框架,让我们的开发更加的简单,支持广泛的应用场景,有着活跃而庞大的社区,这可能就是spring一直存在的原因吧。

我们⽤⼀句更具体的话来概括Spring, 那就是: Spring 是包含了众多⼯具⽅法的 IoC 容器
什么是容器?IOC是什么?

🎈什么是容器?

容器是⽤来容纳某种物品的(基本)装置。⸺来⾃:百度百科
⽣活中的⽔杯, 垃圾桶, 冰箱等等这些都是容器.
我们想想,之前课程我们接触的容器有哪些?
List/Map -> 数据存储容器
Tomcat -> Web 容器

🎈什么是 IoC?

IOC是Spring的核心思想,什么是IOC呢?IOC是一种思想,之前我们在学习spring MVC 中,我们在类上添加@RestController 和 @Controller 注解,就是把这个类对象交给spring管理,spring框架启动时就会加载该类,把对象交给spring管理,就是IOC思想。

IoC: Inversion of Control (控制反转), 也就是说 Spring 是⼀个"控制反转"的容器
  • 什么是控制反转?——控制权反转
  • 控制权反转具体是体现在哪里呢?

获得依赖对象的过程中被反转了,也就是说,当需要某个对象时 ,传统开发模式都是自己new创建对象,现在不需要自己去创建对象了,把创建的对象的任务交给容器(也就是spring容器),程序中只需要注入(Dependency Injection,DI)就可以了.

  • spring是根据什么创建对象的呢?

之前我们在学习spring MVC 中,我们在类上添加@RestController 和 @Controller 注解,就是把这个类对象交给spring管理。——通过注解


IOC思想(在我的理解):通过注解对该对象进行了控制,将其存入到spring容器中,让spring对这个对象进行管理和创建。

相信大家看完上述的描述,对IOC思想了解了很多。下面我们通过一个案例进行加深理解。


📝传统开发思路

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

我们首先在newCarExample中创建car,然后在car类中创建Framework,然后根据车身创建底盘,然后进而创建轮胎。

public class NewCarExample {
 public static void main(String[] args) {
 Car car = new Car();
 car.run();
 }
 /**
 * 汽⻋对象
 */
 static class Car {
     private Framework framework;

    public Car() {
     framework = new Framework();
     System.out.println("Car init....");
    }
     public void run(){
         System.out.println("Car run...");
     }
 }

/**
 * ⻋⾝类
 */
 static class Framework {
 private Bottom bottom;
 public Framework() {
 bottom = new Bottom();
 System.out.println("Framework init...");
 }
 }

/**
 * 底盘类
 */
 static class Bottom 
{
 private Tire tire;
 public Bottom() {
 this.tire = new Tire();
 System.out.println("Bottom init...");
 }
}

/**
 * 轮胎类
 */
 static class Tire {
 // 尺⼨
 private int size;
 public Tire(){
 this.size = 17;
 System.out.println("轮胎尺⼨:" + size);
 }
 }
}
设计问题:
这样的设计看起来没问题,但是可维护性却很低.
接下来需求有了变更: 随着对的⻋的需求量越来越⼤, 个性化需求也会越来越多,我们需要加⼯多种尺⼨的轮胎.
那这个时候就要对上⾯的程序进⾏修改了,修改后的代码如下所⽰:
我们要加上轮胎大小属性,我们要给size传到底盘,然后底盘传到车身,最后传到车子。size这个属性增加了程序的耦合度,让每个对象都要对size进行添加。
从以上代码可以看出,以上程序的问题是:当最底层代码改动之后,整个调⽤链上的所有代码都需要修改.
程序的耦合度⾮常⾼(修改⼀处代码, 影响其他处的代码修改)

📝IOC思想

解决方案:
在上⾯的程序中, 我们是根据轮⼦的尺⼨设计的底盘,轮⼦的尺⼨⼀改,底盘的设计就得修改. 同样因为我们是根据底盘设计的⻋⾝,那么⻋⾝也得改,同理汽⻋设计也得改, 也就是整个设计⼏乎都得改。
我们尝试换⼀种思路, 我们先设计汽⻋的⼤概样⼦,然后根据汽⻋的样⼦来设计⻋⾝,根据⻋⾝来设计底盘,最后根据底盘来设计轮⼦. 这时候,依赖关系就倒置过来了:轮⼦依赖底盘, 底盘依赖⻋⾝, ⻋⾝依赖汽⻋。
我们可以尝试不在每个类中⾃⼰创建下级类,如果⾃⼰创建下级类就会出现当下级类发⽣改变操作,⾃⼰也要跟着修改. 此时, 我们只需要将原来由⾃⼰创建的下级类,改为传递的⽅式(也就是注⼊的⽅式) ,因为我们不 需要在当前类中创建下级类了,所以下级类即使发⽣变化(创建或减少参数),当前类本⾝也⽆需修 改任何代码,这样就完成了程序的解耦.
我们把创建子类的方式,改成注入传递的方式。
public class newCarExample {
    public static void main(String[] args) {
        Tire tire=new Tire(20);
       Bottom bottom=new Bottom(tire);
       Framwork framwork=new Framwork(bottom);
       Car car=new Car(framwork);
       car.run();
    }
}


public class Tire {
    private int size;
    public Tire(int size){
        System.out.println("轮胎的尺寸:"+size);
    }
}


//轮胎依赖底盘
public class Bottom {
    private Tire tire;
    public Bottom(Tire tire){
        this.tire=tire;
        System.out.println("bottom init ..........");
    }
}


//底盘依赖机身
public class Framwork {
    private  Bottom bottom;
    public Framwork(Bottom bottom){
        this.bottom=bottom;
        System.out.println("framwork init....");
    }
}


//机身依赖车子
public class Car {
    private Framwork framwork;
    public Car(Framwork framwork){
        this.framwork=framwork;
        System.out.println("car init....");
    }
    public void run(){
        System.out.println("car run....");
    }
}

代码经过以上调整,⽆论底层类如何变化,整个调⽤链是不⽤做任何改变的,这样就完成了代码之间的解耦,从⽽实现了更加灵活、通⽤的程序设计了

📝IoC 优势

在传统的代码中对象创建顺序是:Car -> Framework -> Bottom -> Tire
改进之后解耦的代码的对象创建顺序是:Tire -> Bottom -> Framework -> Car

我们发现了⼀个规律,通⽤程序的实现代码,类的创建顺序是反的,传统代码是 Car 控制并创建了 Framework,Framework 创建并创建了 Bottom,依次往下,⽽改进之后的控制权发⽣的反转,不再 是使⽤⽅对象创建并控制依赖对象了,⽽是把 依赖对象注⼊将当前对象中,依赖对象的控制权不再由 当前类控制了。而是由newCarExample总体进行创建(也就是Spring容器)
传统上我们都在当前类进行new对象,现在我们只需要将属性注入,创建对象交给容器管理创建,这就是IOC思想。


🎈DI 介绍

DI: Dependency Injection(依赖注⼊)
容器在运⾏期间, 动态的为应⽤程序提供运⾏时所依赖的资源,称之为依赖注⼊。
程序运⾏时需要某个资源,此时容器就为其提供这个资源.
从这点来看, 依赖注⼊(DI)和控制反转(IoC)是从不同的⻆度的描述的同⼀件事情,就是指通过 引⼊ IoC 容器,利⽤依赖关系注⼊的⽅式,实现对象之间的解耦。
上述代码中, 是通过构造函数的⽅式, 把依赖对象注⼊到需要使⽤的对象中的.。
IoC 是⼀种思想,也是"⽬标", ⽽思想只是⼀种指导原则,最终还是要有可⾏的落地⽅案,⽽ DI 就属于具体的实现。所以也可以说, DI 是IoC的⼀种实现.
我们不用自己创建该依赖对象,我们只需要将依赖对象注入到所需要的对象中,创建对象的事情交给spring来管理。
⽐如说我今天⼼情⽐较好,吃⼀顿好的犒劳犒劳⾃⼰,那么"吃⼀顿好的"是思想和⽬标(是
IoC),但最后我是吃海底捞还是杨国福?这就是具体的实现,就是 DI。

对IOC和DI有了初步的了解,我们接下来就要学习spring IOC和DI。

既然 Spring 是⼀个 IoC(控制反转)容器,作为容器, 那么它就具备两个最基础的功能:
• 存
• 取
Spring 容器 管理的主要是对象, 这些对象, 我们称之为"Bean". 我们把这些对象交由Spring管理, 由 Spring来负责对象的创建和销毁. 我们程序只需要告诉Spring, 哪些需要存, 以及如何从Spring中取出 对象。

🚩IoC 详解

通过上⾯的案例, 我们已经知道了Spring IoC 和DI的基本操作, 接下来我们来系统的学习Spring IoC和DI的操作. 前⾯我们提到 IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。 也就是bean的存储.

🎈Bean的存储

在之前的⼊⻔案例中,要把某个对象交给IOC容器管理,需要在类上添加⼀个注解: @Component
⽽Spring框架为了更好的服务web应⽤程序, 提供了更丰富的注解.
共有两类注解类型可以实现:
  • 1. 类注解:@Controller、@Service、@Repository、@Component、@Configuration.
  • 2. ⽅法注解:@Bean

🔴五大类注解

📝@Controller(控制器存储)
使⽤ @Controller 存储 bean 的代码如下所⽰:
@Controller
public class TestController {
   public void doController(){
       System.out.println("do controller....");
   }
}
如何观察这个对象已经存在Spring容器当中了呢? 接下来我们学习如何从Spring容器中获取对象
ApplicationContext 翻译过来就是: Spring 上下⽂
因为对象都交给 Spring 管理了,所以获取对象要从 Spring 中获取,那么就得先得到 Spring 的上下⽂。
观察运⾏结果, 发现成功从Spring中获取到Controller对象, 并执⾏Controller的doController⽅法

为什么TestController对象存入到spring中呢?@Controller注解的作用是什么呢?

NoSuchBeanDefinitionException表示没有注解,那么spring就获取不了该对象bean。所以我们知道了@Controller这个注解就是为了让我们获得该Bean对象存储到spring中,后续我们在主程序中getBean就获取到该对象了,所以我们上面也说了,注解的作用就是让程序运行时,控制这个注解对应的类对象。后续对这个对象进行创建和获取。


👩🏻‍💻获取bean对象的方式

⽐如
  • 类名: UserController, Bean的名称为: userController
  • 类名: AccountManager, Bean的名称为: accountManager
  • 类名: AccountService, Bean的名称为: accountService
也有⼀些特殊情况, 当有多个字符并且第⼀个和第⼆个字符都是⼤写时, 将保留原始的⼤⼩写. 这些规则 与java.beans.Introspector.decapitalize (Spring在这⾥使⽤的)定义的规则相同.
⽐如
  • 类名: UController, Bean的名称为: UController
  • 类名: AManager, Bean的名称为: AManage
根据这个命名规则, 我们来获取Bean

1》根据类型来获取

2》根据名称来获取

根据名称获取的时候,首字母小写,我们看到类名时TestController,不是前两个字母大写,那么就将大写字母转成小写字母即可。并且类型要转换。

2》根据类型和名称来获取


📝@Service(服务存储)
@Service
public class TestService {
    public void doService(){
        System.out.println("do service.....");
    }
}
public class IocDemoApplication {
    public static void main(String[] args) {
        ApplicationContext context=SpringApplication.run(IocDemoApplication.class);
        TestService testService=context.getBean(TestService.class);
        testService.doService();
    }
}


📝@Repository(仓库存储)
📝@Component(组件存储)
📝@Configuration(配置存储)

代码形式都是和上述的两个方式相同


  • @Controller:控制层, 接收请求, 对请求进⾏处理, 并进⾏响应.
  • @Servie:业务逻辑层, 处理具体的业务逻辑.
  • @Repository:数据访问层,也称为持久层. 负责数据访问操作
  • @Configuration:配置层. 处理项⽬中的⼀些配置信息

🍭类注解之间的关系
类注解之间的关系
查看 @Controller / @Service / @Repository / @Configuration 等注解的源码发现:
其实这些注解⾥⾯都有⼀个注解 @Component ,说明 它们本⾝就是属于 @Component 的"⼦类". @Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service , @Repository 等. 这些注解被称为 @Component 的衍⽣注解。
@Controller , @Service @Repository ⽤于更具体的⽤例(分别在控制层, 业务逻辑层, 持
久化层), 在开发过程中, 如果你要在业务逻辑层使⽤ @Component 或@Service,显然@Service是更好的选择。
⽐如杯⼦有喝⽔杯, 刷⽛杯等, 但是我们更倾向于在⽇常喝⽔时使⽤⽔杯, 洗漱时使⽤刷⽛杯。不同的场景运用不同的功能,我们在实际开发中,我们实现业务的时候,我们会更倾向于用@Service。

🔴⽅法注解 @Bean

为什么需要方法注解呢?如何我们使用内部包的其他类或者外部包的其他类,我们就无法添加类注解的,还有一种是一个类中需要多个对象,这些场景,都是需要使用方法注解@Bean.
类注解是添加到某个类上的, 但是存在两个问题:
  • 1. 使⽤外部包⾥的类, 没办法添加类注解
@Data
public class UserInfo {
    private Integer age;
    private String name;
    private Integer id;
}
@Configuration
public class BeanConfigTest {
    @Bean
    public UserInfo userInfo(){
        UserInfo userInfo=new UserInfo();
        userInfo.setName("张老师");
        userInfo.setAge(20);
        userInfo.setId(1);
        return userInfo;
    }
}

在 Spring 框架的设计中, ⽅法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中

  • 2. ⼀个类, 需要多个对象, ⽐如多个数据源
@Configuration
public class BeanConfigTest {
    @Bean
    public UserInfo userInfo(){
        UserInfo userInfo=new UserInfo();
        userInfo.setName("张老师");
        userInfo.setAge(20);
        userInfo.setId(1);
        return userInfo;
    }
    
    @Bean
    public UserInfo userInfo2(){
        UserInfo userInfo=new UserInfo();
        userInfo.setName("chlorine");
        userInfo.setAge(19);
        userInfo.setId(2);
        return userInfo;
    }
}

我们观察上述代码,是同一个类型,存在多个Bean,我们此时是无法通过类型来获取bean。程序会启动失败。

同一个类型,存在多个Bean,我们此时可以通过名称或者(名称+类型)来获取Bean


🔴Bean传递参数

定义了一个叫name2的String类型的对象,定义了一个叫name的String类型的对象。

我们根据名称去拿,如果对应的对象只有一个时,就直接赋值,如果有多个时,通过名称去匹配,如果名称都不匹配,按照内部逻辑来匹配。


🎈SpringBoot特点

Q: 使⽤前⾯学习的四个注解声明的bean,⼀定会⽣效吗?
A: 不⼀定(原因:bean想要⽣效,还需要被Spring扫描)

约定大于配置

比如我们上课,教务处在开学前都会给给定的课表都约定好,等到开学后学生按照课程就直接去哪个教室上课就行了,而配置就是临时老师说调课,就需要老师临时通知,但是肯定是学校提前弄好的课程进行更多,而少数是进行老师进行调课。

springboot也有一个自己的约定,其中之一体现就是:扫描路径。默认的扫描路径是:启动类所在的目录及其子孙目录。

我们创建下项目默认的启动类都是在最大的包中,如果我们调动启动类的位置。将其移到controller包下,我们现在执行Configuration包下的程序,看是否成功。

报错,显示无法找到UserInfo,我们上面说Springboot约定大于配置,约定默认扫描路径是启动类所在的目录及其子孙目录。此时所在的目录和子孙目录,并没有user'Info而在自己的同级目录里,此时报错。

我们就相当于老师突然调课,我们需要给springboot增加配置。添加注解@Component,这个注解可以指定扫描路径,如果没有指定,默认就是该注解所在类的目录及其子孙目录。

我们增加注解,让启动类进行扫描到Configuration包下的类。执行成功。


推荐做法:
把启动类放在我们希望扫描的包的路径下, 这样我们定义的bean就都可以被扫描到

🚩DI 详解

上⾯我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注⼊DI的细节。
依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象.在上⾯程序案例中,我们使⽤了 @Autowired 这个注解,完成了依赖注⼊的操作.简单来说, 就是把对象取出来放到某个类的属性中.
关于依赖注⼊, Spring也给我们提供了三种⽅式:
  • 1. 属性注⼊(Field Injection)
  • 2. 构造⽅法注⼊(Constructor Injection)
  • 3. Setter 注⼊(Setter Injection)
下⾯我们按照实际开发中的模式,将 Service 类注⼊到 Controller 类中
开发过程中可以需要注入静态变量,但是如果使用常规方式,直接在静态变量上面使用@Autowired注解注入是不成功的,使用时报空指针异常

🎈属性注⼊

属性注⼊是使⽤ @Autowired 实现的,将UserInfo类注⼊到 Controller 类中
@Controller
public class UserController {
    @Autowired
    private  UserInfo userInfo2;
    public void doController(){
        System.out.println(userInfo2);
        System.out.println("do Controller.........");
    }
@SpringBootApplication
public class IocDemoApplication {
    public static void main(String[] args) {
//1.属性注入
        ApplicationContext context=SpringApplication.run(IocDemoApplication.class);
        TestController testController=(TestController)context.getBean("testController") ;
       testController.doControl();
    }
}

属性注入以类型进行匹配,与注入的属性名称无关。但是如果一个类型存在多个对象时,有优先名称匹配,如果名称都匹配不上,那就会报错。

我们看到同一个类型出现了两个对象userInfo和userInfo2,那么此时我们要名称匹配,我定义的时候用的是us,此时匹配匹配不上,报错。


🎈构造方法注入


    //二、构造方法注入
    private TestService testService;
    public TestController(){};

    public TestController(UserInfo userInfo){
        this.userInfo=userInfo;
    }
    @Autowired
    public TestController(TestService testService){
        this.testService=testService;
    }
    public TestController(UserInfo userInfo,TestService testService){
        this.userInfo=userInfo;
        this.testService=testService;
    }
    public void doControl2(){
        testService.doService();
        System.out.println("do Controller.........");
    }

注意:

  • 1.如果存在多个构造函数时,需要加@AutoWired注明使用哪个构造函数
  • 2.再添加了有参的构造方法中,最好加上无参的构造方式,防止报错
  • 3.如果只有一个构造函数,@AutoWired可以省略掉
  • 4.明确注入哪个构造函数,否则会造成空指针异常


🎈Setter注入

Setter 注⼊和属性的 Setter ⽅法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注解。
    //三、setter注入
    private TestService testService2;

    @Autowired
    public void setTestService2(TestService testService2) {
        this.testService2 = testService2;
    }
        public void doController3(){
        testService2.doService();
        System.out.println("do Controller.........");
    }


🔴@Autowired存在问题

当同⼀类型存在多个bean时, 使⽤@Autowired会存在问题

程序启动失败,发现类型UserInfo存在多个Bean。


📝解决方法一

属性名和你需要使用的对象名保持一致。


📝解决方法二

使用@Primary注解标识默认的对象


📝解决方法三

使用@Qualifier指定哪个对象


📝解决方法四

使用@Resource注解指定ben


上述四个方法,使用率最高的是@Qualifier和@Resource注解

解决方法思路:给bean重命名或者指定名称


🔴五大注解修改名以及Bean修改名

📝Bean修改名

  @Bean({"u1","uu"})
  @Bean("u1")

一个Bean可以有多个名称,但是一个名称只能对应一个Bean。


📝类注解修改


人生小满胜万全。

  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值