【JavaEE】Spring IoC&DI详解:新手入门与面试指南

目录

一、IoC&DI入门

Spring是什么?

什么是容器?

什么是IoC?

IoC介绍

传统程序开发

问题分析

解决方案

IoC程序开发

IoC优势

DI介绍

二、为什么需要IoC&DI?

[三、Spring IoC容器详解](#三、Spring IoC容器详解)

Bean的存储

@Controller(控制器存储)

@Service(服务存储)

@Repository(仓库存储)

@Component(组件存储)

[@Configuration(配置存储)](#@Configuration(配置存储))

为什么要这么多的类注解?

方法注解@Bean

方法注解要配合类注解使用

定义多个对象

重命名Bean

扫描路径

Bean的生命周期

Bean的作用域

DI详解

属性注入

构造方法注入

Setter注入

三种注入优缺点分析

@Autowired存在的问题

实战案例:从零搭建Spring项目


一、IoC&DI入门

在前面的文章,我们讲解了Spring Boot和Spring MVC的开发,可以完成一些基本功能的开发了,但是什么是Spring呢?Spring,Spring Boot和Spring MVC又有什么关系呢?咱们还是带着问题去学习

我们先看什么是Spring

Spring是什么?

通过前面的学习,我们知道了Spring是一个开源框架,他让我们的开发更加简单。他支持广泛的应用场景,有着活跃而庞大的社区,这也是Spring能够长久不衰的原因

但是这个概念相对来说,还是比较抽象

我们用一句更具体的话来概括Spring,那就是:Spring是包含了众多工具方法的IoC容器

那问题来了,什么是容器?什么又是IoC容器?接下来哦我们一起来看

什么是容器?

容器是用来容纳某种物品的装置。----百度百科

生活中的水杯、垃圾桶、冰箱等等这些都是容器

我们想想,之前Java学习中接触到的容器有哪些?

List/Map-->数据存储容器

Tomcat-->Web容器

什么是IoC?

IoC是Spring的核心思想,也是常见的面试题,那什么是IoC呢?

其实IoC我们在前面已经使用了,我们在前面文章讲到,在类上添加@RestController和@Controller注解,就是把这个对象交给Spring管理,Spring框架启动时就会加载该类。把对象交给Spring管理,就是IoC思想。

IoC(Inversion of Control,控制反转)

IoC是一种设计思想,其核心是将对象的创建、依赖关系的管理从代码中剥离,交由外部容器(如Spring 容器)负责。传统开发中,对象需主动通过 new 创建依赖对象,导致高耦合;而IoC通过容器反转控制权,实现解耦。

类比理解:

传统方式:自己做饭(需亲自买菜、切菜、烹饪)

IoC方式:点外卖(只需下单,由餐厅完成全部流程)

IoC介绍

接下来我们通过案例来了解一下什么是IoC

需求:造一辆车

传统程序开发

我们的实现思路是这样的:

先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据地盘设计车身(Framework),最后根据车身设计好整个汽车(Car)。这里就出现了一个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子

最终程序的实现代码如下:

java 复制代码
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);
        }
    }
}

问题分析

这样的设计看起来没问题,但是可维护性却很低

接下来需求有了变更:随着对车的需求量越来越大,个性化需求也会越来越多,我们需要加工多种尺寸的轮胎

那这个时候就要对上面的程序进行修改了,修改后的代码如下所示:

修改之后,其他调用程序也会报错,我们需要继续修改

完整代码如下:

java 复制代码
public class NewCarExample {
    public static void main(String[] args) {
        Car car = new Car(20);
        car.run();
    }

    /**
     * 汽⻋对象
    **/
    static class Car {
        private Framework framework;

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

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

    * ⻋⾝类
    */
    static class Framework { 
        private Bottom bottom;

        public Framework(int size) { 
            bottom = new Bottom(size);
            System.out.println("Framework init...");
        }
    }

    /**
    * 底盘类
    */
    static class Bottom { 
        private Tire tire;

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

    /**
    * 轮胎类
    */
    static class Tire {
    // 尺⼨
        private int size;

