Spring中是如何实现IoC和DI的?

前言:在前一篇文章中对于IoC的核心思想进行了讲解,而本篇文章则从Spring的角度入手,体会Spring对于IoC是如何实现的。 如果对IoC还有不太了解的可以阅读上一篇文章,相信一定会带来全新的收获:什么是IoC(控制反转)思想?


目录

一.Spring中的IoC

@Controller

@Service

@Repository

@Component

@Configuration

类注解之间的区别

类注解之间的联系

方法注解@Bean

[二.DI------Dependency Injection(依赖注入)](#二.DI——Dependency Injection(依赖注入))

[▐ 属性注入](#▐ 属性注入)

[▐ 构造方法注入](#▐ 构造方法注入)

[▐ Setter注入](#▐ Setter注入)

三种注入的优缺点

@Autowired存在问题


一.Spring中的IoC

在前一篇文章中,我们通过制造小汽车的例子对于IoC的思想有了一定的认知,在制造小汽车的过程中,车身、底盘、轮胎等对象层层依赖,通过IoC我们降低了代码的耦合度。Spring作为Java领域最热门的框架,它有俩个核心思想分别是IoC和AOP,我们也常常会听见一句话:"Spring是包含了众多工具方法的IoC容器"。

IoC是Spring的核心思想,也是常见的面试题,对于IoC的细致讲解在上篇文章中进行了介绍:什么是IoC(控制反转)思想?,这里就只是简单的介绍一下:

IoC全称Inversion of Control (控制反转) ,这里的控制其实是控制权的意思。可以理解为对象的获取权力和方式发生了发转。也就是说,当需要某个对象时,传统开发模式中需要⾃⼰通过 new 创建对象;而IoC的思想则不一样,IoC旨在把创建对象的任务交给外部的容器,程序中只需要引入容器里存放的需要的对象即可。这个容器一般被称为:IoC容器。

其实IoC我们在前⾯已经使⽤了,我们在前⾯讲到,在类上⾯添加 @RestController 和 @Controller 注解,就是把这个对象交给Spring管理,Spring 框架启动时就会加载该类,把对象交给Spring管理,这就是Spring中的IoC思想。Spring对于IoC的实现主要是通过工厂设计模式+反射来实现的,当我们需要某个对象的时候,只需要将创建对象的任务交给容器,在程序中只需要调用(注入)即可。

前⾯我们提到IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。那么在Spring程序中,我们该如何通过代码来实现IoC呢?

Spring框架为了更好的服务应用程序,提供了俩类注解来实现将对象集中管理创建:

  • 类注解:@Controller、@Service、@Repository、@Component、@Configuration
  • 方法注解:@Bean

@Controller

使⽤ @Controller 存储 bean 的代码如下所⽰:

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

如何观察这个对象已经存在Spring容器当中了呢? 我们可以通过启动类中的getBean方法来得到Spring中管理的Bean。

java 复制代码
@SpringBootApplication
public class IoCdemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(IoCdemoApplication.class, args);
        UserController bean = context.getBean(UserController.class);
        bean.sayHi();
    }
}

在控制台即可观察到输出:

但是一旦将@Controller注释掉,程序就会报错找不到这个Bean,这就说明了使用@Controller确实是可以将该类对象交给Spring进行管理

@Service

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

还是来获取一下这个对象,看看结果如何

java 复制代码
ConfigurableApplicationContext context = SpringApplication.run(IoCdemoApplication.class, args);
UserService bean = context.getBean(UserService.class);
bean.sayHi("CSDN");

如果注释掉@Service就会报错,因此可以证实@Service可以将类对象交给Spring进行管理

后续的三个注解如果进行相同方式的验证都会得到一样的结果,这里就不再赘述

@Repository

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

@Component

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

@Configuration

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

那么既然这么多注解干的都是同一件事,为什么还非要分出这么多注解呢?

类注解之间的区别

这个也是和咱们前⾯讲的应⽤分层是呼应的,不同的注解标识着不同的信息,这些注解可以让程序员看到类注解之后,就能直接了解当前类的⽤途。

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

这和每个省/市都有⾃⼰的⻋牌号是⼀样的。⻋牌号都是唯⼀的,标识⼀个⻋辆的。但是为什么还需要设置不同的⻋牌开头呢?⽐如陕西的⻋牌号就是:陕X:XXXXXX,北京的⻋牌号:京X:XXXXXX,甚⾄⼀个省不同的县区也是不同的,⽐如西安就是,陕A:XXXXX,咸阳:陕B:XXXXXX,宝鸡,陕C:XXXXXX,⼀样。

这样做的好处除了可以节约号码之外,更重要的作⽤是可以直观的标识⼀辆⻋的归属地.

对于一般的开发我们通过不同的注解去确认它是哪一层的代码,这样更方面上下层进行调用

类注解之间的联系

细心的朋友可能发现了,上述的注解中少了一个@Component注解,我们不妨打开每个注解的源码看看。

我们会发现上述4个注解中都有@Component注解,这说明它们本⾝就是属于 @Component 的 "⼦类"。@Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller、@Service 、@Repository 等, 这些注解被称为 @Component 的衍⽣注解。

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

诸如@Configuration,见名知意就是用来标记配置相关的,比如我们想用Redis的数据库,在使用Redis之前需要对Redis数据库的密码或者库做一些配置,配置好了之后我们需要将这个配置类交给Spring管理方便我们在别的地方直接访问Redis数据库,这时使用@Configuration就是很好的选择。

⽐如杯⼦有喝⽔杯、刷⽛杯等,但是我们更倾向于在⽇常喝⽔时使⽤⽔杯,洗漱时使⽤刷⽛杯。

方法注解@Bean

类注解是添加到某个类上的, 但是存在两个问题

  1. 使⽤外部包⾥的类, 没办法添加类注解
  2. ⼀个类, 需要多个对象, ⽐如多个数据源

比如我们想通过引入第三方的工具类去操作数据库,我们想将这个类交给Spring管理方便我们进行二次开发,但是由于第三方包只能读取不能写入的情况,就会陷入进退俩难的情况。

@Bean 同上述类注解的功能一样,都是将标记的对象交给Spring进行管理,但在 Spring 框架的设计中,⽅法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中,如下代码所⽰:

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

我们也可以通过设置name属性给Bean对象进行重命名

java 复制代码
@Bean(name = {"u1","user1"})

二.DI------Dependency Injection(依赖注入)

依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象,在之前程序案例中,我们使⽤了 @Autowired 这个注解,完成了依赖注⼊的操作。

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

  • 属性注⼊(Field Injection)
  • 构造⽅法注⼊(Constructor Injection)
  • Setter 注⼊(Setter Injection)

**▐**属性注入

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

Service 类的实现代码如下:

java 复制代码
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void sayHi() {
        System.out.println("Hi,UserService");
    }
}

Controller 类的实现代码如下:

java 复制代码
@Controller
public class UserController {
    //注⼊⽅法1: 属性注⼊
    @Autowired
    private UserService userService;
    
    public void sayHi() {
        System.out.println("hi,UserController...");
        userService.sayHi();
    }
}

**▐**构造方法注入

构造⽅法注⼊是在类的构造⽅法中实现注⼊,如下代码所⽰:

java 复制代码
@Controller
public class UserController2 {
    //注⼊⽅法2: 构造⽅法
    private UserService userService;
    @Autowired
    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();
    }
}

