目录
[1. 传统程序开发](#1. 传统程序开发)
[2. 问题分析](#2. 问题分析)
[3. 解决方案](#3. 解决方案)
[4. IoC程序开发](#4. IoC程序开发)
[5. IoC优势](#5. IoC优势)
[1. Bean的存储](#1. Bean的存储)
[1.1 @Controller(控制器存储)](#1.1 @Controller(控制器存储))
[1.2 @Service(服务存储)](#1.2 @Service(服务存储))
[1.3 @Repository(仓库存储)](#1.3 @Repository(仓库存储))
[1.4 @Component(组件存储)](#1.4 @Component(组件存储))
[1.5 @Configuration(配置存储)](#1.5 @Configuration(配置存储))
[2. 为什么需要这么多注解?](#2. 为什么需要这么多注解?)
[3. 方法注解@Bean](#3. 方法注解@Bean)
[3.1 方法注解要配合类注解使用](#3.1 方法注解要配合类注解使用)
[3.2 定义多个对象](#3.2 定义多个对象)
[3.3 重命名Bean](#3.3 重命名Bean)
[3.4 扫描路径](#3.4 扫描路径)
一、IoC概述
控制反转(Inversion of Control,IoC)是一种设计原则,用于解耦组件之间的依赖关系。传统编程中,调用者主动创建和管理依赖对象;而IoC将控制权交给外部容器或框架,由容器负责创建和注入依赖对象。
接下来我们通过案例来了解一下什么是IoC。
需求:造一辆车
1. 传统程序开发
我们的设计思路是这样的:
先设计轮子(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);
}
}
}
2. 问题分析
这样的设计看起来没问题,但是可维护性却很低。
如果接下来需求有了变更:随着对车的需求量越来越大,个性化需求也会越来越多,我们需要加工多种尺寸的轮胎。
那这个时候就需要对上面的程序进行修改,修改后的代码如下所示:

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



完整代码如下:
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);
}
}
}
从以上代码可以看出:当最底层代码改动之后,整个调用链上的所有代码都需要修改(程序的耦合度非常高)。
3. 解决方案
我们尝试换一种思路,我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘,底盘依赖车身,车身依赖汽车。

如何来实现呢:
我们可以尝试不在每个类中自己创建下级类,如果自己创建下级类就会出现当下级类发生改变操作,自己也要跟着修改。此时,我们只需要将原来由自己创建的下级类,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦。
4. 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);
}
}
}
代码经过以上调整,无论底层类如何变化,整个调用链是不用做任何改变的,这样就完成了代码之间的解耦,从而实现了更加灵活、通用的程序设计了。
5. IoC优势
- 降低耦合度:组件不直接依赖具体实现。
- 增强可测试性:便于模拟依赖进行单元测试。
- 提高可维护性:依赖关系集中管理,修改时影响小。
- 灵活性:运行时动态替换依赖实现。
Spring就是一种IoC容器,帮助我们来做了这些资源管理。
二、IoC详解
1. Bean的存储
Spring框架为了更好的服务web应用程序,提供了更丰富的注解。
共有两类注解类型可以实现:
- 类注解:@Controller、@Service、@Repository、@Component、@Configuration
- 方法注解:@Bean
1.1 @Controller(控制器存储)
使用@Controller存储bean的代码如下所示:
java
import org.springframework.stereotype.Controller;
@Controller
public class HelloController {
public void print() {
System.out.println("Hello control");
}
}
从Spring容器中获取这个对象:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
//从Spring上下文中获取对象
HelloController bean = context.getBean(HelloController.class);
//使用对象
bean.print();
}
}
ApplicationContext:Spring上下文
因为对象都交给Spring管理了,所以获取对象要从Spring中获取,那么就得下得到Spring的上下文。
观察运行结果,发现成功从Spring中获取到Controller对象,并执行Controller的print方法。

若把@Controller删掉,观察运行结果:

报错信息显示:找不到bean。
ApplicationContext也提供了其他获取bean的方式,ApplicationContext获取bean对象的功能,是父类BeanFactory提供的功能。
java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.beans.factory;
import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
public interface BeanFactory {
String FACTORY_BEAN_PREFIX = "&";
char FACTORY_BEAN_PREFIX_CHAR = '&';
//1
Object getBean(String name) throws BeansException;
//2
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
//3
Object getBean(String name, Object... args) throws BeansException;
//4
<T> T getBean(Class<T> requiredType) throws BeansException;
//5
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
//6
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
//7
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
boolean containsBean(String name);
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;
String[] getAliases(String name);
}
常用的是上述1,2,4种,这三种方式获取到的bean是一样的。其中1,2种都涉及到根据名称来获取对象,Bean的名称可以参考官方文档。

程序开发人员不需要为bean指定名称(Beanld),如果没有显式的提供名称(Beanld),Spring容器将为该bean生成唯一的名称。
命名约定使用Java标准约定作为实例字段名。也就是说,bean名称以小写字母开头,然后使用驼峰式大小写。
比如
类名:UserController,Bean的名称为:userController
类名:AccountManager,Bean的名称为:accountManager
类名:AccountService,Bean的名称为:accountService
也有一些特殊情况,当有多个字符并且第一个和第二个字符都是大写时,将保留原始的大小写。这些规则与java.beans.Introspector.decapitalize(Spring在这里使用的)定义的规则相同。
比如
类名:UController,Bean的名称为:UController
类名:AManager,Bean的名称为:AManager
根据这个命名规则,我们来获取bean。
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
HelloController bean = context.getBean(HelloController.class);
bean.print();
HelloController bean2 = (HelloController) context.getBean("helloController");
bean2.print();
HelloController bean3 = context.getBean("helloController", HelloController.class);
bean3.print();
System.out.println(bean);
System.out.println(bean2);
System.out.println(bean3);
}
}

三种方法都成功从Spring中获取到Controller对象,并执行Controller的print方法。并且地址一样,说明是同一个对象。
ApplicationContext VS BeanFactory
- 从继承关系和功能方面来说:Spring容器有两个顶级的接口:BeanFactory和ApplicationContext。其中BeanFactory提供了基础的访问容器的能力,而ApplicationContext属于BeanFactory的子类,它除了继承了BeanFactory的所有功能之外,它还拥有独特的特性,还添加了对国际化支持、资源访问支持、以及事件传播等方面的支持。
- 从性能方面来说:ApplicationContext是一次性加载并初始化所有的Bean对象,而BeanFactory是需要那个才去加载那个,因此更加轻量。(空间换时间)
1.2 @Service(服务存储)
使用@Service存储的代码如下所示:
java
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void print() {
System.out.println("Hello service");
}
}
读取bean的代码:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
UserService bean = context.getBean(UserService.class);
bean.print();
}
}