        public Tire(int size){ 
            this.size = size;
            System.out.println("轮胎尺⼨:" + size);
        }
    }
}

从以上代码可以看出,当最底层代码改动之后,整个调用链上的所有代码都需要修改

程序的耦合度非常高(修改一处代码,会影响其他处的代码修改)

主要原因是,我们在每个类中自己创建下级类,像在Car类中,自己使用new来创建Framework类,在Framework类中,自己创建Bottom类.......,如果自己创建下级类就会出现当下级类发生改变操作,自己也要跟着修改。

解决方案

在上面的程序中,我们是根据轮子的尺寸设计的轮盘,轮子的尺寸一改,底盘的设计就得修改。同样因为我们是根据底盘设计的车身,那么底盘一改,车身也得改,同理汽车设计也得改,也就是整个设计几乎都得改

我们尝试换一种思路,我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘,底盘依赖车身,车身依赖汽车

这就类似我们打造一辆完整的汽车,如果所有的配件都是自己造,那么当客户需求发生改变的时候,比如轮胎的尺寸不再是原来的尺寸了,那我们要需要自己动手来改了。但如果我们是把轮胎外包出去,那么即时是轮胎的尺寸发生改变了,我们只需要向代理工厂下订单就行了,我们自身是不需要出力的

如何来实现呢?

从上述例子,我们知道,如果自己创建下级类就会出现 当下级类发生改变操作时,自己也要跟着修改

此时,我们只需要将原来由自己创建的下级类,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦。

IoC程序开发

基于以上思路,我们把调用汽车的程序示例改造一下,把创建子类的方式,改为注入传递的方式

具体实现代码如下:

java 复制代码
  public class IocCarExample {
      public static void main(String[] args) {
          Tire tire = new Tire(20);
          Bottom bottom = new Bottom(tire);
          Framework framework = new Framework(bottom);
          Car car = new Car(framework);
          car.run();
  	}
  
    static class Car {
        private Framework framework;     

        public Car(Framework framework) {
          this.framework = framework;
            System.out.println("Car init	");
    	}
        public void run() {
            System.out.println("Car run	");
    	}
    }
    
    static class Framework {
        private Bottom bottom;     

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

    static class Bottom {
        private Tire tire;

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

    static class Tire {
        private int size;

