本节目标
-
了解Spring,Spring MVC, Spring Boot 之间的联系及区别
-
掌握IoC&DI的概念以及写法
1. IoC & DI 入门
在前面的章节中, 我们学习了Spring Boot和Spring MVC的开发, 可以完成一些基本功能的开发了, 但是 什么是Spring呢? Spring, Spring Boot 和SpringMVC又有什么关系呢? 咱们还是带着问题去学习.
我们先看什么是Spring
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管理, 就是loc思想
IoC: Inversion of Control (控制反转), 也就是说 Spring 是一个"控制反转"的容器
1.2 IoC 介绍
接下来我们通过案例来了解一下什么是IoC
**需求:**造一辆车
1.2.1 传统程序开发
我们的实现思路是这样的
先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据底盘设计车身(Framework),最 后根据车身设计好整个汽车(Car)。这里就出现了一个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子
java
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;
c Tire(){
this.size = 17;
System.out.println("轮胎尺寸:" + size);
}
}
}
1.2.2 问题分析
这样的设计看起来没问题,但是可维护性却很低
接下来需求有了变更: 随着对的车的需求量越来越大, 个性化需求也会越来越多,我们需要加工多种尺寸的轮胎.
那这个时候就要对上面的程序进行修改了,修改后的代码如下所示
java
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);
}
}
}
从以上代码可以看出,以上程序的问题是:当最底层代码改动之后,整个调用链上的所有代码都需要修改.
程序的耦合度非常高(修改一处代码, 影响其他处的代码修改)
1.2.3 解决方案
在上面的程序中, 我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改. 同样因 为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改, 也就是整个设计几乎都得改
我们尝试换一种思路, 我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计 底盘,最后根据底盘来设计轮子. 这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身, 车身依赖汽车
1.2.4 IoC程序开发
基于以上思路,我们把调用汽车的程序示例改造一下,把创建子类的方式,改为注入传递的方式.
java
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);
}
}
}
代码经过以上调整,无论底层类如何变化,整个调用链是不用做任何改变的,这样就完成了代码之间的解耦,从而实现了更加灵活、通用的程序设计了
1.2.5 IoC 优势
在传统的代码中对象创建顺序是:Car -> Framework -> Bottom -> Tire
改进之后解耦的代码的对象创建顺序是:Tire -> Bottom -> Framework -> Car
1.3 DI 介绍
上面学习了IoC, 什么是DI呢?
DI: Dependency Injection(依赖注入)
容器在运行期间, 动态的为应用程序提供运行时所依赖的资源,称之为依赖注入
2. IoC & DI 使用
对IoC和DI有了初步的了解, 我们接下来具体学习Spring IoC和DI的代码实现
依然是先使用, 再学习
目标: 把BookDao, BookService 交给Spring管理, 完成Controller层, Service层, Dao层的解耦
步骤:
-
Service层及Dao层的实现类,交给Spring管理: 使用注解: @Component
-
在Controller层 和Service层 注入运行时依赖的对象: 使用注解 @Autowired
实现:
-
把BookDao 交给Spring管理, 由Spring来管理对象
@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;
}
}
-
把BookService 交给Spring管理, 由Spring来管理对象
@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;
}
} -
删除创建BookDao的代码, 从Spring中获取对象
@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;
}
} -
删除创建BookService的代码, 从Spring中获取对象
@RequestMapping("/book")
@RestController
public class BookController {
@Autowired
private BookService bookService;
@RequestMapping("/getList")
public List<BookInfo> getList(){
//获取数据
List<BookInfo> books = bookService.getBookList();
return books;
}
}
3. IoC 详解
通过上面的案例, 我们已经知道了Spring IoC 和DI的基本操作, 接下来我们来系统的学习Spring IoC和DI 的操作
前面我们提到IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对 象。 也就是bean的存储
3.1 Bean的存储
在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解: @Component 而Spring框架为了更好的服务web应用程序, 提供了更丰富的注解
共有两类注解类型可以实现:
-
类注解:@Controller、@Service、@Repository、@Component、@Configuration.
-
方法注解:@Bean.
3.1.1 @Controller(控制器存储)
使用 @Controller 存储 bean 的代码如下所示
@Controller // 将对象存储到 Spring 中
public class UserController {
public void sayHi(){
System.out.println("hi,UserController...");
}
}
@SpringBootApplication
public class SpringBookDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringBookDemoApplication.class, args);
Demo bean = context.getBean(Demo.class);
bean.t1();
}
}
获取Bean的其他方法
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context =
SpringApplication.run(SpringIocDemoApplication.class, args);
//从Spring上下文中获取对象
//根据bean类型, 从Spring上下文中获取对象
UserController userController1 = context.getBean(UserController.class);
//根据bean名称, 从Spring上下文中获取对象
UserController userController2 = (UserController)
context.getBean("userController");
//根据bean类型+名称, 从Spring上下文中获取对象
UserController userController3 =
context.getBean("userController",UserController.class);
System.out.println(userController1);
System.out.println(userController2);
System.out.println(userController3);
}
}