1.3 @Repository(仓库存储)
使用@Repository存储的代码如下所示:
java
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
public void print() {
System.out.println("Hello Repository");
}
}
读取bean的代码:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
UserRepository bean = context.getBean(UserRepository.class);
bean.print();
}
}

1.4 @Component(组件存储)
使用@Component存储bean的代码如下所示:
java
import org.springframework.stereotype.Component;
@Component
public class UserComponent {
public void print() {
System.out.println("Hello component");
}
}
读取bean的代码:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
UserComponent bean = context.getBean(UserComponent.class);
bean.print();
}
}

1.5 @Configuration(配置存储)
使用@Configuration存储bean的代码如下所示:
java
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
public void print() {
System.out.println("Hello configuration");
}
}
读取bean的代码:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
UserConfig bean = context.getBean(UserConfig.class);
bean.print();
}
}

2. 为什么需要这么多注解?
这个和应用分层是呼应的,让程序员看到类注解之后,就能直接了解当前类的用途。
- @Controller:控制层,接收请求,对请求进行处理,并进行响应
- @Servie:业务逻辑层,处理具体的业务逻辑
- @Repository:数据访问层,也称为持久层.负责数据访问操作
- @Configuration:配置层.处理项目中的一些配置信息
程序的应用分层,调用流程如下:

查看@Controller /@Service /@Repository /@configuration等注解的源码发现:

其实这些注解里面都有一个注解@Component,说明它们本身就是属于@Component 的"子类"。@Component是一个元注解,也就是说可以注解其他类注解,如@Controller,@Service,@Repository等。这些注解被称为@Component的衍生注解。
@Controller,@Service和@Repository用于更具体的用例(分别在控制层,业务逻辑层,持久化层),在开发过程中,如果你要在业务逻辑层使用@Component或@Service,显然@Service是更好的选择。
3. 方法注解@Bean
类注解是添加到某个类上的,但是存在两个问题:
- 使用外部包里的类,没办法添加类注解
- 一个类,需要多个对象,比如多个数据源
这种场景,我们就需要使用方法注解@Bean。
下来看方法注解如何使用,创建一个Student类:
java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Student {
private String name;
private Integer age;
}
java
public class StudentComponent {
@Bean
public Student student() {
return new Student("zhangsan", 18);
}
}
尝试获取Bean对象中的student:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
Student student = context.getBean(Student.class);
System.out.println(student);
}
}
但是未能成功获取。

3.1 方法注解要配合类注解使用
在Spring框架的设计中,方法注解@Bean要配合类注解才能将对象正常的存储到Spring容器中,
如下代码所示:
java
@Component
public class StudentComponent {
@Bean
public Student student() {
return new Student("zhangsan", 18);
}
}
再次运行:
3.2 定义多个对象
对于同一个类,如何定义多个对象呢?(比如多数据源的场景,类是同一个,但是配置不同,志向不同的数据源)
@Bean的使用:
java
@Component
public class StudentComponent {
@Bean
public Student student1() {
return new Student("zhangsan", 18);
}
@Bean
public Student student2 () {
return new Student("lisi", 20);
}
}
根据类型获取到的是哪个对象呢?
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
Student student = context.getBean(Student.class);
System.out.println(student);
}
}

报错信息显示:期望只有一个匹配,结果发现了两个。
从报错信息中,可以看出@Bean注解的bean,bean的名称就是它的方法名。
接下来我们根据名称来获取bean对象:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
//根据Bean名称,从Spring上下文获取对象
Student student1 = (Student) context.getBean("student1");
Student student2 = (Student) context.getBean("student2");
System.out.println(student1);
System.out.println(student2);
}
}

3.3 重命名Bean
可以通过设置name属性给Bean对象进行重命名操作,如下代码所示:
java
@Bean(name = {"s","stu"})
public Student student() {
return new Student("zhangsan", 18);
}
此时我们使用s或stu就可以获取到Student对象了:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
Student student1 = (Student) context.getBean("s");
Student student2 = (Student) context.getBean("stu");
System.out.println(student1);
System.out.println(student2);
}
}

"name="可以省略:
java@Bean({"s","stu"}) public Student student() { return new Student("zhangsan", 18); }
只有一个名称时,"{}"也可以省略:
java@Bean("s") public Student student() { return new Student("zhangsan", 18); }
3.4 扫描路径
下面我们先修改启动类的代码:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
Student bean = (Student) context.getBean("s");
System.out.println(bean);
}
}
再修改项目工程的目录结构,测试bean对象是否生效:


使用五大注解声明的bean,要想生效,还需要配置扫描路径,让Spring扫描到这些注解,也就是通过@ComponentScan来配置扫描路径。
java
@ComponentScan("com.bite.springiocdemo")
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
Student bean = (Student) context.getBean("s");
System.out.println(bean);
}
}

在之前的代码中,@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中了,默认扫描的范围是SpringBoot启动类所在包及其子包。在配置类上添加@ComponentScan注解,该注解默认会扫描该类所在的包下所有的配置类。
创作不易,给个三连支持一下吧