        public Tire(int size) { 
            this.size = size;
            System.out.println("轮胎尺⼨:" + size);
        }
    }
}

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

IoC优势

在传统的代码中,对象创建顺序是:Car-->Framework-->Bottom-->Tire

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

我们发现了一个规律,通用程序的实现代码,类的创建顺序是反的,传统代码是Car控制并创建了

Framework,Framewoke控制并创建了Bottom,依次往下。而改进之后的控制权发生了反转,不再是使用方对象创建并控制依赖对象了,而是把依赖对象注入到当前对象中,依赖对象的控制权不再由当前类控制了。

这样的话,即使依赖类发生任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是IoC的实现思想。

学到这里,我们大概就知道什么是控制发转了,那什么是控制反转容器呢?也就是IoC容器

这部分代码,就是IoC容器做的工作

从上面也可以看出来,IoC容器具备以下优点

资源不由使用资源的双方管理,而由不使用资源的第三方管理,这可以带来很多好处。

1.资源集中管理:IoC容器会帮我们管理一些资源(对象等),我们需要使用时,只需要从IoC容器中去取就可以了

2.我们在创建实例的时候不需要了解其中的细节,降低了使用资源双方的依赖程度,也就是耦合度

Spring就是一种IoC容器,帮助我们来做了这些资源管理

DI介绍

上面学习了IoC,那什么是DI呢?

DI(Dependency Injection,依赖注入)

DI是IoC的具体实现方式,指容器在运行时动态将依赖对象注入到目标对象中。通过构造函数、Setter方法或注解(如@Autowired)完成注入,避免硬编码依赖

程序运行时需要某个资源,此时容器就为其提供这个资源

从这点来看,依赖注入(DI)和控制反转(IoC)是从不同的角度描述同一件事情,依赖注入是从应用程序的角度来描述,就是指通过引入IoC容器,利用依赖关系注入的方式,实现对象之间的解耦

我们上述代码中,就是通过构造函数的方式,把依赖对象注入到需要使用的对象中的

IoC是一种思想,也是"目标",而思想只是一种指导原则,最终还是要有可行的落地方案,而DI就属于具体的实现。所以也可以说,DI是IoC的一种实现

关键点

IoC是目标:解耦代码,提升可维护性

DI是手段:通过注入依赖实现IoC

二、为什么需要IoC&DI?

传统开发的痛点:

高耦合:修改一个类可能导致依赖链上的所有类需调整。

难测试:单元测试需模拟所有依赖对象

代码重复:多个类重复创建相同依赖对象

IoC&DI的优势:

解耦:对象不直接依赖具体实现,而是面向接口编程

易测试:可轻松注入模拟对象(Mock)进行测试

可维护性:修改依赖关系只需调整配置,无需改动代码

统一管理:容器集中管理对象生命周期和依赖

三、Spring IoC容器详解

IoC控制反转,就是将对象的控制权交给Spring的IoC容器,由IoC容器创建及管理对象,也就是Bean(对象)的存储

Bean的存储

我们要把某个对象交给IoC容器管理,需要在类上添加一个注解:@Component

而Spring框架为了更好的服务Web应用程序,提供了更丰富的注解

共有两类注解类型可以实现:

1.类注解:@Controller、@Service、@Repository、@Component、@Configuration

2.方法注解:@Bean

接下来我们分别来看上述注解

@Controller(控制器存储)

使用@Controller存储Bean的代码如下所示

java 复制代码
@Controller  //将对象存储到Spring中
public class UserController{
    public void sayHi(){
        System.out.println("hi,UserController...");
    }
}

如何观察这个对象是否已经存储到Spring容器当中了呢?

接下来我们学习如何从Spring容器中获取对象

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication{

    public static void main(String[] args){
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplcation.class,args);
        //从Spring上下文中获取对象
        UserController userController=context.getBean(UserController.class);
        //使用对象
        userController.sayHi();
    }
}

解析:

ApplicationContext翻译过来就是:Spring上下文

因为对象都交给Spring管理了,所以获取对象要从Spring中获取,那么就得先得到Spring的上下文

关于上下文的概念

在计算机领域,上下文这个概念,咱们最早是在学习线程时了解到过,比如我们应用进行线程切换的时候,切换前都会把线程的状态信息暂时存储起来,这里的上下文就包括了当前线程的信息,等下次该线程又得到CPU时间的时候,从上下文中拿到线程上次运行的信息

这个上下文,就是指当前的运行环境,也可以看作是一个容器,容器里存了很多内容,这些内容是当前运行的环境

观察运行结果,发现成功从Spring中获取到Controller对象,并执行Controller的sayHi方法

如果我们把UserController的@Controller删掉,再观察运行结果:

报错信息显示:找不到类型是: 'com.fei.springiocdemo.controller.UserController'的bean

获取bean对象的其他方式

java 复制代码
UserController userController=context.getBean(UserController.class);

上述代码是根据类型(UserController.class)来查找对象,如果Spring容器中,同一个类型存在多个bean的话,怎么来获取呢?

先来说说为什么会出现多个同类型Bean?

Spring允许为同一个接口或类注册多个Bean实例,常见场景包括:

1.多个实现类:一个接口有多个实现类,且均被Spring管理

java 复制代码
@Service
public class EmailNotificationService implements NotificationService { ... }

@Service
public class SmsNotificationService implements NotificationService { ... }

这里的EmailNotficationService和SmsNotificationService即是NotificationService接口的实现类

当使用getBean(NotificationService.class)来获取Bean对象时,由于NotificationService接口有两个实现类,Spring容器无法确定应该返回哪一个,因此会抛出异常

2.重复配置:通过@Bean方法或XML配置重复定义了同类型的Bean

java 复制代码
@Configuration
public class AppConfig {
    @Bean
    public NotificationService emailService() { 
        return new EmailNotificationService(); 
    }

