一、本章学习目标与知识路线
在学习 Spring IoC 和 DI 之前,我们已经接触过 Spring Boot、Spring MVC,并且使用它们完成了一些简单的前后端交互。
例如,我们之前的图书管理系统已经有了 Controller、Service、Dao 三层结构。
但是,还有一些问题需要解决:
-
Controller 中为什么可以直接使用 Service 对象?
-
Service 为什么可以直接使用 Dao 对象?
-
为什么有时候不需要自己使用
new创建对象? -
Spring 是如何知道我们需要哪个对象的?
-
如果容器中存在多个相同类型的对象,Spring 应该使用哪一个?
这些问题就是本章需要解决的。
二、Spring、Spring MVC、Spring Boot 的关系
spring是什么:
spring是一个开源的java开发框架,目的是简化应用程序开发的,并且提供了很多的功能,里面IoC就是一个核心的功能,并且也是这篇文章会具体讲解的
spring mvc是什么:
这个主要是spring的web开发中的一个开发框架
spring boot是什么:
是为了进一步简化spring项目开发而出现的快速框架,主要解决的是项目搭建和配置繁琐的问题
三、什么是 IoC?
就可以理解为是一个容器,IoC的中文翻译是控制反转,也就是原本由程序员控制的对象创建 和依赖关系管理工作,现在交给spring容器负责,这里我们来举一个造车的例子来看看(为了简化理解,我们就只关注底盘以及轮胎两者的关系)
传统程序开发:自己创建对象
在我们这个例子当中,底盘是依赖轮胎的,制造完整的底盘是需要轮胎的
Bottom:
java
public class Bottom {
private Tire tire;
public Bottom() {
tire=new Tire();
System.out.println("Bottom init...");
}
public void MadeOk(){
System.out.println("MadeOk...");
}
}
Tire:
java
public class Tire {
private int size;
public Tire() {
this.size = 17;
System.out.println("轮胎尺寸:" + size);
}
}
主方法:
java
public class NewCar {
public static void main(String[] args) {
Bottom bottom=new Bottom();
bottom.MadeOk();
}
}
IoC 的解决思路:将创建改成传递
Bottom:
java
public class Bottom {
private Tire tire;
public Bottom(Tire tire) {
this.tire = tire;
System.out.println("Bottom init...");
}
public void MadeOk(){
System.out.println("MadeOk...");
}
}
Tire:
java
public class Tire {
private int size;
public Tire(int size) {
this.size = size;
System.out.println("轮胎尺寸:" + size);
}
}
主方法:
java
public class IocCar {
public static void main(String[] args) {
Tire tire=new Tire(17);
Bottom bottom=new Bottom(tire);
bottom.MadeOk();
}
}
来对这两个方式做一个对比和总结,我们的形式从底盘自己创建轮胎到main方法统一创建轮胎,主要我们底盘就不需要再关系以及管理轮胎相关的东西,这样在创建底盘的时候,轮胎只是一个我们需要的东西并且由main创建好传递过来而已,原来的创建顺序是我先创建一个底盘,然后底盘需要轮胎,我们再来创建一个轮胎,现在是先把轮胎创建好,然后底盘需要这个依赖,然后我们将轮胎进行传递过去(这个跟控制反转也是呼应上了),这样有什么好处呢:一,资源集中管理,对象不再是每个创建者自己创建,而是可以交给一个统一的容器管理;第二:降低耦合度,当依赖对象发生变化的时候,使用它的类不再需要负责修改对象创建过程;当然上面只是用慢方法来模拟实现IoC的思想,真正的 Spring IoC 容器还会负责对象的创建、管理和依赖关系处理
四、DI 是什么?与 IoC 有什么区别?
DI 的英文全称是:
Dependency Injection,中文叫作依赖注入,什么是依赖注入:就是在对象运行时需要依赖某个依赖对象,由外部或者IoC容器将这个对象提供给它
比如:
java
public class Bottom {
private Tire tire;
public Bottom(Tire tire) {
this.tire = tire;
System.out.println("Bottom init...");
}
public void MadeOk(){
System.out.println("MadeOk...");
}
}
Bottom依赖Tire,我们就通过构造方法将Tire传递给它
IoC 和 DI 的区别
| 对比 | IoC | DI |
|---|---|---|
| 英文 | Inversion of Control | Dependency Injection |
| 中文 | 控制反转 | 依赖注入 |
| 本质 | 一种思想 | 一种具体实现方式 |
| 关注点 | 谁负责创建和管理对象 | 如何将依赖对象提供给使用者 |
所以可以理解为DI是IoC的一种具体的实现方式,因为依赖的注入,所以我们就跟刚刚上面讲到的一样进行了反转
五、Spring IoC 的实际使用:存储和获取 Bean
前面只是通过汽车案例理解 IoC 和 DI。
现在我们开始使用真正的 Spring IoC 容器。
Spring 容器最基本的两个功能:
-
存:将对象交给 Spring 管理。
-
取:从 Spring 容器中获取对象。
Bean 的存储方式:五大类注解与方法注解
Bean就是由Spring IoC 容器管理的对象,spring也提供了很多方式将对象交给IoC容器进行管理
第一类:类注解
-
@Controller(控制层)
-
@Service(业务逻辑层)
-
@Repository(数据访问层)
-
@Component(通用组件)
-
@Configuration(配置类)
你可以查看除了conponent之外的几个注解的源码,会发现他们都使用了@Component,所以这些都是@Component的衍生注解,那我们为什么不直接使用@Component就好了呢,当然这样也可以,毕竟都是交给spring来进行管理,但是这样不同的注解可以更加方便我们编写程序的时候知道这个类是负责什么的,使用的方式也很简单,直接加到类上面就可以了,我拿其中一个进行举例
java
import org.springframework.stereotype.Component;
@Component
public class UserComponent {
public void sayHi() {
System.out.println("Hi, UserComponent");
}
}
第二类:方法注解
- @Bean
前面的注解都是加在类上面的,但是有些时候会因为类注解不方便所以我们选择使用方法注解
@Bean 的基本使用
首先创建一个User类
java
public class User {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
然后创建配置类:
java
import com.example.springcaptchademo1.User.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfiguration {
@Bean({"u1", "user1"})
public User user1(){
User user=new User("zhangsan",18);
return user;
}
@Bean({"u2", "user2"})
public User user2(){
User user=new User("lisi",17);
return user;
}
}
这里是spring扫描到了UserConfiguration这个配置类,然后发现了其中的user1根user2方法使用了@Bean注解,然后将这两个方法返回的对象存在spring容器当中
那为什么@Bean需要配合类注解,如果这个类没有被spring扫描到,spring就不会知道这个方法的存在,也自然没办法将这个对象交给spring进行管理,所以得加上类注解,我们也可以在@Bean注解中添加name属性来对应方便的找到这个方法
六、Spring 如何获取 Bean?
ApplicationContext 是什么?
这个是spring的上下文,也是spring容器的一个重要的访问入口,这个东西在哪呢,这个就是我们在启动spring的时候SpringApplication.run(SpringCaptchaDemo1Application.class, args)的返回对象就是ApplicationContext的一个子类,我们就可以通过它来进行获取bean对象
java
@SpringBootApplication
public class SpringCaptchaDemo1Application {
public static void main(String[] args) {
ApplicationContext context =SpringApplication.run(SpringCaptchaDemo1Application.class, args);
Object user1 = context.getBean("user1");
System.out.println(user1);
Object user2 = context.getBean("user2");
System.out.println(user2);
}
}
获取方式
我们有三种方法,通过类,通过name,还有就是通过name+类,其中如果我们没有写name属性的话,spring会通过类名来生成name,类名是首字母大写的话会改成首字母小写其他不变,如果前两个都是大写的话就原模原样
多个对象可能会出现的问题
但是需要注意的是,当我们通过类来找对象的话,按照上面这个例子,就是使用User.class,因为这两个对象就是属于User类的,但是这个时候就会有问题了,因为我们有两个对象都是User类的,那spring应该去拿哪一个呢,这个时候就会报错,所以我们可以在加上一个name的属性来一起寻找,@Bean这个方法注解的作用就在这里体现了
在添加类注解的基础上还需要保证被扫描到
那现在我们加上了类注解之后,就一定能获取到这个类吗,不一定的还需要看spring扫描到这个类,也就是看启动类的位置在哪,就比如下面这个例子,我们的启动类就会扫描在com.example.springcaptchademo1这个包下面的所有子类中去找加了注解的,但是我们将这个启动类的位置乱放的话可能就会出现问题,那我们也可以通过@ComponentScan这个注解来手动的去找一个路径

七、DI 依赖注入的三种方式
当我们学习完如何将对象交给spring进行管理之后,我们还有一个问题:就是当我们的UserController类需要UserService类的时候,我们该怎么获取UserService
spring提供了三种常见的方式:属性注入,构造方法注入,setter注入
三种注入方法的使用
主要注解@Autowired
java
@Controller
public class UserController {
//属性注入
@Autowired
private UserService userService;
public void sayHi() {
System.out.println("Hi, UserController");
userService.sayHi();
}
//构造方法注入
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
public void sayHi() {
System.out.println("Hi, UserController");
userService.sayHi();
}
//setter注入
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void sayHi() {
System.out.println("Hi, UserController");
userService.sayHi();
}
}
三种注入方式的对比
| 对比 | 属性注入 | 构造方法注入 | Setter 注入 |
|---|---|---|---|
| 实现方式 | 属性上添加注解 | 构造方法传递 | Setter 方法注入 |
| 代码简洁性 | 简洁 | 参数较多时较繁琐 | 中等 |
| 能否注入 final | 不能 | 可以 | 不能 |
| 是否容易修改依赖 | 不适合直接修改 | 不容易修改 | 可以修改 |
| 适用情况 | 简单场景 | 依赖明确的场景 | 需要后续重新设置依赖的场景 |
为什么构造方法不需要注解但是setter需要,因为 UserController 已经加入容器了,但是它里面的 UserService 没有完成依赖注入,如果你没有注解在set方法上面,spring不会自动调用这个方法,因为创建UserController对象并不需要调用set方法,set方法只是这个对象中的一个普通的java方法而已,所以最后使用的时候还是会报空指针异常,但是构造方法就不一样了,因为构造方法是创建对象时必须经过的,所以就会从容器当中去找UserService
当有多个注入对象的时候
如果有多个注入对象,就跟上面一样有多个user的时候,我们依旧可以通过一个注解来进行寻找,就是@Qualifier(),里面依旧是通过名字来寻找,也可以在对应的Bean对象上面添加@Primary注解来进行优先选择那个对象
@Qualifier()
java
@Autowired
@Qualifier("user2")
private User user;
@Primary
java
@Configuration
public class BeanConfig {
@Primary
@Bean("user1")
public User user1() {
return new User();
}
@Bean("user2")
public User user2() {
return new User();
}
}
@Resource
java
@Controller
public class UserController {
@Resource(name = "user2")
private User user;
}
三种方式的区别
| 注解 | 作用 |
|---|---|
| @Primary | 指定默认优先使用的 Bean |
| @Qualifier | 配合 @Autowired,指定 Bean 名称 |
| @Resource | 通过 name 指定 Bean 名称 |
@Resource和**@Autowired**两者区别
| 对比 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 框架 | Java 标准注解体系 |
| 默认方式 | 按类型 | 按名称 |
| 指定名称 | 配合 @Qualifier | 使用 name 属性 |
| 使用方式 | 支持属性、构造方法、Setter 等注入 | 支持属性、Setter 等注入 |
八、将 IoC 和 DI 应用到图书管理系统
BookDao
java
package com.example.spring_book2_demo_fixed.dao;
import com.example.spring_book2_demo_fixed.entity.BookInfo;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.ArrayList;
import java.math.BigDecimal;
@Repository
public class BookDao {
public List<BookInfo> mockData() {
List<BookInfo> books = new ArrayList<>();
for (int i = 0; i < 10; i++) {
BookInfo book = new BookInfo();
book.setId(i + 1);
book.setBookName("书籍" + (i + 1));
book.setAuthor("作者" + (i + 1));
book.setCount(i * 5 + 3);
book.setPrice(BigDecimal.valueOf(20 + i * 10));
book.setPublish("出版社" + (i + 1));
book.setStatus(1);
books.add(book);
}
return books;
}
}
这里将BookDao通过注解交给spring来进行管理
BookService
java
package com.example.spring_book2_demo_fixed.service;
import com.example.spring_book2_demo_fixed.dao.BookDao;
import com.example.spring_book2_demo_fixed.entity.BookInfo;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class BookService {
// 1. 定义成员变量
private final BookDao bookDao;
// 2. 构造方法:进行依赖注入
public BookService(BookDao bookDao) {
this.bookDao = bookDao;
}
public List<BookInfo> getBookList() {
List<BookInfo> books = bookDao.mockData();
for (BookInfo book : books) {
if (Integer.valueOf(1).equals(book.getStatus())) {
book.setStatusCN("可借阅");
} else {
book.setStatusCN("不可借阅");
}
}
return books;
}
}
通过构造方法引入BookDao对象
BookController
java
package com.example.spring_book2_demo_fixed.controller;
import com.example.spring_book2_demo_fixed.entity.BookInfo;
import com.example.spring_book2_demo_fixed.service.BookService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/book")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/getList")
public List<BookInfo> getList() {
return bookService.getBookList();
}
}

最终效果还是实现出来的也就是成功了