三种注入的优缺点

属性注⼊

优点:

  • 简洁,使⽤⽅便

缺点:

  • 只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指针异常)
  • 不能注⼊⼀个Final修饰的属性

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

优点:

  • 可以注⼊final修饰的属性
  • 注⼊的对象不会被修改
  • 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法是在类加载阶段就会执⾏的⽅法.
  • 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的

缺点:

  • 注⼊多个对象时, 代码会⽐较繁琐

Setter注入(Spring 3.X推荐)

优点:

  • ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊

缺点:

  • 不能注⼊⼀个Final修饰的属性
  • 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险

@Autowired存在问题

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

java 复制代码
@Component
public class BeanConfig {
    @Bean("u1")
    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
public class UserController {
    
    @Autowired
    private UserService userService;
    //注⼊user
    @Autowired
    private User user;
    public void sayHi(){
        System.out.println("hi,UserController...");
        userService.sayHi();
        System.out.println(user);
    }
}

程序会出现无法正确找到Bean的报错

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

  • @Primary
  • @Qualifier
  • @Resource

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

java 复制代码
@Component
public class BeanConfig {
    @Primary //指定该bean为默认bean的实现
    @Bean("u1")
    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
public class UserController {
    @Qualifier("user2") //指定bean名称
    @Autowired
    private User user;
    public void sayHi(){
        System.out.println("hi,UserController...");
        System.out.println(user);
    }
}

使⽤@Resource注解:是按照bean的名称进⾏注⼊。通过name属性指定要注⼊的bean的名称。并且由于@Resource是由JDK提供的,因此在其他框架下也有可延展性。

java 复制代码
@Controller
public class UserController {
    @Resource(name = "user2")
    private User user;
    public void sayHi(){
        System.out.println("hi,UserController...");
        System.out.println(user);
    }
}

@Autowired 是spring框架提供的注解,⽽@Resource是JDK提供的注解
@Autowired 默认是按照类型注⼊,⽽@Resource是按照名称注⼊。相⽐于 @Autowired 来说 @Resource ⽀持更多的参数设置,例如 name 设置,根据名称获取 Bean




本次的分享就到此为止了,希望我的分享能给您带来帮助,创作不易也欢迎大家三连支持,你们的点赞就是博主更新最大的动力! 如有不同意见,欢迎评论区积极讨论交流,让我们一起学习进步! 有相关问题也可以私信博主,评论区和私信都会认真查看的,我们下次再见

相关推荐
大厂码农老A2 分钟前
面试官:“聊聊你最复杂的项目?” 为什么90%的候选人第一句就栽了?
java·面试
爱读源码的大都督9 分钟前
Java已死?别慌,看我如何用Java手写一个Qwen Code Agent,拯救Java
java·人工智能·后端
lssjzmn9 分钟前
性能飙升!Spring异步流式响应终极指南:ResponseBodyEmitter实战与架构思考
java·前端·架构
黑客飓风24 分钟前
从基础功能到自主决策, Agent 开发进阶路怎么走?
面试·log4j·bug
LiuYaoheng25 分钟前
【Android】View 的基础知识
android·java·笔记·学习
勇往直前plus33 分钟前
Sentinel微服务保护
java·spring boot·微服务·sentinel
星辰大海的精灵33 分钟前
SpringBoot与Quartz整合,实现订单自动取消功能
java·后端·算法
小鸡脚来咯36 分钟前
一个Java的main方法在JVM中的执行流程
java·开发语言·jvm
江团1io037 分钟前
深入解析三色标记算法
java·开发语言·jvm
天天摸鱼的java工程师1 小时前
RestTemplate 如何优化连接池?—— 八年 Java 开发的踩坑与优化指南
java·后端