Spring核心:IoC与DI详解

本节目标

  1. 了解Spring,Spring MVC, Spring Boot 之间的联系及区别

  2. 掌握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层的解耦

步骤:

  1. Service层及Dao层的实现类,交给Spring管理: 使用注解: @Component

  2. 在Controller层 和Service层 注入运行时依赖的对象: 使用注解 @Autowired

实现:

  1. 把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;
      }
      }
  2. 把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;
    }
    }

  3. 删除创建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;
    }
    }

  4. 删除创建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应用程序, 提供了更丰富的注解

共有两类注解类型可以实现:

  1. 类注解:@Controller、@Service、@Repository、@Component、@Configuration.

  2. 方法注解:@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也给我们提供了三种方式:

  1. 属性注入(Field Injection)

  2. 构造方法注入(Constructor Injection)

  3. 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

重新运行代码, 验证程序访问正常

相关推荐
运维 小白2 小时前
PostgreSQL高可用(Patroni + etcd + Keepalived)
数据库·postgresql·etcd
2301_813599552 小时前
HTML图片怎么用UnoCSS对齐_UnoCSS原子化CSS图片对齐实战
jvm·数据库·python
m0_377618232 小时前
c++怎么在不加载整个大文件的情况下获取其SHA256校验值【进阶】
jvm·数据库·python
檬柠wan2 小时前
MySQL-数据库增删改查学习
数据库·学习·mysql
qq_189807032 小时前
CSS如何实现纯CSS树状目录结构_利用-checked与递归思维构建交互节点
jvm·数据库·python
2301_777599372 小时前
Go语言如何做HTTP连接池_Go语言HTTP连接池教程【最新】
jvm·数据库·python
Wy_编程2 小时前
Redis数据类型和常用命令
数据库·redis·缓存
Polar__Star3 小时前
Redis如何利用位图快速判断数据存在性
jvm·数据库·python
2301_817672263 小时前
CSS如何实现优雅的间距_使用CSS Grid控制盒模型间隙
jvm·数据库·python