Java EE 进阶:5. Spring IoCDI(第一弹):Spring IoC
目录
本节目标
1.IoC & DI 入门
1.1 Spring 是什么?
1.1.1 什么是容器?
1.1.2 什么是 IoC?
1.2 IoC 介绍
1.2.1 传统程序开发
1.2.2 问题分析
1.2.3 解决方案
1.2.4 IoC 程序开发
1.2.5 IoC 优势(课件内容)
1.3 DI 介绍
2. IoC & DI 使用
本节目标
1. 了解Spring,Spring MVC, Spring Boot 之间的联系及区别
2. 掌握IoC&DI的概念以及写法
1.IoC & DI 入门
在前面的章节中, 我们学习了Spring Boot和Spring MVC的开发, 可以完成一些基本功能的开发了, 但是什么是Spring呢? Spring, Spring Boot 和SpringMVC又有什么关系呢? 咱们还是带着问题去学习.
我们先看什么是SpringSpring 涉及到的面试题是非常少的,EE 进阶涉及到的内容非常少,其中 Spring 有两个重点:IoC 和 AOP
为啥 Spring 好用??Spring(Spring Core) 两大核心思想:IoC + AOP
现有 Spring Core 再有 Spring Boot,Spring Boot 已经给我们引进来了,这都是原生的Spring 核心👇
我们发现官方文档里也是包含了这些东西👇
之前的 Spring 都是指 Spring 家族,但是现在说的都是指 Spring Core
1.1 Spring 是什么?
通过前面的学习, 我们知道了Spring是一个开源框架, 他让我们的开发更加简单. 他支持广泛的应用场景, 有着活跃而庞大的社区, 这也是Spring能够长久不衰的原因
但是这个概念相对来说, 还是比较抽象.
我们用一句更具体的话来概括Spring, 那就是:Spring 是包含了众多工具方法的 IoC 容器
那问题来了,什么是容器?什么是 IoC 容器?接下来我们一起来看
1.1.1 什么是容器?
容器是用来容纳某种物品的(基本)装置。 ——来自:百度百科
生活中的水杯, 垃圾桶, 冰箱等等这些都是容器.
我们想想,之前课程我们接触的容器有哪些?
• List/Map -> 数据存储容器• Tomcat -> Web 容器
1.1.2 什么是 IoC?
IoC 是Spring的核心思想, 也是常见的面试题, 那什么是IoC呢?
其实IoC我们在前面已经使用了, 我们在前面讲到, 在类上面添加 @RestController 和
@Controller 注解, 就是把这个对象交给Spring管理, Spring 框架启动时就会加载该类. 把对象交给Spring管理, 就是IoC思想.IoC: Inversion of Control (控制反转), 也就是说 Spring 是一个"控制反转"的容器.
什么是控制反转呢? 也就是控制权反转. 什么的控制权发生了反转? 获得依赖对象的控制权被反转了之前:谁使用,控制权就在谁手上
现在:Spring 来控制
比如下面这段代码👇
public class BookService { public List<BookInfo> getList(){ BookDao bookDao=new BookDao(); //理应从数据库中查询图书信息,但是还未学习数据库相关操作,暂且mock数据 //mock:造假数据,用于自行测试 List<BookInfo> bookInfos=bookDao.mockData(); //注意,实际开发中可借阅这个东西应该交给前端做(有时候领导觉得需要改个名字啥的后端不好处理) //对结果进行二次处理 for(BookInfo bookInfo:bookInfos){ if(bookInfo.getStatus()==1){ bookInfo.setStatusCN("可借阅"); }else{ bookInfo.setStatusCN("不可借阅"); } } return bookInfos; } }随着调用 BookService 这个类,或者说调用 getList 这个方法,bookDao 就 出生 了
这个类或者这个方法不用了,bookDao 就 消亡 了
等咱们后续把这里改成 Spring 的写法就是 Spring 控制bookDao 的生死了~~
也就是说, 当需要某个对象时, 传统开发模式中需要自己通过 new 创建对象, 现在不需要再进行创建, 把创建对象的任务交给容器, 程序中只需要依赖注入 (Dependency Injection,DI)就可以了.
这个容器称为:IoC容器. Spring是一个IoC容器, 所以有时Spring 也称为Spring 容器.
控制反转是一种思想, 在生活中也是处处体现.
比如自动驾驶, 传统驾驶方式, 车辆的横向和纵向驾驶控制权由驾驶员来控制, 现在交给了驾驶自动化系统来控制, 这也是控制反转思想在生活中的实现.
比如招聘, 企业的员工招聘,入职, 解雇等控制权, 由老板转交给给HR(人力资源)来处理
1.2 IoC 介绍
接下来我们通过案例来了解一下什么是IoC
需求: 造一辆车
1.2.1 传统程序开发
我们的实现思路是这样的:
先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据底盘设计车身(Framework),最后根据车身设计好整个汽车(Car)。这里就出现了一个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子.
最终程序的实现代码如下: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("Tire init,size:"+size); } } }运行结果👇
你需要啥就 new 一个啥,控制权都在使用方手上
1.2.2 问题分析
这样的设计看起来没问题,但是可维护性却很低.
接下来需求有了变更: 那这个轮胎,有的人喜欢 17寸的,有的人喜欢 21寸的,那我们干脆提供个参数,你需要啥就自己传啥
那这个时候就要对上面的程序进行修改了,修改后的代码如下所示:
修改之后, 其他调用程序也会报错, 我们需要继续修改
完整代码如下:
public class Main { public static void main(String[] args) { Car car=new Car(21); car.run(); } } public class Car { private Framework framework; public Car(Integer size){ this.framework=new Framework(size); //有了车身,汽车才完成初始化 System.out.println("Car init..."); } public void run(){ System.out.println("Car run..."); } } public class Framework {//车身 public Framework(Integer size){ this.bottom=new Bottom(size); System.out.println("Framework init..."); } private Bottom bottom; } public class Bottom {//底盘 private Tire tire; public Bottom(Integer size){ this.tire=new Tire(size); System.out.println("Bottom init..."); } } public class Tire {//轮胎 int size; public Tire(Integer size){ this.size=size; System.out.println("Tire init,size:"+size); } }运行结果👇
那这时候用户又说了,我想要全身黑色的,还要轮胎带花纹的,那我们还得一层一层给改回去~~太TM烦了!!
现在我们想到了解决方法:这车我不造了,我外包出去,包给别人干,你爱要啥样的,我就从外包公司拿,你要啥样的,就给我传过来,我都不自己 new 了,全从外包公司拿,然后自己组装一下就行了~~
从以上代码可以看出,以上程序的问题是:当最底层代码改动之后,整个调用链上的所有代码都需要修改.
程序的耦合度非常高(修改一处代码, 影响其他处的代码修改)
1.2.3 解决方案
在上面的程序中, 我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改. 同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改, 也就是整个设计几乎都得改我们尝试换一种思路, 我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子. 这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身,车身依赖汽车
这就类似我们打造一辆完整的汽车, 如果所有的配件都是自己造,那么当客户需求发生改变的时候,比如轮胎的尺寸不再是原来的尺寸了,那我们要自己动手来改了,但如果我们是把轮胎外包出去,那么即使是轮胎的尺寸发生变变了,我们只需要向代理工厂下订单就行了,我们自身是不需要出力的如何来实现呢:
我们可以尝试不在每个类中自己创建下级类,如果自己创建下级类就会出现当下级类发生改变操作,自己也要跟着修改.
此时,我们只需要将原来由自己创建的下级类,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦.
1.2.4 IoC 程序开发
基于以上思路,我们把调用汽车的程序示例改造一下,把创建子类的方式,改为注入传递的方式.
具体实现代码如下:public class Main { public static void main(String[] args) { Tire tire=new Tire(17); 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 run..."); } } public class Framework {//车身 public Framework(Bottom bottom){ this.bottom=bottom; System.out.println("Framework init..."); } private Bottom bottom; } public class Bottom {//底盘 private Tire tire; public Bottom(Tire tire){ this.tire=tire; System.out.println("Bottom init..."); } } public class Tire {//轮胎 int size; String color; public Tire(Integer size){ this.size=size; this.color=color; System.out.println("Tire init,size:"+size); } }运行效果👇
这种做法就好比,我有什么要求我就交给外包公司就行了,我什么都不用改了,比如说用户想加个颜色,那么此时我们这里依然会有报错👇
但是这次报错的地方只有这一个了👇我们就没必要再一层一层改了👇
代码经过以上调整,无论底层类如何变化,整个调用链是不用做任何改变的,这样就完成了代码之间的解耦,从而实现了更加灵活、通用的程序设计了~~
那有人就说了,那这些方法不还是要写??
public class Main { public static void main(String[] args) { Tire tire=new Tire(17,"red"); Bottom bottom=new Bottom(tire); Framework framework=new Framework(bottom); Car car=new Car(framework); car.run(); } }当然不用写了,这些类我们都交给外包公司了,而 Spring 就是我们的外包公司,我们需要什么直接去 Spring 去取就好了,让 Spring 去帮我们管理这些对象~~
之前对象的控制权是这一大堆👇谁用谁来管
现在的对象控制权就在 Spring,让 Spring 帮我们做这些事情了,这就是 IoC~~
1.2.5 IoC 优势(课件内容)
在传统的代码中对象创建顺序是:Car -> Framework -> Bottom -> Tire
改进之后解耦的代码的对象创建顺序是:Tire -> Bottom -> Framework -> Car
我们发现了一个规律,通用程序的实现代码,类的创建顺序是反的,传统代码是 Car 控制并创建了 Framework,Framework 创建并创建了 Bottom,依次往下,而改进之后的控制权发生的反转,不再是使用方对象创建并控制依赖对象了,而是把依赖对象注入将当前对象中,依赖对象的控制权不再由当前类控制了.
这样的话, 即使依赖类发生任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是 IoC 的实现思想。学到这里, 我们大概就知道了什么是控制反转了, 那什么是控制反转容器呢, 也就是IoC容器
这部分代码, 就是IoC容器做的工作.
从上面也可以看出来, IoC容器具备以下优点:
资源不由使用资源的双方管理,而由不使用资源的第三方管理,这可以带来很多好处。第一,资源集中管理,实现资源的可配置和易管理。第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。
1. 资源集中管理: IoC容器会帮我们管理一些资源(对象等), 我们需要使用时, 只需要从IoC容器中去取就可以了
2. 我们在创建实例的时候不需要了解其中的细节, 降低了使用资源双方的依赖程度, 也就是耦合度.
Spring 就是一种IoC容器, 帮助我们来做了这些资源管理.
1.3 DI 介绍
上面学习了IoC, 什么是DI呢?
我们去创建对象的这个过程叫 IoC,但是对象创建完之后总要去用的,我拿来赋值给它,这个过程叫做“注入”,也就是 DI
DI: Dependency Injection(依赖注入)
容器在运行期间, 动态的为应用程序提供运行时所依赖的资源,称之为依赖注入。
程序运行时需要某个资源,此时容器就为其提供这个资源.从这点来看, 依赖注入(DI)和控制反转(IoC)是从不同的角度的描述的同一件事情,依赖注入是从应用程序的角度来描述, 就是指通过引入 IoC 容器,利用依赖关系注入的方式,实现对象之间的解耦.
上述代码中, 是通过构造函数的方式, 把依赖对象注入到需要使用的对象中的
IoC 是一种思想,也是"目标", 而思想只是一种指导原则,最终还是要有可行的落地方案,而 DI 就属于具体的实现。所以也可以说, DI 是IoC的一种实现.
比如说我今天心情比较好,吃一顿好的犒劳犒劳自己,那么"吃一顿好的"是思想和目标(是
IoC),但最后我是吃海底捞还是杨国福?这就是具体的实现,就是 DI。
2. IoC & DI 使用
对IoC和DI有了初步的了解, 我们接下来具体学习Spring 如何帮助我们实现 IoC 和 DI 的~~
闲聊:其实大家都猜出来了,就是使用“注解”,一个注解搞不定我就再来一个注解,还搞不定就再来一个,反正是能搞定的,我们的 Spring 就是各种注解~~
依然是先使用, 再学习在之前的代码中,我们知道 bookDao 这个对象是我们自己创建的,并不是 Spring 创建的
那我们如何让 Spring 帮我们创建呢??我们不知道,但我们已经在用了👇
这个 @Controller 就是告诉 Spring 帮我们创建一个对象~~我们之前学习的,你要是想调用一个类里的方法,就要先有一个对象才能调用它的方法,除非它是 static 方法(静态方法无需 new 对象即可调用),普通方法要想被调用必须要有对象,那么这个对象咋来的??我们从头到尾都没有 new UserController 👇
这个过程就是 Spring 帮我们创建了一个 UserController 这样的对象,就是通过 @Controller 这个注解创建的~~
如果我还想让它帮我创建 bookservice,还想让它帮我创建 bookDao,怎么创建呢??
我们来学习一个新的注解:@Component
我们的 Controller 表示 控制层(表示层),我们继续看 Controller 里面的实现👇
Controller 的实现依靠 Component,所以说真正起作用的其实是 @Component
既然 Spring 是一个 IoC(控制反转)容器,作为容器, 那么它就具备两个最基础的功能:
• 存
• 取
Spring 容器 管理的主要是对象, 这些对象, 我们称之为"Bean". 我们把这些对象交由Spring管理, 由 Spring来负责对象的创建和销毁. 我们程序只需要告诉Spring, 哪些需要存, 以及如何从Spring中取出对象
目标:把BookDao, BookService 交给Spring管理, 完成Controller层, Service层, Dao层的解耦
步骤:
1. Service层及Dao层的实现类,交给Spring管理: 使用注解: @Component
2. 在Controller层 和Service层 注入运行时依赖的对象: 使用注解 @Autowired我们只需要在需要创建对象的上面加上 @Controller,比如👇就可以从 Spring 的容器中去拿出 BookService 这样的对象了
如果说我还想拿出 BookService 里的 BookDao 对象,我不再去 new 了,也是相同的方法👇
此时 Spring 帮我们管理了这两个对象:BookService 和 BookDao,接下来我们学习怎么用~~
@RequestMapping("/book") @RestController public class BookController { //在此声明这个对象 private BookService bookService;//此时还未初始化,还是none @RequestMapping("/getList") public List<BookInfo> getList(){ //此时这个代码就可以不要了 // BookService bookService=new BookService(); return bookService.getList(); } }接下来我们在上面加上 @Autowired 这个注解,这个注解就表示:从 Spring 中获得对象,并且把值赋给它,此时 bookservice 就有值了,当然 BookDao 也是相同的操作
@RequestMapping("/book") @RestController public class BookController { @Autowired private BookService bookService;//此时还未初始化,还是none @RequestMapping("/getList") public List<BookInfo> getList(){ //此时这个代码就可以不要了 // BookService bookService=new BookService(); return bookService.getList(); } }@Component public class BookService { @Autowired private BookDao bookDao; public List<BookInfo> getList(){ BookDao bookDao=new BookDao();//此时这里就可以不要了 //理应从数据库中查询图书信息,但是还未学习数据库相关操作,暂且mock数据 //mock:造假数据,用于自行测试 List<BookInfo> bookInfos=bookDao.mockData(); //注意,实际开发中可借阅这个东西应该交给前端做(有时候领导觉得需要改个名字啥的后端不好处理) //对结果进行二次处理 for(BookInfo bookInfo:bookInfos){ if(bookInfo.getStatus()==1){ bookInfo.setStatusCN("可借阅"); }else{ bookInfo.setStatusCN("不可借阅"); } } return bookInfos; } }接下来我们来运行一下看看对不对👇
输入错误的👇
输入正确的👇
发现确实没问题,那么如果此时我们把 BookDao 的 @Component 去掉,看看是什么效果👇
我们发现服务启动都启动不起来👇
那可能是 @Autowired 出问题了,那就不加了呗👇
此时再来运行看一下👇
发现服务确实能启动起来,但是登录进去后👇
发现什么数据都没有,观察控制台发现此时服务响应的状态码是 500👇
500 是服务端错误,我们观察服务端的日志发现👇
发现 BookDao 值为 null,所以报了个空指针异常,为啥没有值??因为没有取出来,没有从 Spring 中取值,这就说明 @Autowired 是从 Spring 中拿东西,去找类型为 BookDao 这样的对象,找到之后赋值给它,当然这一切的大前提都是你要先交给 Spring 管理,这样 Spring 才能帮我们 new 对象
@Component:把 对象 交给 Spring 管理→存
@Autowired:从 Spring 中获取对象,并给属性赋值(不能加在类上)→取
答疑:
Q1:没有前端请求,怎么赋值??
这都是跟后端有关,跟前端没关系的~~
Q2:Spring 是不是使用无参构造函数创建的??
目前咱们涉及到的是这样的,但是它不一定,当然也会有有参的,与我们的写法有关,咱们在后面会慢慢涉及到~~
实现:(课件内容,跟上面一样)
1. 把BookDao 交给Spring管理, 由Spring来管理对象
import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Random; @Component public class BookDao { /** * 数据Mock 获取图书信息 * * @return */ public List<BookInfo> mockData() { List<BookInfo> books = new ArrayList<>(); for (int i = 0; i < 5; i++) { BookInfo book = new BookInfo(); book.setId(i); book.setBookName("书籍" + i); book.setAuthor("作者" + i); book.setCount(i * 5 + 3); book.setPrice(new BigDecimal(new Random().nextInt(100))); book.setPublish("出版社" + i); book.setStatus(1); books.add(book); } return books; } }2. 把BookService 交给Spring管理, 由Spring来管理对象
import org.springframework.stereotype.Component; import java.util.List; @Component public class BookService { private BookDao bookDao = new BookDao(); public List<BookInfo> getBookList() { List<BookInfo> books = bookDao.mockData(); for (BookInfo book : books) { if (book.getStatus() == 1) { book.setStatusCN("可借阅"); } else { book.setStatusCN("不可借阅"); } } return books; } }3. 删除创建BookDao的代码, 从Spring中获取对象
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; @Component public class BookService { @Autowired private BookDao bookDao; public List<BookInfo> getBookList() { List<BookInfo> books = bookDao.mockData(); for (BookInfo book : books) { if (book.getStatus() == 1) { book.setStatusCN("可借阅"); } else { book.setStatusCN("不可借阅"); } } return books; } }4. 删除创建BookService的代码, 从Spring中获取对象
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RequestMapping("/book") @RestController public class BookController { @Autowired private BookService bookService; @RequestMapping("/getList") public List<BookInfo> getList(){ //获取数据 List<BookInfo> books = bookService.getBookList(); return books; } }5. 重新运行程序, http://127.0.0.1:8080/book_list.html