    @Bean
    public NotificationService smsService() { 
        return new SmsNotificationService(); 
    }
}

冲突原因

两个方法都返回NotificationService类型,Spring会为它们生成两个不同的Bean定义(名称默认为方法名:emailService和smsService)

当调用context.getBean(NotificationService.class)时,Spring发现容器中有两个同类型Bean(emailService和smsServic),无法确定应该返回哪一个,因此抛出异常

ApplicationContext也提供了其他获取bean的方式,ApplicationContext获取Bean对象功能是由其父类BeanFactory提供的

java 复制代码
public interface BeanFactory {
    String FACTORY_BEAN_PREFIX = "&";
    char FACTORY_BEAN_PREFIX_CHAR = '&';
//1.根据bean名称获取bean
    Object getBean(String name) throws BeansException;
//2.根据bean名称和类型获取bean
    <T> T getBean(String name, Class<T> requiredType) throws BeansException;
//3.按bean名称和构造函数参数动态创建bean,只适用于具有原型(prototype)作用域的bean
    Object getBean(String name, Object... args) throws BeansException;
//4.根据类型获取bean
    <T> T getBean(Class<T> requiredType) throws BeansException;
//5.按bean类型和构造函数参数动态创建bean,只适用于具有原型(prototype)作用域的bean
    <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

常用的是上述1、2、4种,这三种方式获取到的bean是一样的

其中1、2种都涉及到根据名称来获取对象。

那么bean的名称是什么呢?

Spring bean是Spring框架在运行时管理的对象,Spring会给管理的对象起一个名字

后续,Spring就可以根据Bean的名称(BeanId)就可以获取到对应的对象

Bean命名约定

大家可以看下官方文档的说明:Bean Overview :: Spring Framework

程序开发人员不需要为Bean指定名称(BeanId),如果没有显式的提供名称(BeanId),Spring容器将为该bean生成唯一的名称

命名约定使用Java标准约定作为实例字段名。也就是说,bean名称以小写字母开头,然后使用驼峰式大小写

比如:

类名:UserController-->Bean的名称为:userController

也有一些特殊情况,当有多个字符并且第一个和第二个都是大写时,将保留原始的大小写。这些规则与java.beans.Introspector.decapitalize(Spring在这里使用的)定义的规则相同

比如:

类名:UController-->Bean的名称为:UController

根据这个命名规则,我们来获取Bean

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        //从Spring上下文中获取对象
        //1.根据Bean类型获取
        UserController userController1=context.getBean(UserController.class);
        //2.根据Bean名称获取
        UserController userController2=(UserController)context.getBean("userController");
        //3.根据Bean类型+名称获取
        UserController userController3=context.getBean("userController",UserController.class);
        //使用对象
        System.out.println(userController1);
        System.out.println(userController2);
        System.out.println(userController3);
    }
}

运行结果:

地址一样,说明对象是一个

获取Bean对象,是父类BeanFactory提供的功能

常见面试题

ApplicationContext VS BeanFactory

继承关系和功能来说:Spring容器有两个顶级的接口:BeanFactory和ApplicationContext。其中BeanFactoryy提供了基础的访问容器的能力,而ApplicationContext属于BeanFactory的子类,它除了继承了BeanFactory的所有功能之外,它还拥有独特的特性,还添加了对国际化支持、资源访问支持、以及事件传播等方面的支持

从性能方面来说:ApplicationContext是一次性加载并初始化所有的Bean对象,而BeanFactory是需要哪个采取加载哪个,因此更加轻量(空间换时间)

@Service(服务存储)

使用@Service存储bean的代码如下所示:

java 复制代码
@Service
public class UserService {
    public void sayHi(String name){
        System.out.println("hi,"+name);
    }
}

读取bean的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        UserService userService=context.getBean(UserService.class);
        userService.sayHi("world");
    }
}

观察运行结果,发现成功从Spring中获取到UserService对象,并执行UserService的sayHi方法

同样的,把注解@Service删掉,再观察运行结果,如下图:

@Repository(仓库存储)

使用@Repository存储bean的代码如下

java 复制代码
@Repository
public class UserRepository {
    public void sayHi(){
        System.out.println("Hi,Repository");
    }
}

读取bean的代码

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        UserRepository userRepository=context.getBean(UserRepository.class);
        userRepository.sayHi();
    }
}

观察运行结果,发现成功从Spring中获取到UserRepository对象,并执行UserRepository的sayHi方法

同样的,把注解@Repository删掉,运行结果类似上面@Service

@Component(组件存储)

使用@Component存储bean的代码如下:

java 复制代码
@Component
public class UserComponent {
    public void sayHi(){
        System.out.println("Hi,Component");
    }
}

读取bean的代码

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        UserComponent userComponent=context.getBean(UserComponent.class);
        userComponent.sayHi();
    }
}

运行结果同上述@Repository

@Configuration(配置存储)

和上述四个注解类似,这里就不再复述了

为什么要这么多的类注解?

这个其实就涉及到了应用分层。

简单来说,如果按照某一合理的项目结构去将代码分类,项目代码会简洁美观,便于查找

