1. IoC & DI ⼊⻔
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 是⼀个"控制反转"的容器.
什么是控制反转呢? 也就是控制权反转. 什么的控制权发⽣了反转? 获得依赖对象的过程被反转了
也就是说, 当需要某个对象时, 传统开发模式中需要⾃⼰通过 new 创建对象, 现在不需要再进⾏创
建, 把创建对象的任务交给容器, 程序中只需要依赖注⼊ (Dependency Injection,DI)就可以了.
这个容器称为:IoC容器. Spring是⼀个IoC容器, 所以有时Spring 也称为Spring 容器.
控制反转是⼀种思想, 在⽣活中也是处处体现.
⽐如⾃动驾驶, 传统驾驶⽅式, ⻋辆的横向和纵向驾驶控制权由驾驶员来控制, 现在交给了驾驶⾃
动化系统来控制, 这也是控制反转思想在⽣活中的实现.
⽐如招聘, 企业的员⼯招聘,⼊职, 解雇等控制权, 由⽼板转交给给HR(⼈⼒资源)来处理
1.2 IoC 介绍
接下来我们通过案例来了解⼀下什么是IoC
需求: 造⼀辆⻋
1.2.1 传统程序开发
我们的实现思路是这样的:
先设计轮⼦(Tire),然后根据轮⼦的⼤⼩设计底盘(Bottom),接着根据底盘设计⻋⾝(Framework),最 后根据⻋⾝设计好整个汽⻋(Car)。这⾥就出现了⼀个"依赖"关系:汽⻋依赖⻋⾝,⻋⾝依赖底盘,底 盘依赖轮⼦.
1.2.2 问题分析
这样的设计看起来没问题,但是可维护性却很低.
接下来需求有了变更: 随着对的⻋的需求量越来越⼤, 个性化需求也会越来越多,我们需要加⼯多种尺 ⼨的轮胎.
以上的问题是:当最底层代码之后,整个调⽤链上的所有东西都需要 修改.
程序的耦合度⾮常⾼(修改⼀处代码, 影响其他处的代码修改)
1.2.3 解决⽅案
在上⾯的程序中, 我们是根据轮⼦的尺⼨设计的底盘,轮⼦的尺⼨⼀改,底盘的设计就得修改. 同样因 为我们是根据底盘设计的⻋⾝,那么⻋⾝也得改,同理汽⻋设计也得改, 也就是整个设计⼏乎都得改 我们尝试换⼀种思路, 我们先设计汽⻋的⼤概样⼦,然后根据汽⻋的样⼦来设计⻋⾝,根据⻋⾝来设计 底盘,最后根据底盘来设计轮⼦. 这时候,依赖关系就倒置过来了:轮⼦依赖底盘, 底盘依赖⻋⾝, ⻋⾝依赖汽⻋
这就类似我们打造⼀辆完整的汽⻋, 如果所有的配件都是⾃⼰造,那么当客⼾需求发⽣改变的时候, ⽐如轮胎的尺⼨不再是原来的尺⼨了,那我们要⾃⼰动⼿来改了,但如果我们是把轮胎外包出去,那 么即使是轮胎的尺⼨发⽣变变了,我们只需要向代理⼯⼚下订单就⾏了,我们⾃⾝是不需要出⼒的.
那在代码上如何来实现呢:
我们可以尝试不在每个类中⾃⼰创建下级类,如果⾃⼰创建下级类就会出现当下级类发⽣改变操作, ⾃⼰也要跟着修改. 此时,我们只需要将原来由⾃⼰创建的下级类,改为传递的⽅式(也就是注⼊的⽅式),因为我们不 需要在当前类中创建下级类了,所以下级类即使发⽣变化(创建或减少参数),当前类本⾝也⽆需修 改任何代码,这样就完成了程序的解耦.
学到这⾥, 我们⼤概就知道了什么是控制反转了, 那什么是控制反转容器呢, 也就是IoC容器
这部分工作, 就是IoC容器做的⼯作
从上⾯也可以看出来, IoC容器具备以下优点:
资源不由使⽤资源的双⽅管理,⽽由不使⽤资源的第三⽅管理,这可以带来很多好处。第⼀,资源集 中管理,实现资源的可配置和易管理。第⼆,降低了使⽤资源双⽅的依赖程度,也就是我们说的耦合 度。
1.
资源集中管理: IoC容器会帮我们管理⼀些资源(对象等), 我们需要使⽤时, 只需要从IoC容器中去取 就可以了
2.
我们在创建实例的时候不需要了解其中的细节, 降低了使⽤资源双⽅的依赖程度, 也就是耦合度.
Spring 就是⼀种IoC容器, 帮助我们来做了这些资源管理.
1.3 DI 介绍
上⾯学习了IoC, 什么是DI呢?
DI: Dependency Injection(依赖注⼊)
容器在运⾏期间, 动态的为应⽤程序提供运⾏时所依赖的资源,称之为依赖注⼊。
程序运⾏时需要某个资源,此时容器就为其提供这个资源.
从这点来看, 依赖注⼊(DI)和控制反转(IoC)是从不同的⻆度的描述的同⼀件事情,就是指通过
引⼊ IoC 容器,利⽤依赖关系注⼊的⽅式,实现对象之间的解耦。
上述代码中, 是通过构造函数的⽅式, 把依赖对象注⼊到需要使⽤的对象中的
IoC 是⼀种思想,也是"⽬标", ⽽思想只是⼀种指导原则,最终还是要有可⾏的落地⽅案,⽽ DI 就属于 具体的实现。所以也可以说, DI 是IoC的⼀种实现.
⽐如说我今天⼼情⽐较好,吃⼀顿好的犒劳犒劳⾃⼰,那么"吃⼀顿好的"是思想和⽬标(是
IoC),但最后我是吃海底捞还是杨国福?这就是具体的实现,就是 DI。
2. IoC & DI 使⽤
对IoC和DI有了初步的了解, 我们接下来具体学习Spring IoC和DI的代码实现
既然 Spring 是⼀个 IoC(控制反转)容器,作为容器, 那么它就具备两个最基础的功能:
•
存
•
取
Spring 容器 管理的主要是对象, 这些对象, 我们称之为"Bean". 我们把这些对象交由Spring管理, 由
Spring来负责对象的创建和销毁. 我们程序只需要告诉Spring, 哪些需要存, 以及如何从Spring中取出 对象
⽬标: 把BookDao, BookService 交给Spring管理, 完成Controller层, Service层, Dao层的解耦
步骤:
1.
Service层及Dao层的实现类,交给Spring管理: 使⽤注解:
@Component
2.
在Controller层 和Service层 注⼊运⾏时依赖的对象: 使⽤注解
@Autowired
把BookService 交给Spring管理, 由Spring来管理对象
然后只要在使用对象的时候加上Autowired,后面的对象就不用再自己创建了,而是会自动从Ioc容器当中取。
从上面我们也可以稍微总结一下,什么类适合交给IoC容器去管理!
无状态的类适合交给IoC容器去管理(不是无参数,而是谁去调用的时候都是一样的,就可以称之为无状态的类)
3.1 Bean(对象)的存储
在之前的⼊⻔案例中,要把某个对象交给IOC容器管理,需要在类上添加⼀个注解:
@Component
⽽Spring框架为了更好的服务web应⽤程序, 提供了更丰富的注解.
共有两类注解类型可以实现:
1.
类注解:@Controller、@Service、@Repository、@Component、@Configuration.
2.
⽅法注解:@Bean.
接下来我们分别来看
3.1.1 @Controller(控制器存储)
使⽤ @Controller 存储 bean 的代码如下所⽰:
如何观察这个对象已经存在Spring容器当中了呢?
接下来我们学习如何从Spring容器中获取对象
ApplicationContext 翻译过来就是: Spring 上下⽂
因为对象都交给 Spring 管理了,所以获取对象要从 Spring 中获取,那么就得先得到 Spring 的上下 ⽂ 关于上下⽂的概念
上学时, 阅读理解经常会这样问: 根据上下⽂, 说⼀下你对XX的理解
在计算机领域, 上下⽂这个概念, 咱们最早是在学习线程时了解到过, ⽐如我们应⽤进⾏线程切换时
候,切换前都会把线程的状态信息暂时储存起来,这⾥的上下⽂就包括了当前线程的信息,等下该
线程⼜得到CPU时间的时候, 从上下⽂中拿到线程上次运⾏的信息
这个上下⽂, 就是指当前的运⾏环境, 也可以看作是⼀个容器, 容器⾥存了很多内容, 这些内容是当前 运⾏的环境
获取bean对象的其他⽅式
常⽤的是上述1,2,4种, 这三种⽅式,获取到的bean是⼀样的(主要是三个红字标明的方法)
其中1,2种都涉及到根据名称来获取对象. bean的名称是什么呢?
Spring bean是Spring框架在运⾏时管理的对象, Spring会给管理的对象起⼀个名字.
⽐如学校管理学⽣, 会给每个学⽣分配⼀个学号, 根据学号, 就可以找到对应的学⽣.
Spring也是如此, 给每个对象起⼀个名字, 根据Bean的名称(BeanId)就可以获取到对应的对象.
Bean 命名约定
我们看下官⽅⽂档的说明:
Bean Overview :: Spring Framework
程序开发⼈员不需要为bean指定名称(BeanId),
bean⽣成唯⼀的名称.
命名约定使⽤Java标准约定作为实例字段名. 也
⼤⼩写.
⽐如(小驼峰写法)
类名: UserController, Bean的名称为: userController
类名: AccountManager, Bean的名称为: accountManager
类名: AccountService, Bean的名称为: accountService
也有⼀些特殊情况, 当有多个字符并且第⼀个和第⼆个字符都是⼤写时, 将保留原始的⼤⼩写. 这些规则 与java.beans.Introspector.decapitalize (Spring在这⾥使⽤的)定义的规则相同.
⽐如
类名: UController, Bean的名称为: UController
类名: AManager, Bean的名称为: AManager
那么这三个bean是一样的吗?
我们可以先打印出来看看。
地址⼀样, 说明对象是⼀个
获取bean对象, 是⽗类BeanFactory提供的功能
ApplicationContext VS BeanFactory
•
继承关系和功能⽅⾯来说:Spring 容器有两个顶级的接⼝:BeanFactory 和
ApplicationContext。其中 BeanFactory 提供了基础的访问容器的能⼒,⽽
ApplicationContext 属于 BeanFactory 的⼦类,它除了继承了 BeanFactory 的所有功能之外,
它还拥有独特的特性,还添加了对国际化⽀持、资源访问⽀持、以及事件传播等⽅⾯的⽀持.
•
从性能⽅⾯来说:ApplicationContext 是⼀次性加载并初始化所有的 Bean 对象,⽽
BeanFactory 是需要那个才去加载那个,因此更加轻量. (空间换时间)
3.1.2 @Service(服务存储)
使⽤ @Service 存储 bean 的代码如下所⽰:
读取 bean 的代码:
还有几种方式都是类似的,不过多赘述,只是注解名字不一样。
3.1.3 @Repository(仓库存储)
3.1.4 @Component(组件存储)
3.1.5 @Configuration(配置存储)
3.2 为什么要这么多类注解?
这个也是和咱们前⾯讲的应⽤分层是呼应的. 让程序员看到类注解之后,就能直接了解当前类的⽤途.
•
@Controller:控制层, 接收请求, 对请求进⾏处理, 并进⾏响应.
•
@Servie:业务逻辑层, 处理具体的业务逻辑.
•
@Repository:数据访问层,也称为持久层. 负责数据访问操作
•
@Configuration:配置层. 处理项⽬中的⼀些配置信息.
程序的应⽤分层,调⽤流程如下:
类注解之间的关系
其实这些注解⾥⾯都有⼀个注解
@Component
,说明它们本⾝就是属于
@Component
的"⼦类"
@Component
是⼀个元注解,也就是说可以注解其他类注解,如
@Controller
,
@Service
,
@Repository
等. 这些注解被称为
@Component
的衍⽣注解.
@Controller
,
@Service
和
@Repository
⽤于更具体的⽤例(分别在控制层, 业务逻辑层, 持
久化层), 在开发过程中, 如果你要在业务逻辑层使⽤
@Component
或@Service,显然@Service是更 好的选择
3.3 ⽅法注解 @Bean
类注解是添加到某个类上的, 但是存在两个问题:
1.
使⽤外部包⾥的类, 没办法添加类注解
2.
⼀个类, 需要多个对象, ⽐如多个数据源
这种场景, 我们就需要使⽤⽅法注解
@Bean
然⽽,当我们写完以上代码,尝试获取 bean 对象中的 user 时却发现,根本获取不到:
这是为什么呢?
3.3.1 ⽅法注解要配合类注解使⽤
在 Spring 框架的设计中,⽅法注解
@Bean
要配合类注解才能将对象正常的存储到 Spring 容器中, 如下代码所⽰:
3.3.2 定义多个对象
定义了多个对象的话, 我们根据类型获取对象, 获取的是哪个对象呢?
报错信息显⽰: 期望只有⼀个匹配, 结果发现了两个, user1, user2
从报错信息中, 可以看出来, @Bean 注解的bean, bean的名称就是它的⽅法名
接下来我们根据名称来获取bean对象
3.3.3 重命名 Bean
可以通过设置 name 属性给 Bean 对象进⾏重命名操作,如下代码所⽰:
此时我们使⽤ u1 就可以获取到 User 对象了
3.4 扫描路径
Q: 使⽤前⾯学习的四个注解声明的bean,⼀定会⽣效吗?
A: 不⼀定(原因:bean想要⽣效,还需要被Spring扫描)
下⾯我们通过修改项⽬⼯程的⽬录结构,来测试bean对象是否⽣效:
解释: 没有bean的名称为u1
为什么没有找到bean对象呢?
使⽤五⼤注解声明的bean,要想⽣效, 还需要配置扫描路径, 让Spring扫描到这些注解
也就是通过
@ComponentScan
来配置扫描路径
{} ⾥可以配置多个包路径
这种做法仅做了解, 不做推荐使⽤
那为什么前⾯没有配置 @ComponentScan注解也可以呢?
@ComponentScan
注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解
@SpringBootApplication
中了
默认扫描的范围是SpringBoot启动类所在包及其⼦包
在配置类上添加 @ComponentScan 注解, 该注解默认会扫描该类所在的包下所有的配置类
推荐做法:
把启动类放在我们希望扫描的包的路径下, 这样我们定义的bean就都可以被扫描到
4. DI 详解
上⾯我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注⼊DI的细节。
依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象. 在上⾯程序案例中,我们使⽤了 @Autowired
这个注解,完成了依赖注⼊的操作.
简单来说, 就是把对象取出来放到某个类的属性中.
在⼀些⽂章中, 依赖注⼊也被称之为 "对象注⼊", "属性装配", 具体含义需要结合⽂章的上下⽂来理解
关于依赖注⼊, Spring也给我们提供了三种⽅式:
1.
属性注⼊(Field Injection)
2.
构造⽅法注⼊(Constructor Injection)
3.
Setter 注⼊(Setter Injection)
4.1 属性注⼊
属性注⼊是使⽤
@Autowired
实现的,将 Service 类注⼊到 Controller 类中.
Service 类的实现代码如下:
4.2 构造⽅法注⼊
4.3 Setter 注⼊
Setter 注⼊和属性的 Setter ⽅法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注
解 ,如下代码所⽰:
4.4 三种注⼊优缺点分析
•
属性注⼊
◦
优点: 简洁,使⽤⽅便;
◦
缺点:
▪
只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指
针异常)
▪
不能注⼊⼀个Final修饰的属性
•
构造函数注⼊(Spring 4.X推荐)
◦
优点:
▪
可以注⼊final修饰的属性
▪
注⼊的对象不会被修改
▪
依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法
是在类加载阶段就会执⾏的⽅法.
▪
通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的
◦
缺点:
▪
注⼊多个对象时, 代码会⽐较繁琐
更多DI相关内容参考:
Dependencies :: Spring Framework
4.5 @Autowired存在问题
当同⼀类型存在多个bean时, 使⽤@Autowired会存在问题
报错的原因是,⾮唯⼀的 Bean 对象。
如何解决上述问题呢?Spring提供了以下⼏种解决⽅案:
•
@Primary
•
@Qualifier
•
@Resource
使⽤@Primary注解:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现.
使⽤@Qualifier注解:指定当前要注⼊的bean对象。 在@Qualifier的value属性中,指定注⼊的bean
的名称。
•
@Qualifier注解不能单独使⽤,必须配合@Autowired使⽤
使⽤@Resource注解:是按照bean的名称进⾏注⼊。通过name属性指定要注⼊的bean的名称。
6. 总结
6.1 Spring, Spring Boot 和Spring MVC的关系以及区别
Spring: 简单来说, Spring 是⼀个开发应⽤框架,什么样的框架呢,有这么⼏个标签:轻量级、⼀
站式、模块化,其⽬的是⽤于简化企业级应⽤程序开发.
Spring的主要功能: 管理对象,以及对象之间的依赖关系, ⾯向切⾯编程, 数据库事务管理, 数据访
问, web框架⽀持等.
但是Spring具备⾼度可开放性, 并不强制依赖Spring, 开发者可以⾃由选择Spring的部分或者全
部, Spring可以⽆缝继承第三⽅框架, ⽐如数据访问框架(Hibernate 、JPA), web框架(如Struts、
JSF)
Spring MVC: Spring MVC是Spring的⼀个⼦框架, Spring诞⽣之后, ⼤家觉得很好⽤, 于是按照MVC
模式设计了⼀个 MVC框架(⼀些⽤Spring 解耦的组件), 主要⽤于开发WEB应⽤和⽹络接⼝,所以,
Spring MVC 是⼀个Web框架
Spring MVC基于Spring进⾏开发的, 天⽣的与Spring框架集成. 可以让我们更简洁的进⾏Web层
开发, ⽀持灵活的 URL 到⻚⾯控制器的映射, 提供了强⼤的约定⼤于配置的契约式编程⽀持, ⾮常
容易与其他视图框架集成,如 Velocity、FreeMarker等
Spring Boot: Spring Boot是对Spring的⼀个封装, 为了简化Spring应⽤的开发⽽出现的,中⼩型
企业,没有成本研究⾃⼰的框架, 使⽤Spring Boot 可以更加快速的搭建框架, 降级开发成本, 让开发 ⼈员更加专注于Spring应⽤的开发,⽽⽆需过多关注XML的配置和⼀些底层的实现.
Spring Boot 是个脚⼿架, 插拔式搭建项⽬, 可以快速的集成其他框架进来.
⽐如想使⽤SpringBoot开发Web项⽬, 只需要引⼊Spring MVC框架即可, Web开发的⼯作是
SpringMVC完成的, ⽽不是SpringBoot, 想完成数据访问, 只需要引⼊Mybatis框架即可.
Spring Boot只是辅助简化项⽬开发的, 让开发变得更加简单, 甚⾄不需要额外的web服务器, 直接
⽣成jar包执⾏即可.
最后⼀句话总结: Spring MVC和Spring Boot都属于Spring,Spring MVC 是基于Spring的⼀个
MVC 框架,⽽Spring Boot 是基于Spring的⼀套快速开发整合包
这三者专注的领域不同,解决的问题也不⼀样, 总的来说,Spring 就像⼀个⼤家族,有众多衍⽣产
品, 但他们的基础都是Spring, ⽤⼀张图来表⽰他们三个的关系:
至此本文结束,感谢大家观看。