Spring IOC 控制反转
文章目录
- Spring IOC 控制反转
- 一、前言
- 什么是控制反转(IOC)
- 什么是依赖注入(DI)
- 二、介绍 IOC
- 2.1 传统思想代码
- 2.2 解决方案
- 2.3 IOC思想代码
- 2.4 IOC 使用(@Autowired依赖注入)
- 2.5 IOC 优势
- 三、IOC详解
- 3.1 从Spring容器中获取对象:Spring上下文
- 3.2 Bean的存储
- 3.2.1 为什么要这么多类注解?
- 3.3 方法注解@Bean
- 3.3.1 @Bean方法注解一定要配合类注解使用
- 3.3.2 @Bean方法定义多个对象
- 3.3.3 重命名@Bean
- 3.4 扫描路径
- 四、DI依赖注入
- 4.1 属性注入
- 4.2 构造方法注入
- 4.3 Setter注入
- 五、@Autowired存在问题
- 5.1 常见面试题:@Autowired与@Resource的区别
- 六、附加总结
一、前言
Spring 框架中的核心概念之一就是控制反转(Inversion of Control,IoC)。
IOC就是一种思想,而依赖注入(Dependency Injection, DI) 是控制反转的一种实现方式。
Spring本身是一个容器,存的是对象。对象这个词,在 Spring的范围内,称之为 Bean。
什么是控制反转(IOC)
控制反转(Inversion of Control,IoC)是一种设计原则,它将对象的创建和依赖关系的管理从程序代码中解耦出来,交由框架或容器进行处理。传统的编程方式中,应用程序代码主动创建和管理对象,而通过IoC,框架或容器负责对象的创建和管理,应用程序代码只需要声明依赖关系。转换对象控制权,让Spring帮我们管理或创建 bean。
什么是依赖注入(DI)
依赖注入(Dependency Injection,DI)是一种设计模式,它将对象所依赖的其他对象的创建和管理职责从对象自身剥离出来,通过外部容器(如Spring IoC容器)将所需的依赖对象注入到目标对象中,从而实现对象之间的解耦和提高代码的可维护性和可测试性。
二、介绍 IOC
下面将通过案例来分析什么是IOC
需求:造一辆车
2.1 传统思想代码
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);
}
}
}
从上述代码中可以看到,以上程序的问题是代码耦合性过高,导致修改底层代码后,需要调整整体的代码。
2.2 解决方案
利用IOC思想,控制反转。
具体操作是将原来由我们自己创建的下极类,改为传递的方式(也就是注入的方式)。
因为我们不需要在当前类中创建下极类了,所以下极类及时发生变化,当前类本身也无需修改任何代码,这样就完成了解耦合。
2.3 IOC思想代码
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);
}
}
}
2.4 IOC 使用(@Autowired依赖注入)
Spring 作为一个IOC容器帮我们管理对象,其主要功能就是 存 和 取 。
存:存的是对象bean,可以使用 @Component或者其他注解(下文中会讲到)
取:告诉 Spring ,从容器中取出这个对象,赋值给当前对象的属性。也就是依赖注入 使用注解 @Autowired
2.5 IOC 优势
传统开发中,对象创建的顺序是:Car -> Framework -> Bottom -> Tire
改进之后解耦代码的创建顺序是:Tire -> Bottom -> Framework -> Car
我们发现了一个规律,通用程序的实现代码,类的创建顺序是反的,传统代码是 Car 控制并创建了Framework,Framework 创建并创建了 Bottom,依次往下,而改进之后的控制权发生的反转,不再是使用方对象创建并控制依赖对象了 ,而是把依赖对象注入将当前对象中,依赖对象的控制权不再由当前类控制了。
这样的话,即使依赖类发生任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是 IoC 的实现思想。
上述改进后的程序main中的代码就是IOC容器需要存储的数据。
从上面也可以看出来, IoC容器具备以下优点:
资源不由使用资源的双方管理,而由不使用资源的第三方管理。
- 资源的集中管理:IOC会帮我们管理一些资源(对象等),需要的时候,直接去IOC中取即可。
- 在创建实例的时候不需要了解其中的细节,降低了使用资源双方的依赖程度,降低耦合度。
三、IOC详解
前面提到的IOC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。也就是存储bean。
3.1 从Spring容器中获取对象:Spring上下文
在学习如何存储对象之前,先来看如何从Spring容器中获取对象?
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//String上下文
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//获取到这个类的对象
context.getBean(Class<T> aClass);
//根据bean名称获取bean
context.getBean(String s);
}
}
获取Bean的三种常用方法
通过Bean名称来获取Bean,如果没有显式的提供名称(BeanId),Spring容器将为该bean生成唯一的名称。
Bean的命名约定:查看官方文档
其大致意思是,bean名称以小写字母开头,然后使用驼峰式大小写
比如:
类名:UserController,Bean的名称为:userController;
类名:AccountManager,Bean的名称为:accountManage;
也有特殊情况:
比如 :
类名:UController,Bean的名称为:UController;
类名:AController,Bean的名称为:AController;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//上下文
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//通过类名获取bean
UserController usercontroller1 = context.getBean(UserContorller.class);
//通过Bean名获取bean
UserController usercontroller2 = context.getBean(userController);
system.out.println(usercontroller1);
system.out.println(usercontroller2);
}
}
3.2 Bean的存储
在Spring中,要把某个对象交给IOC容器管理,需要在类上添加注解,下文中就会讲到Spring框架为服务web应用程序,提供了丰富的注解。
共有两类注解类型可以实现:
- 类注解:@Controller、@Service、@Repository、@Component、@Configuration
- 方法注解:@Bean
观察下面类注解的源代码,都是@component的衍生类,因此@Component的作用范围更广。
@Controller控制存储器
@Service服务存储
@Repository仓库存储
@Component组件存储
@Configuration配置存储
3.2.1 为什么要这么多类注解?
最直接的一个原因就是,可以让程序员看到类注解之后,就能直接了解当前类的用途。
- @Controller:控制层, 接收请求, 对请求进行处理, 并进行响应.
- @Servie:业务逻辑层, 处理具体的业务逻辑.
- @Repository:数据访问层,也称为持久层. 负责数据访问操作
- @Configuration:配置层. 处理项目中的一些配置信息
3.3 方法注解@Bean
五大注解只能加在类上,并且只能加在自己的代码上,如果我引入了一个第三方jar包,也希望交给Spring管理,是没有办法加五大注解。比如说:数据库操作,定义多个数据源
。
@Bean方法一定要配合类注解使用
使用@Bean注解时,bean的名称是方法名。
3.3.1 @Bean方法注解一定要配合类注解使用
在 Spring 框架的设计中,方法注解 要配合类注解才能将对象正常的存储到 Spring 容器中,下代码所示:
@Component
public class BeanConfig {
@Bean
public User user(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
}
3.3.2 @Bean方法定义多个对象
对于同一个类,如何定义多个对象呢?
比如说,多数据源的场景,类是同一个,但是配置不同,指向的数据源也不同。
@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;
}
}
此时,如果通过类型获取对象的话,Spring就会给我们报错,因为有两个对象,Spring不知道取哪个。接下来根据名称来获取bean对象。
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//根据bean名称, 从Spring上下文中获取对象
// User user1 = (User) context.getBean("user1");
User user2 = (User) context.getBean("user2");
System.out.println(user1);
System.out.println(user2);
}
}
3.3.3 重命名@Bean
@Bean(name = {"u1","user1"})
public User user1(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
3.4 扫描路径
SpringBoot 特点就是约定大于配置。其中之一的体现就是扫描路径。
默认扫描路径:启动类所在的目录及其子孙目录
如果更改启动类所在目录,而未进行路径的标注就会出现报错。
通过@ComponentScan()这个注解可以指定扫描路径。
@ComponentScan({"com.example.demo"})
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//从Spring上下文中获取对象
User u1 = (User) context.getBean("u1");
//使用对象
System.out.println(u1);
}
}
四、DI依赖注入
上面我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注入DI的细节。依赖注入是一个过程,是指IoC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象。在上面程序案例中,我们使用了@Autowired
这个注解,完成了依赖注入的操作。
关于依赖注入,Spring也给我们提供了三种方式:
- 属性注入
- 构造方法注入
- Setter注入
4.1 属性注入
属性注入是使用@Autowired
实现的。
下面是将Service类注入到Controller类中。
//Service类
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void sayHi() {
System.out.println("Hi,UserService");
}
}
@Controller
public class UserController {
//注入方法1: 属性注入
@Autowired
private UserService userService;
public void sayHi(){
System.out.println("hi,UserController...");
userService.sayHi();
}
}
4.2 构造方法注入
构造方法注入是在类的构造方法中实现注入,代码如下:
@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();
}
}
4.3 Setter注入
Setter注入和属性的Setter方法实现类似,只不过在设置 set 方法的时候需要加上@Autowired注解,代码如下:
@Controller
public class UserController3 {
//注入方法3: Setter方法注入
private UserService us;
@Autowired
public void setUS(UserService us) {
this.us = us;
}
public void sayHi(){
System.out.println("hi,UserController3...");
userService.sayHi();
}
}
五、@Autowired存在问题
当一个类存在多个bean时,使用@Autowored会存在问题
@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;
}
}
@Controller
public class UserController {
@Autowired
private UserService11 userService;
//此时注入的user,Spring不知道是user1还是user2
@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注解,来确定默认的实现.
@Component
public class BeanConfig {
//此时Spring默认的就是user1()
@Primary
@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;
}
}
使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。
@Controller
public class UserController
@Autowired
private UserService11 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的名称。
@Controller
public class UserController
@Autowired
private UserService11 userService;
@Resource(name = "user2")
private User user;
public void sayHi() {
System.out.println("hi,UserController...");
userService.sayHi();
System.out.println(user);
}
}
5.1 常见面试题:@Autowired与@Resource的区别
@Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
@Autowired 默认是按照类型注入,而@Resource是按照名称注入. 相比于@Autowired 来说,@Resource 支持更多的参数设置,例如 name 设置,根据名称获取 Bean。