实例:

让程序员看到类注解之后,就能直接了解当前类的用途

@Controller:控制层,接收请求,对请求进行处理,并返回响应

@Service:业务逻辑层,处理具体的业务逻辑

@Repository:数据访问层,也成为持久层。负责数据访问操作

@Configuration:配置层。处理项目中的一些配置信息

类注解之间的关系

这里我们先查看@Controller/@Service/@Repository/@Configuration等注解的源码:

其实这些注解里面都有一个@Component,说明它们本身就是属于@Component的"子类"。

@Component是一个元注解,也就是说可以注解其他类注解,如@Controller,@Service,@Repository等。这些注解被称为@Component的衍生注解

@Controller,@Service和@Repository用于更具体的用例(分别在控制层,业务逻辑层,持久化层),在开发过程中,如果我们要在业务逻辑层使用@Component或@Service,显然@Service是更好的选择

方法注解@Bean

上面我们讲解了五大类注解,知道类注解是添加到某个类上的,但是存在两个问题:

1.使用外部包里的类,没办法添加类注解

2.一个类,需要多个对象,比如多个数据源

这种场景,我们就需要使用方法注解@Bean

我们先来看看方法注解如何使用:

java 复制代码
public class BeanConfig {
    @Bean
    public User user(){
        User user=new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
}

然而,当我们写完上述代码,尝试获取bean对象中的user时却发现,根本获取不到:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        User user=context.getBean(User.class);
        System.out.println(user);
    }
}

程序执行结果如下:

显示没有User类型的bean对象

这是为什么呢?

结论是:方法注解要配合类注解使用

方法注解要配合类注解使用

在Spring框架的设计中,方法注解@Bean要配合类注解才能将对象正常的存储到Spring容器中,代码如下所示:

java 复制代码
@Component
public class BeanConfig {
    @Bean
    public User user(){
        User user=new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
}

再次执行获取User对象的代码,结果如下:

定义多个对象

对于同一个类,如何定义多个对象呢?

比如多数据源的场景,类是同一个,但是配置不同,指向不同的数据源

我们看下@Bean的使用

java 复制代码
@Component
public class BeanConfig {
    @Bean
    public User user1(){
        User user=new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
    
    @Bean
    public User user2(){
        User user=new User();
        user.setName("lisi");
        user.setAge(19);
        return user;
    }
}

定义了多个对象的话,我们根据类型获取对象,获取的是哪个对象呢?

运行结果:

报错信息显示:期望只有一个匹配,结果发现了两个,user1,user2

从报错信息中,可以看出来,@Bean注解的对象名称就是它的方法名

接下来我们根据名称来获取bean对象

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        User user1=(User) context.getBean("user1");
        User user2=(User) context.getBean("user2");
        System.out.println(user1);
        System.out.println(user2);
    }
}

运行结果:

可以看到,@Bean可以针对同一个类,定义多个对象

重命名Bean

可以通过设置name属性给Bean对象进行重命名操作,代码如下:

java 复制代码
@Bean(name = {"u1","user1"})
    public User user1(){
        User user=new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }

此时我们使用u1就可以获取到User1对象了,代码如下:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        User u1=(User) context.getBean("u1");
        System.out.println(u1);
    }
}

运行结果如下:

注意:这里name={}可以省略,如下所示:

java 复制代码
@Bean({"u1","user1"})
    public User user1(){
        User user=new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }

只有一个名称时,{}也可以省略,如:

java 复制代码
@Bean("u1")
    public User user1(){
        User user=new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
扫描路径

使用前面学习的四个注解声明的Bean,一定会生效吗?

答案:不一定(原因:Bean想要生效,还需要被Spring扫描)

下面我们通过修改项目工程的目录结构,来测试bean对象是否生效:

再运行代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        User u1=(User) context.getBean("u1");
        System.out.println(u1);
    }
}

运行结果:

解释:没有bean的名称为u1

为什么没有找到bean对象呢?

使用五大注解声明的bean,要想生效,还需要配置扫描路径,让Spring扫描到这些注解

也就是通过@ComponentScan来配置扫描路径

java 复制代码
@ComponentScan({"com.fei.springiocdemo"})
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        User u1=(User) context.getBean("u1");
        System.out.println(u1);
    }
}

{}里可以配置多个包路径

