目录
[一. IoC & DI 入门](#一. IoC & DI 入门)
[1. 重谈Spring](#1. 重谈Spring)
[2. 容器](#2. 容器)
[3. IoC](#3. IoC)
[① 传统程序开发](#① 传统程序开发)
[② IoC 程序开发](#② IoC 程序开发)
[③ IoC 的优势](#③ IoC 的优势)
[4. DI](#4. DI)
[3. IoC & DI 使用](#3. IoC & DI 使用)
[二. IoC & DI 详解](#二. IoC & DI 详解)
[1. Bean的存储](#1. Bean的存储)
[2. Bean的重命名](#2. Bean的重命名)
[3. 扫描路径](#3. 扫描路径)
[三. DI 详解](#三. DI 详解)
[1. 属性注入](#1. 属性注入)
[2. 构造方法注入](#2. 构造方法注入)
[3. Setter注入](#3. Setter注入)
[4. 三种注入方式的优缺点分析](#4. 三种注入方式的优缺点分析)
[(1) 属性注入](#(1) 属性注入)
[(2) 构造方法注入](#(2) 构造方法注入)
[(3) Setter注入](#(3) Setter注入)
一. IoC & DI 入门
1. 重谈Spring
我们知道 Spring 是一个功能强大的开源框架. 这是从Spring 的用途和功能角度来谈的.
而从另一个角度来说, Spring就是一个包含了众多工具和方法的IoC容器.
2. 容器
什么是容器? --> 容器就是用来容纳某种物品的装置.
生活中的容器是用来容纳某种物品的装置. 在Java开发中, 也是类似的: 容器也是用来容纳某些东西的. 我们可以回想以前学过的容器:
- 集合框架中:List, Map --> 用来存储数据的容器.
- SpringMVC中: Tomcat --> 是一个 Servlet 容器, 也常被称为 Java Web 容器.
3. IoC
IoC 是Spring 的核心思想. 那么IoC究竟是什么呢?
其实前面我们已经使用过IoC, 在类上面加上 @RestController 和 @Controller 注解, 就是把这个类的对象交给Spring管理. 在Spring启动时, 就会自动加载该类.
所以说: 把对象交给Spring管理, 就是IoC思想.
详细的说: IoC (Inversion Of Control) 即 "控制反转". 说Spring是一个IoC容器, 也就是说他是一个"控制反转"容器. 那么, 何为控制反转? --> 控制反转, 也就是对象的控制权反转 . ++在传统的开发中, 如果我们需要某个类的对象, 需要自己通过new来创建. 而使用Spring之后 则不需要再手动进行创建了++. 我们把创建对象的任务交给容器, 程序中只需要进行依赖注入(DI "Dependency Injection") 就可以了. 这个容器就是IoC容器. Spring就是一个IoC容器.
- 下面我们通过第一个例子来介绍一下何为IoC:
假设我们现在要造一辆车: 先设计轮子(Tire), 然后根据轮子设计底盘(Bottom), 接着根据底盘设计车身(Framework), 最后根据车身设计好整个汽车(Car). 这里就产生了一个"依赖"关系: 汽车依赖车身, 车身依赖底盘, 底盘依赖轮子.
① 传统程序开发
java
package org.example.iocdemo;
public class NewCarExample {
public static void main(String[] args) {
Car car = new Car();
car.run();
}
}
public class Car {
private Framework framework;
public Car() {
framework = new Framework();
System.out.println("Car init...");
}
public void run() {
System.out.println("Car running...");
}
}
public class Framework {
private Bottom bottom;
public Framework() {
bottom = new Bottom();
System.out.println("Framework init...");
}
}
public class Bottom {
private Tire tire;
public Bottom() {
tire = new Tire();
System.out.println("Bottom init...");
}
}
public class Tire {
private int size;
public Tire() {
size = 17;
System.out.println("轮胎尺寸" + size);
}
}
上述代码看起来是没有问题的, 但是可维护性非常低. 如果我们后续对轮胎尺寸有修改的需求, 就需要把size设为参数, 那么相关的依赖类也需要跟着改动(加上参数). 修改后的代码如下所示:
java
package org.example.iocdemo;
public class NewCarExample {
public static void main(String[] args) {
Car car = new Car(13);
car.run();
}
}
public 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 running...");
}
}
public class Framework {
private Bottom bottom;
public Framework(int size) {
bottom = new Bottom(size);
System.out.println("Framework init...");
}
}
public class Bottom {
private Tire tire;
public Bottom(int size) {
tire = new Tire(size);
System.out.println("Bottom init...");
}
}
public class Tire {
private int size;
public Tire(int size) {
this.size = size;
System.out.println("轮胎尺寸" + size);
}
}
从上述代码可以看出, 使用传统开发方式的最大问题就是: 当最底层代码改动之后, 整个程序调用链上所有的代码都需要改动. (程序的耦合度非常高 --> 修改一处代码, 其他很多地方都要跟着修改).
解决方案:
在上面的程序中, 我们是根据轮子设计的底盘; 轮子一改, 依赖轮子的底盘就得改; 底盘一改, 依赖底盘的车身就得改; 车身一改, 依赖车身的车就得改. 也就是说, 整个设计全部需要改动 (牵一发而动全身).
那么, 我们可以尝试换一种思路:
我们先设计出汽车大概的样子, 然后: 根据汽车设计车身, 根据车身设计底盘, 根据底盘设计轮子. 这样一来, 依赖关系就倒置过来了: 轮胎依赖底盘, 底盘依赖车身, 车身依赖汽车.
那如何实现这样的思路呢?
如果我们还是在当前类中创建自己的下级类对象, 那么当下级类发生改变时, 当前类也要跟着修改. 所以我们不采用这样的方式.
我们把原来由自己创建的下级类改为传递的方式(也就是"注入"). 这种方式下, 因为我们没有在当前类中创建下级类对象, 所以即使下级类发生变化, 当前类也不需要做任何的修改. 这样一来就完成了程序的解耦合.
② IoC 程序开发
基于上面说的思路, 我们把程序修改一下, 把创建子类的方式改为注入传递的方式. 具体代码实现如下:
java
package org.example.iocdemo;
public class NewCarExample {
public static void main(String[] args) {
Tire tire = new Tire(22);
Bottom bottom = new Bottom(tire);
Framework framework = new Framework(bottom);
Car car = new Car(framework);
car.run();
}
}
public class Car {
private Framework framework;
public Car(Framework framework) {
this.framework = framework;
System.out.println("car init...");
}
public void run() {
System.out.println("Car running");
}
}
public class Framework {
private Bottom bottom;
public Framework(Bottom bottom) {
this.bottom = bottom;
System.out.println("Framework init...");
}
}
public class Bottom {
private Tire tire;
public Bottom(Tire tire) {
this.tire = tire;
System.out.println("Bottom init...");
}
}
public class Tire {
private int size;
public Tire(int size) {
this.size = size;
System.out.println("轮胎尺寸" + size);
}
}
经过上述调整, 无论底层类如何变化, 整个调用链的代码都是不用做任何改变的. 这样就完成了代码之间的 "解耦", 实现了更加灵活, 通用的程序设计方式.
③ IoC 的优势
传统代码中, 对象创建的顺序是: Car --> Framework --> Bottom --> Tire
改进之后的"解耦"代码中, 对象的创建顺序是: Tire --> Bottom --> Framework --> Car
我们发现一个规律: 改进之后的代码, 类的创建顺序是反的. 传统代码是 ++Car控制并创建了Framework, Framework控制并创建了Bottom, Bottom控制并创建了Tire.++ 但是改进之后, 控制权发生了反转. 不再是使用方控制并创建依赖对象了, 而是把依赖对象注入当前对象中. 这样的话, 即使依赖类发生任何改变, 当前类都是不受影响的.
学到这里, 我们大致清楚了什么是控制反转. 那么什么是控制反转容器呢? --> 就是IoC容器.
上图这一部分代码就是IoC容器做的工作.
从上面也可以看出来, IoC容器具备以下优点: 资源不由使用资源的双方管理, 而是由不使用资源的第三方管理.
这样可以带来很多好处
- 第一: 资源集中管理, 实现资源的可配置和易管理. (IoC容器会帮我们管理一些资源(对象等), 我们需要使用时直接去IoC容器中去取就可以了)
- 第二: 降低了使用资源双方的依赖程度, 也就是我们说的耦合度. (降低耦合度).
4. DI
DI, 即 Dependency Injection ("依赖注入"). 容器在运行期间, 动态地为应用程序提供运行时所依赖的资源, 称为"依赖注入".
上述代码中, 就是通过构造函数的方式, 把++依赖对象++ 注入到需要使用的对象中的.
IoC 是一种思想或者说是"目标", 而 DI 则是具体的实现. 所以也可以说, DI 是 IoC 的一种实现.
3. IoC & DI 使用
既然 Spring 是一个 IoC 容器. 那么作为容器, 它就有两个最基本的功能: "存" 和 "取".
Spring 容器管理的是对象. 这些对象, 我们称之为**"Bean". 我们把这些对象交给Spring管理, 由Spring 来负责对象的创建和销毁**. 而我们程序需要做的就是告诉Spring哪些需要存, 以及如何从 Spring 中取出对象.
使用 @Component注解 将类交给Spring管理, 使用 @Autowired注解 实现依赖注入.
我们下面以图书管理系统为例说明:
- 把Data类交给Spring管理
java
@Component
public class Data {
public List<BookInfo> mockData(){
List<BookInfo> list2 = new ArrayList<>();
for (int i = 0; i < 5; i++) {
BookInfo bookInfo = new BookInfo();
bookInfo.setId(i);
bookInfo.setBookName("Java编程思想"+i);
bookInfo.setCount(1);
bookInfo.setPublish("机械工业出版社");
bookInfo.setPrice(new Random().nextInt(100));
bookInfo.setAuthor("高斯林");
bookInfo.setStatus(1);
list2.add(bookInfo);
}
return list2;
}
}
- 把BookService交给Spring管理
java
@Component
public class BookService {
public List<BookInfo> getList(){
Data data = new Data();
List<BookInfo> list = data.mockData();
for (BookInfo bookInfo:list){
if (bookInfo.status == 1){
bookInfo.setStatusCN("可借阅");
}else{
bookInfo.setStatusCN("不可借阅");
}
}
return list;
}
}
- 删除创建Data对象的代码, 使用 @Autowired 进行注入.
java
@Component
public class BookService {
@Autowired // 依赖注入(Data对象注入Service)
public Data data;
public List<BookInfo> getList(){
List<BookInfo> list = data.mockData();
for (BookInfo bookInfo:list){
if (bookInfo.status == 1){
bookInfo.setStatusCN("可借阅");
}else{
bookInfo.setStatusCN("不可借阅");
}
}
return list;
}
}
- 删除创建BookService的代码, 使用 @Autowired 进行注入.
java
@RequestMapping("/book")
@RestController
public class BookController {
@Autowired // 依赖注入 (Service对象注入Controller)
public BookService bookService;
@RequestMapping("/getList")
public List<BookInfo> getList(){
List<BookInfo> bookInfos = new ArrayList<>();
bookInfos = bookService.getList();
return bookInfos;
}
}
二. IoC & DI 详解
在前面的图书管理系统案例中, 要把某个类的对象交给IoC容器管理, 就需要在类上添加 @Component 注解.
而 Spring框架 为了更好的服务web应用程序, 还提供了更加丰富的注解.
1. Bean的存储
共有两类注解类型可以实现:
① 类注解: @Controller @Service @Repository @Component @Configuration
② 方法注解: @Bean
@Controller 用于将接口层代码交给Spring管理.
@Service 用于将逻辑层代码交给Spring管理.
@Repository 用于将持久层代码交给Spring管理.
@Configuration 用于将配置信息代码交给Spring管理.
使用实例:
java
@Controller
public class SayHiController {
public void sayHi(){
System.out.println("hi,Spring");
}
}
@Service
public class UserService {
public void sayHi(){
System.out.println("hi,UserService");
}
}
@Repository
public class UserRepository {
public void sayHi(){
System.out.println("Hi,UserRepository");
}
}
这几个注解本质作用都是一样的, 都是将当前类对象交给Spring管理, 只是名字不一样. 这样做是方便程序员一眼看到注解名就能知道当前注解在那一层发挥作用.
@Bean 是方法注解, 作用也是将对象交给Spring管理. 但是方法注解需要搭配类注解来使用, 下面我们来看一个实例:
java
@Component
public class BeanConfig {
@Bean
public User user(){
User user = new User();
user.setAge(18);
user.setName("zhangsan");
return user;
}
@Bean
public User user1(){
User user1 = new User();
user1.setName("lisi");
user1.setAge(18);
return user1;
}
}
2. Bean的重命名
我们可以通过设置name属性给Bean对象进行重命名:
java
@Component
public class BeanConfig {
@Bean(name = "u")
public User user(){
User user = new User();
user.setAge(18);
user.setName("zhangsan");
return user;
}
@Bean(name = "u1")
public User user1(){
User user1 = new User();
user1.setName("lisi");
user1.setAge(18);
return user1;
}
}
一个Bean对象也可以有多个name:
java
@Component
public class BeanConfig {
@Bean(name = {"u","u0"})
public User user(){
User user = new User();
user.setAge(18);
user.setName("zhangsan");
return user;
}
}
3. 扫描路径
启动类默认的扫描路径是SpringBoot启动类所在包及其子包.
如果程序路径放错, 那么就会发生错误.
三. DI 详解
依赖注入是一个过程, 指的是IoC容器在创建Bean时, 提供运行时所依赖的资源, 而资源指的就是对象. 在前面程序案例中, 我们使用了 @Autowired注解 完成了依赖注入的操作.
一些文章中, 依赖注入也被称为 "对象注入", "属性装配" ...
Spring提供了依赖注入的三种方式:
① 属性注入 (Filed Injection)
② 构造方法注入 (Constructor Injection)
③ Setter注入 (Setter Injection)
1. 属性注入
属性注入是使用 @Autowired注解 实现的, 将 Service类 注入到 Controller类 中.
java
public class BookController {
@Autowired // 依赖注入 (Service对象注入Controller)
public BookService bookService;
@RequestMapping("/getList")
public List<BookInfo> getList(){
List<BookInfo> bookInfos = new ArrayList<>();
bookInfos = bookService.getList();
return bookInfos;
}
}
2. 构造方法注入
构造方法注入是在类的构造方法中实现注入.
具体操作就是在类的构造方法的上面加上 @Autowired注解. 如下代码所示:
java
@Controller
public class UserController2 {
public UserService2 userService2;
@Autowired //构造方法注入
public UserController2(UserService2 userService2) {
this.userService2 = userService2;
}
public void sayHi(){
userService2.sayHi();
System.out.println("Hi,UserController2");
}
}
**[注意]:**如果类只有⼀个构造方法, 那么 @Autowired注解 可以省略; 如果类中有多个构造方法,
那么需要添加上 @Autowired注解 来明确指定到底使用哪个构造方法.
3. Setter注入
Setter注入 和属性的Setter方法实现类似, 只不过在设置 set 方法的时候需要加上 @Autowired注
解. 即:
**在一个属性的Setter方法上面加上@Autowired实现注入.**如下代码所示:
java
@Controller
public class UserController2 {
public UserService2 userService2;
@Autowired // Setter注入
public void setUserService2(UserService2 userService2) {
this.userService2 = userService2;
}
public void sayHi(){
userService2.sayHi();
System.out.println("Hi,UserController2");
}
}
4. 三种注入方式的优缺点分析
(1) 属性注入
**优点:**简洁, 使用方便.
缺点: ① 只能用于IoC容器, 非IoC容器不可使用; ② 不能注入final修饰的属性.
(2) 构造方法注入
优点: ① 可以注入final修饰的属性; ② 注入的对象不会被修改; ③ 依赖对象在使用前一定会被完全初始化 (因为依赖是在类的构造方法中执行的, 而构造方法是在类加载阶段就会执行的方法); ④ 通用性好, 构造方法是JDK支持的, 所以更换任何框架, 他都是适用的.
缺点: 注入多个对象时, 代码会比较繁琐.
(3) Setter注入
优点: 方便类在实例之后, 重新对该对象进行配置或注入.
缺点: ① 不能注入final修饰的属性; ② 注入对象可能会被改变, 因为setter方法可能会被多次调用, 就有被修改的风险.