Spring核心:IoC与DI详解
本节目标1. 了解Spring,Spring MVC, Spring Boot 之间的联系及区别2. 掌握IoCDI的概念以及写法1. IoC DI 入门在前面的章节中, 我们学习了Spring Boot和Spring MVC的开发, 可以完成一些基本功能的开发了, 但是 什么是Spring呢? Spring, Spring Boot 和SpringMVC又有什么关系呢? 咱们还是带着问题去学习.我们先看什么是Spring1.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)。这里就出现了一个依赖关系汽车依赖车身车身依赖底盘底盘依赖轮子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 问题分析这样的设计看起来没问题但是可维护性却很低接下来需求有了变更: 随着对的车的需求量越来越大, 个性化需求也会越来越多我们需要加工多种尺寸的轮胎.那这个时候就要对上面的程序进行修改了修改后的代码如下所示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程序开发基于以上思路我们把调用汽车的程序示例改造一下把创建子类的方式改为注入传递的方式.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 - Car1.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管理: 使用注解: Component2. 在Controller层 和Service层 注入运行时依赖的对象: 使用注解 Autowired实现:1. 把BookDao 交给Spring管理, 由Spring来管理对象Component public class BookDao { /** * 数据Mock 获取图书信息 * * return */ public ListBookInfo mockData() { ListBookInfo 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 ListBookInfo getBookList() { ListBookInfo 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 ListBookInfo getBookList() { ListBookInfo 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 ListBookInfo getList(){ //获取数据 ListBookInfo 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 的代码如下所示Service public class UserService { public void sayHi(String name) { System.out.println(Hi, name); } }读取 bean 的代码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 方法注解 Bean3.3.1 方法注解要配合类注解使用Component public class BeanConfig { Bean public User user(){ User user new User(); user.setName(zhangsan); user.setAge(18); return user; } }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的使用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; } }定义了多个对象的话, 我们根据类型获取对象, 获取的是哪个对象呢?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 对象进行重命名操作如下代码所示Bean(name {u1,user1}) public User user1(){ User user new User(); user.setName(zhangsan); user.setAge(18); return user; }此时我们使用 u1 就可以获取到 User 对象了如下代码所示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{} 可以省略如下代码所示Bean({u1,user1}) public User user1(){ User user new User(); user.setName(zhangsan); user.setAge(18); return user; }只有一个名称时, {}也可以省略,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 类中.import org.springframework.stereotype.Service; Service public class UserService { public void sayHi() { System.out.println(Hi,UserService); } }Controller 类的实现代码如下Controller public class UserController { //注入方法1: 属性注入 Autowired private UserService userService; public void sayHi(){ System.out.println(hi,UserController...); userService.sayHi(); } }获取 Controller 中的 sayHi方法: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 构造方法注入构造方法注入是在类的构造方法中实现注入如下代码所示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 注解如下代码所示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会存在问题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; } }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 • ResourceComponent 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使用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的名称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层的注解, 改成 ServiceDao层的注解, 改成 Repository重新运行代码, 验证程序访问正常