这种做法仅做了解,不推荐使用

再次运行:

那为什么前面在没有配置@ComponentScan注解也可以正确运行呢?

原先@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中了

默认的扫描范围是SpringBoot启动类所在包及其子包

在配置类上添加@ComponentScan注解,该注解默认会扫描该类所在的包下所有配置类

推荐做法:

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

即:原先的包结构

Bean的生命周期

实例化:容器通过反射创建Bean实例

属性填充:注入依赖对象(DI阶段)

初始化:调用@PostConstruct注解方法或自定义init-method

使用:Bean处于就绪状态,供应用程序调用

销毁:容器关闭时调用@PreDestroy或destory-method

Bean的作用域

Singleton(默认):整个容器中仅一个实例

Prototype:每次请求创建新实例

Request(Web环境):每个HTTP请求一个实例

Session(Web环境):每个用户会话一个实例

四、DI详解

上面我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注入DI的细节

依赖注入是一个过程,是指IoC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象

举例:

java 复制代码
@Controller  //将对象存储到Spring中
public class UserController{
    
    //注入UserService
    @Autowired
    private UserService userService;
    
    public void sayHi(){
        System.out.println("hi,UserController...");
    }
}

在上面程序案例中,我们使用@Autowired这个注解,完成了依赖注入的操作

简单来说,就是把对象取出来放到某个类的属性中

关于依赖注入,Spring也给我们提供了三种方式:

1.属性注入(Field Injection)

2.构造方法注入(Constructor Injection)

3.Setter注入(Setter Injection)

java 复制代码
@Controller  //将对象存储到Spring中
public class UserController{

    @Autowired
    private UserService userService;

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

接下来,我们分别来看

下面我们按照实际开发中的模式,将Service类注入到Controller类中

属性注入

属性注入是使用@Autowired实现的,将Service类注入到Controller类中。

Service类的实现代码如下:

java 复制代码
@Service
public class UserService {
    public void sayHi(){
        System.out.println("hi,UserService");
    }
}

Controller类的实现代码如下:

java 复制代码
@Controller  //将对象存储到Spring中
public class UserController{

    //注入方法1:属性注入
    @Autowired
    private UserService userService;

    public void sayHi(){
        System.out.println("hi,UserController...");
        userService.sayHi();//注意这里调用UserService中的sayHi方法
    }
}

获取Controller中的sayHi方法:

java 复制代码
    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        UserController userController=(UserController) context.getBean("userController");
        userController.sayHi();
    }

运行结果如下:

这里,我们去掉UserService的@Autowired注解,再运行程序观察结果:

构造方法注入

构造方法注入是在类的构造方法中实现注入,代码如下所示:

java 复制代码
@Controller  //将对象存储到Spring中
public class UserController2{

    //注入方法2:构造方法
    private UserService userService;

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

    public void sayHi(){
        System.out.println("hi,UserController2...");
        userService.sayHi();
    }
}

运行结果:

注意:如果类只有一个构造方法,那么@Autowired注解可以省略;如果类中有多个构造方法,那么需要添加上@Autowired来明确指定到底使用哪个构造方法

Setter注入

Setter注入和属性的Setter方法实现类似,只不过在设置set方法的时候需要加上@Autowired注解,代码如下所示:

java 复制代码
@Controller
public class UserController3 {
    //注入方法3:setter方法注入
    private UserService userService;

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

    public void sayHi(){
        System.out.println("hi,UserController3...");
        userService.sayHi();
    }
}

这里如果移除了@Autowired注解后,Spring无法自动通过setter方法注入UserService,导致依赖缺失而报错。

三种注入优缺点分析

属性注入

优点:简洁,使用方便

缺点:

只能用于IoC容器,非IoC容器不可用,并且只有在使用的时候才会出现空指针异常

不能注入一个final修饰的属性

构造函数注入(Spring 4.X推荐)

优点:

可以注入final修饰的属性

注入的对象不会被修改

依赖对象在使用前一定会被完全初始化,因为依赖是在类的构造方法中执行的,而构造方法是在类加载阶段就会执行的方法

通用性好,构造方法是JDK支持的,所以无论更换任何框架,它都是适用的

缺点:

注入多个对象时,代码会比较繁琐

Setter注入(Spring 3.X推荐)

优点:

方便在类实例之后,重新对该对象进行配置或者注入