3.1.2 @Service(服务存储)
使用 @Service 存储 bean 的代码如下所示:
java
@Service
public class UserService {
public void sayHi(String name) {
System.out.println("Hi," + name);
}
}
读取 bean 的代码:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context =
SpringApplication.run(SpringIocDemoApplication.class, args);
//从Spring中获取UserService对象
UserService userService = context.getBean(UserService.class);
//使用对象
userService.sayHi();
}
}
3.2 为什么要这么多类注解?
这个也是和咱们前面讲的应用分层是呼应的. 让程序员看到类注解之后,就能直接了解当前类的用途.
• @Controller:控制层, 接收请求, 对请求进行处理, 并进行响应.
• @Servie:业务逻辑层, 处理具体的业务逻辑.
• @Repository:数据访问层,也称为持久层. 负责数据访问操作
• @Configuration:配置层. 处理项目中的一些配置信息
3.3 方法注解 @Bean
3.3.1 方法注解要配合类注解使用
java
@Component
public class BeanConfig {
@Bean
public User user(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
}
java
@SpringBootApplication
public class SpringBookDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringBookDemoApplication.class, args);
User bean1 = context.getBean(User.class);
System.out.println(bean1);
}
}

3.3.2 定义多个对象
我们看下@Bean的使用
java
@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;
}
}
定义了多个对象的话, 我们根据类型获取对象, 获取的是哪个对象呢?
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context =
SpringApplication.run(SpringIocDemoApplication.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
可以通过设置 name 属性给 Bean 对象进行重命名操作,如下代码所示
java
@Bean(name = {"u1","user1"})
public User user1(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
此时我们使用 u1 就可以获取到 User 对象了,如下代码所示
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context =
SpringApplication.run(SpringIocDemoApplication.class, args);
//从Spring上下文中获取对象
User u1 = (User) context.getBean("u1");
//使用对象
System.out.println(u1);
}
}
name={} 可以省略,如下代码所示
java
@Bean({"u1","user1"})
public User user1(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
只有一个名称时, {}也可以省略,
java
@Bean("u1")
public User user1(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
3.4 扫描路径
Q: 使用前面学习的四个注解声明的bean,一定会生效吗?
A: 不一定(原因:bean想要生效,还需要被Spring扫描
那为什么前面没有配置 @ComponentScan注解也可以呢? @ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中了
默认扫描的范围是SpringBoot启动类所在包及其子包
4. DI 详解
上面我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注入DI的细节。
关于依赖注入, Spring也给我们提供了三种方式:
-
属性注入(Field Injection)
-
构造方法注入(Constructor Injection)
-
Setter 注入(Setter Injection)
4.1 属性注入
属性注入是使用 @Autowired 实现的,将 Service 类注入到 Controller 类中.
java
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void sayHi() {
System.out.println("Hi,UserService");
}
}
Controller 类的实现代码如下
java
@Controller
public class UserController {
//注入方法1: 属性注入
@Autowired
private UserService userService;
public void sayHi(){
System.out.println("hi,UserController...");
userService.sayHi();
}
}
获取 Controller 中的 sayHi方法:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context =
SpringApplication.run(SpringIocDemoApplication.class, args);
//从Spring上下文中获取对象
UserController userController = (UserController)
context.getBean("userController");
//使用对象
userController.sayHi();
}
}
4.2 构造方法注入
构造方法注入是在类的构造方法中实现注入,如下代码所示
java
@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();
}
}
注意事项:如果类只有一个构造方法,那么 @Autowired 注解可以省略;如果类中有多个构造方法, 那么需要添加上 @Autowired 来明确指定到底使用哪个构造方法
4.3 Setter注入
Setter 注入和属性的 Setter 方法实现类似,只不过在设置 set 方法的时候需要加上 @Autowired 注解,如下代码所示
java
@Controller
public class UserController3 {
//注入方法3: Setter方法注入
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void sayHi(){
System.out.println("hi,UserController3...");
userService.sayHi();
}
}
练习一下:尝试一下 set 方法如果不加 @Autowired 注解能注入成功吗?
4.4 三种注入优缺点分析
• 属性注入
◦ 优点: 简洁,使用方便;
◦ 缺点:
▪ 只能用于 IoC 容器,如果是非 IoC 容器不可用,并且只有在使用的时候才会出现 NPE(空指 针异常)
▪ 不能注入一个Final修饰的属性
4.5 @Autowired存在问题
当同一类型存在多个bean时, 使用@Autowired会存在问题
java
@Component
public class BeanConfig {
@Bean("u1")
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;
}
}
java
@Controller
public class UserController {
@Autowired
private UserService userService;
//注入user
@Autowired
private User user;
public void sayHi(){
System.out.println("hi,UserController...");
userService.sayHi();
System.out.println(user);
}
}

如何解决上述问题呢?Spring提供了以下几种解决方案
• @Primary • @Qualifier • @Resource
java
@Component
public class BeanConfig {
@Primary //指定该bean为默认bean的实现
@Bean("u1")
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 的名称
• @Qualifier注解不能单独使用,必须配合@Autowired使用
java
@Controller
public class UserController {
@Qualifier("user2") //指定bean名称
@Autowired
private User user;
public void sayHi(){
System.out.println("hi,UserController...");
System.out.println(user);
}
}
使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称
java
@Controller
public class UserController {
@Resource(name = "user2")
private User user;
public void sayHi(){
System.out.println("hi,UserController...");
System.out.println(user);
}
}
常见面试题
@Autowird 与 @Resource的区别
@Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
•@Autowired 默认是按照类型注入,而@Resource是按照名称注入.
5. 练习
通过上面的学习, 我们把前面的图书管理系统代码进行调整
Service层的注解, 改成 @Service
Dao层的注解, 改成 @Repository
重新运行代码, 验证程序访问正常