一、引言
在现代软件开发中,Spring 框架已经成为构建企业级应用的首选工具,而在这其中,IoC(控制反转)和 DI(依赖注入)是简化开发过程、提升代码灵活性和可维护性的关键利器。它们不仅解放了开发者的双手,还为系统架构的可扩展性和模块化提供了坚实的基础。
这篇博客将带你深入探索 Spring 框架中 IoC 和 DI 的核心概念,从理论到实践,逐步揭示它们如何将繁琐的依赖管理转化为优雅的代码架构。无论你是刚接触 Spring 的新手,还是希望深入了解其背后原理的资深开发者,这篇文章都将为你提供独特的视角和实用的指导。让我们一起揭开这两个强大工具的神秘面纱,看看它们如何成为简化 Spring 应用开发的秘密武器。
二、控制反转 IoC
2.1、什么是 IoC
a)IoC 的定义与原理
大家都说 Spring 是包含了众多工具方法的 IoC 容器,那究竟什么是 IoC 容器呢?首先得理解容器这个词的意思,容器就是装某个东西的东西,举个例子,杯子是水的容器,好比我们之前学的 Tomcat,它是一个 Web 容器,Map、List 等集合,是一个数据存储的容器。
IoC 是Spring 的核心思想,我们之前其实就接触过 IoC 了,比如写代码的时候用到的五大注解,如:@RestController
、@Controller
这些,就是把我们写的这些类交给 Spring 统一进行管理,Spring 框架启动时就会加载该类,这种思想就是 IoC 思想。而 IoC 这种思想有个专业术语叫 "控制反转"。很好理解嘛,字面意思,本来控制权是我们自己的,但是通过注解的方式,把控制权交给 Spring 管理,我们自己就不管理了,就不用每次使用的时候 new 对象了,Spring 在项目启动的时候就给我们 new 好了,我们直接用就行,有点饿汉模式内味儿了哈。
b)传统对象创建与管理 vs IoC
再使用代码说明一下,假设我们要造一辆汽车,这辆汽车是不是依赖于车身,然后车身依赖于底盘,底盘又依赖于轮子,那我们的代码就这样诞生了,如下:
public class BlogTest {
public static void main(String[] args) {
Car car = new Car();
car.run();
}
}
// 车子
class Car {
private Framework framework;
public Car() {
framework = new Framework();
}
public void run() {
System.out.println("cat run...");
}
}
// 车身
class Framework {
private Bottom bottom;
public Framework() {
bottom = new Bottom();
System.out.println("Framework init...");
}
}
// 底盘
class Bottom {
private Tire tire;
public Bottom() {
tire = new Tire();
System.out.println("Bottom init...");
}
}
// 轮胎
class Tire {
private Integer size;// 轮胎尺寸
public Tire() {
this.size = 17;
System.out.println("轮胎尺⼨:" + size);
}
}
这是运行结果:
虽然说这样子我们看着没问题,但是假设说我们需要自己定义轮子的大小呢?那代码就得改成这样了:
public class BlogTest {
public static void main(String[] args) {
Car car = new Car(19);
car.run();
}
}
// 车子
class Car {
private Framework framework;
public Car(int size) {
framework = new Framework(size);
}
public void run() {
System.out.println("cat run...");
}
}
// 车身
class Framework {
private Bottom bottom;
public Framework(int size) {
bottom = new Bottom(size);
System.out.println("Framework init...");
}
}
// 底盘
class Bottom {
private Tire tire;
public Bottom(int size) {
tire = new Tire(size);
System.out.println("Bottom init...");
}
}
// 轮胎
class Tire {
private Integer size;// 轮胎尺寸
public Tire(int size) {
this.size = size;
System.out.println("轮胎尺⼨:" + this.size);
}
}
大家也看到了,耦合度非常的高,那既然这样,我们不妨换个思路,直接就让它们先创建好,然后我们直接使用,再通俗一点就是,你先造好配件,然后我直接用就行,如下:
public class BlogTest {
public static void main(String[] args) {
Tire tire = new Tire(10);
Bottom bottom = new Bottom(tire);
Framework framework = new Framework(bottom);
Car car = new Car(framework);
car.run();
}
}
// 车子
class Car {
private Framework framework;
public Car(Framework framework) {
this.framework = framework;
}
public void run() {
System.out.println("cat run...");
}
}
// 车身
class Framework {
private Bottom bottom;
public Framework(Bottom bottom) {
this.bottom = bottom;
System.out.println("Framework init...");
}
}
// 底盘
class Bottom {
private Tire tire;
public Bottom(Tire tire) {
this.tire = tire;
System.out.println("Bottom init...");
}
}
// 轮胎
class Tire {
private Integer size;// 轮胎尺寸
public Tire(int size) {
this.size = size;
System.out.println("轮胎尺⼨:" + this.size);
}
}
那这样的耦合度是不是就降低了呢?这样的思想就是控制反转。
在传统的代码中对象创建顺序是:Car -> Framework -> Bottom -> Tire
改进之后解耦的代码的对象创建顺序是:Tire -> Bottom -> Framework -> Car
用一张图表示就是:
我们发现了⼀个规律,通⽤程序的实现代码,类的创建顺序是反的,传统代码是 Car 控制并创建了 Framework,Framework 创建并创建了 Bottom,依次往下,而改进之后的控制权发⽣的反转,不再是使⽤⽅对象创建并控制依赖对象了,而是把依赖对象注⼊将当前对象中,依赖对象的控制权不再由当前类控制了,这样的话,即使依赖类发⽣任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是 IoC 的实现思想。
2.2、IoC 的优点
- 提高代码的模块化和可维护性:通过将对象的创建和管理职责交给 IoC 容器,可以使代码更加模块化。
- 降低类之间的耦合度:减少类与类之间的直接依赖,提高代码的灵活性。
- 增强测试性:通过依赖注入,可以方便地对类进行单元测试。
三、依赖注入 DI
3.1、什么是 DI
其实 IoC 和 DI 是从不同的角度的描述的同⼀件事情,就是指通过引入 IoC 容器,利用依赖关系注入的方式,实现对象之间的解耦。说的通俗点就是 IoC 是把对象交给 Spring 管理,就是往这个容器里面存东西,而 DI 呢就是从这个容器里面把取出来。所以 DI 是实现 IoC 的一种具体方式。
3.1、DI 的实现
- Spring 框架中如何实现 DI:Spring 通过其 IoC 容器实现依赖注入,支持多种配置方式。
- XML 配置 vs 注解配置 vs Java 配置:Spring 提供了多种配置方式来实现依赖注入,包括 XML 配置文件、注解和 Java 配置类。
四、IoC & DI 使用
我们知道了控制反转和依赖注入,那我们又该怎样使用呢?控制反转的方式是使用五大注解和 @Bean
,DI 又分三种,分别是构造方法注入,Setter方法注入,字段注入,接口注入,方法注入。这里只详细讲解字段注入,这也是最常用的方式。
4.1、如何把控制权交给 Spring ?
a)类处理方式
类的话主要是使用五大注解,分别是:@Controller
、@Service
、@Repository
、@Component
、@Configuration
。那这些注解有什么区别呢?如下:
- @Controller:控制层,接收请求,对请求进行处理,并进行响应。
- @Servie:业务逻辑层,处理具体的业务逻辑。
- @Repository:数据访问层,也称为持久层,负责数据访问操作。
- @Configuration:配置层,处理项目中的⼀些配置信息。
代码演示如下:
@Service
public class BlogTest {
public void test() {
System.out.println("Service Run...");
}
}
这样我们就存好了,那该怎么取出来呢?欸,这就要使用 ApplicationContext
里面的 getBean
方法了(注意导包,是这个 import org.springframework.context.ApplicationContext;
),也就是我们启动 web 那个程序的方法,代码如下:
@SpringBootApplication
public class BeanTestApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
context.getBean(BlogTest.class).test();
}
}
此时我是完全没有 new 对象的哈,完全就是从 Spring 里面拿的,其他的主页使用方式也是一样的,改一下注解名字就行了。
b)方法处理方式
方法的话主要是使用 @Bean
来声明,代码如下,为了能更直观的看到,要稍作修改代码:
@Service
public class BlogTest {
@Bean
public String test() {
return "BlogTest Run...";
}
}
@SpringBootApplication
public class BeanTestApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
String s = (String) context.getBean("test");
System.out.println(s);
}
}
运行结果如图:,除了这种方法,我们还可以使用上面那种找类的方式,直接在 Spring 里面找哪个方法是返回 String 就行了,代码如下:
@SpringBootApplication
public class BeanTestApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
String s = context.getBean(String.class);
System.out.println(s);
}
}
可是这种方式,在我们定义多个返回值相同类型的时候就会出问题,比如这样:
@Service
public class BlogTest {
@Bean
public String test1() {
return "BlogTest1 Run...";
}
@Bean
public String test2() {
return "BlogTest2 Run...";
}
}
这是错误日志:
它会说找到了两个合适的 bean,分别是 test1 和 test2,这就起冲突了,这时候我们就可以指定找 bean 的名字了,可以直接就是方法名,也可以自己重新定义一个,然后使用重新定义的名字找,像上面的 (String) context.getBean("test");
这种就是根据方法名去找的,这里就不过多演示,重命名是这样的:
@Service
public class BlogTest {
@Bean(name = "t1")// 这是正常写
public String test1() {
return "BlogTest1 Run...";
}
@Bean("t2")// 这是简写
public String test2() {
return "BlogTest2 Run...";
}
}
@SpringBootApplication
public class BeanTestApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
String s1 = (String) context.getBean("t1");
System.out.println(s1);
String s2 = (String) context.getBean("t2");
System.out.println(s2);
}
}
运行结果:
但是有个问题,在通过在把方法写成重载之后,好像每次找到的都是参数最多的那一个(通过本人实验得知,不一定正确,可能和环境有关),代码如下:
@Service
public class BlogTest {
// 先交给 Spring 一些参数,到时候好拿
@Bean
public String name1() {
return "name1";
}
@Bean
public String name2() {
return "name2";
}
@Bean
public String name3() {
return "name3";
}
@Bean
public String name4() {
return "name4";
}
@Bean
public String name5() {
return "name5";
}
@Bean
public String test() {
return "BlogTest1 Run...";
}
@Bean
public String test(String name1) {
return "BlogTest2 Run...";
}
@Bean
public String test(String name1, String name2) {
return "BlogTest3 Run...";
}
@Bean
public String test(String name1,String name2, String name3) {
return "BlogTest4 Run...";
}
@Bean
public String test(String name1, String name2, String name3, String name4) {
return "BlogTest5 Run...";
}
@Bean
public String test(String name1, String name2, String name3, String name4, String name5) {
return "BlogTest6 Run...";
}
}
这是运行结果,,不管我如何更换这几个重载方法的位置,每次都是这个结果。
4.2、扫描路径
前面我们说到了使用五大注解和 Bean 去给 Spring 声明,那问题来了,只要声明了,那就一定会生效吗?答案是不一定,得看它能不能扫描得到,我们可以通过修改项目工程的目录结构,来测试 bean 对象是否生效:
当我把这个类移动到这个包里面时,再次运行就变成了这样:
这样它就找不到了,为什么没有找到 bean 对象呢?使用五大注解声明的 bean,要想生效,还需要配置扫描路径,让 Spring 扫描到这些注解,也就是通过 @ComponentScan
来配置扫描路径。配置你要在哪个路径下扫描,如下:
@SpringBootApplication
@ComponentScan({"com.zmbdp.beantest"})
public class BeanTestApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(BeanTestApplication.class, args);
String s = (String) context.getBean("test");
System.out.println(s);
}
}
这样就运行成功了。
4.3、如何取出来
存钱现在讲完了,那我们该如何取钱呢?总不能每次都依赖 ApplicationContext
进行扫描吧?那也太麻烦了,这里我就主要介绍通过注解得方式注入依赖,这个注解是 @Autowired
,代码如下:
@Data// 这个注解会自动帮我们写好 get、set 和 toString 方法
@Configuration
public class UserInfo {
private Integer id;
private String name;
}
@RestController
@RequestMapping("/blogTest")
public class BlogTest {
@Autowired
private UserInfo userInfo;
@RequestMapping("/setUser")
public String setUser() {
userInfo.setId(19);
userInfo.setName("zhangsan");
return userInfo.toString();
}
}
然后,我们访问这个页面时:,这就是依赖注入。但是当多个 bean 相同时,也会存在非唯一 bean 的问题,这时候我们只需要在 @Autowired
上加个 @Qualifier("")
// 在 "" 中指定 bean 的名称就可以了。
五、bean 的命名
5.1、五大注解存储 bean
- 前两位字目均为大写, bean 名称为类名
- 其他的为类名首字母小写
- Controller(value = "user")
5.2、@Bean 注解存储 bean
- bean 名称为方法名
- 通过 name 属性设置
@Bean(name = {"", ""})
(可简写)
六、总结
通过本文的详细讲解,我们深入理解了 Spring 框架中 IoC 和 DI 的核心概念,并且通过多个实例,展示了如何在实际开发中应用这些概念。IoC(控制反转)通过将对象的创建和管理职责交给 Spring IoC 容器,使代码更加模块化和可维护。而 DI(依赖注入)则是实现 IoC 的一种具体方式,依赖注入将对象的依赖关系从容器中取出,大大降低了类与类之间的耦合度。
在实际应用中,IoC 和 DI 的结合不仅简化了对象的管理,还为系统架构的可扩展性和模块化提供了强有力的支持。Spring 提供了多种配置方式,如 XML 配置、注解配置和 Java 配置类,灵活地满足了不同开发需求。通过对比传统的对象创建与管理方式,我们可以清晰地看到,采用 IoC 和 DI 后,代码的灵活性和可测试性得到了极大的提升。
七、结语
Spring 框架中的 IoC 和 DI 为现代软件开发注入了新的活力。它们不仅使开发者能够专注于业务逻辑的实现,更通过简化依赖管理和提升代码的可维护性,奠定了系统架构的稳固基础。通过这篇文章,我们不仅深入了解了控制反转与依赖注入的概念和实现,还探索了它们在实际项目中的应用场景。
在未来的开发之路上,希望你能将 IoC 和 DI 的理念融入到日常的编码实践中,打造出更加灵活、高效的应用程序。记住,技术的学习不仅仅是知识的积累,更是思维的拓展和创新的探索。愿你在代码的世界中不断进步,成为技术领域的佼佼者,让每一行代码都闪耀智慧的光芒。勇敢追求技术梦想,未来属于不断创新的你!