缺点::

不能注入一个final修饰的属性

注入对象可能会被改变,因为setter方法可能会被多次调用,就有被修改的风险

@Autowired存在的问题

当同一类型存在多个Bean时,使用@Autowired会存在问题

java 复制代码
@Component
public class BeanConfig {

    @Bean(name = {"u1","user1"})
    public User user1(){
        User user=new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }

    @Bean
    public User user2(){
        User user=new User();
        user.setName("lisi");
        user.setAge(19);
        return user;
    }
}
java 复制代码
@Controller  //将对象存储到Spring中
public class UserController{

    //注入方法1:属性注入
    @Autowired
    private UserService userService;


    @Autowired
    private User user;

    public void sayHi(){
        System.out.println("hi,UserController...");
        userService.sayHi();
        System.out.println(user);
    }
}
java 复制代码
@ComponentScan({"com.fei.springiocdemo"})
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
        UserController u1=(UserController) context.getBean("userController");
        u1.sayHi();
    }
}

运行结果:

报错原因是:非唯一的Bean对象

主要是因为在BeanConfig中有两个Bean对象,并且它们的返回类型都为User,而在UserController中需要注入User对象,此时Spring就不知道到底该注入BeanConfig的哪个对象了

这里我们只需明确将BeanConfig中的哪个Bean对象注入给UserController即可

如何解决上述问题呢?Spring提供了以下几种解决方案:

1.@Primary

2.@Qualifier

3.@Resource

使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现

java 复制代码
@Component
public class BeanConfig {

    @Primary //指定该bean为默认bean的实现
    @Bean(name = {"u1","user1"})
    public User user1(){
        User user=new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }

    @Bean
    public User user2(){
        User user=new User();
        user.setName("lisi");
        user.setAge(19);
        return user;
    }
}

使用@Qualifier注解:指定当前要注入的bean对象。在@Qualifier的value属性中,指定注入的bean的名称

注意:@Qualifier注解不能单独使用,必须配合@Autowired使用

java 复制代码
@Controller  //将对象存储到Spring中
public class UserController{

    //注入方法1:属性注入
    @Autowired
    private UserService userService;


    @Qualifier("user2") //指定bean的名称
    @Autowired
    private User user;

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

使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称

java 复制代码
@Controller  //将对象存储到Spring中
public class UserController{

    //注入方法1:属性注入
    @Autowired
    private UserService userService;


    @Resource(name="user2")
    private User user;

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

注意:与@Qualifier不同的时,在使用@Resource注解时,不能与@Autowired同时使用

@Autowired与@Resource的区别

@Autowired是Spring框架提供的注解,而@Resource是JDK提供的注解

@Autowired默认是按照类型注入,而@Resource是按照名称注入。相比于@Autowired来说,@Resource支持更多的参数设置,例如name设置,根据名称获取bean

Autowired注入顺序

五、实战案例:从零搭建Spring项目

1.添加依赖(Maven)

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

2.定义接口和实现类

java 复制代码
public interface UserRepository {
    void save(String name);
}

@Repository
public class JdbcUserRepository implements UserRepository {
    @Override
    public void save(String name) {
        System.out.println("Saving user: " + name);
    }
}

3.使用构造函数注入依赖

java 复制代码
@Service
public class UserService {
    private final UserRepository repository;

    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public void addUser(String name) {
        repository.save(name);
    }
}

4.运行测试

java 复制代码
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        UserService userService = context.getBean(UserService.class);
        userService.addUser("Alice");
    }
}

输出结果:

Saving user: Alice

相关推荐
梦未2 小时前
Spring控制反转与依赖注入
java·后端·spring
喜欢流萤吖~2 小时前
Lambda 表达式
java
ZouZou老师3 小时前
C++设计模式之适配器模式:以家具生产为例
java·设计模式·适配器模式
曼巴UE53 小时前
UE5 C++ 动态多播
java·开发语言
VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue音乐管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
程序员鱼皮3 小时前
刚刚,IDEA 免费版发布!终于不用破解了
java·程序员·jetbrains
Hui Baby4 小时前
Dubbo/springCloud同机房收敛
spring·spring cloud·dubbo
Hui Baby4 小时前
Nacos容灾俩种方案对比
java
曲莫终4 小时前
Java单元测试框架Junit5用法一览
java