五、Spring IoC&DI ★ ✔

5. Spring IoC&DI

  • [1. IoC & DI ⼊⻔](#1. IoC & DI ⼊⻔)
    • [1.1 Spring 是什么?★ (Spring 是包含了众多⼯具⽅法的 IoC 容器)](#1.1 Spring 是什么?★ (Spring 是包含了众多⼯具⽅法的 IoC 容器))
      • [1.1.1 什么是容器?](#1.1.1 什么是容器?)
      • [1.1.2 什么是 IoC?★ (IoC: Inversion of Control (控制反转))](#1.1.2 什么是 IoC?★ (IoC: Inversion of Control (控制反转)))
        • [总结:把传统模式 被依赖对象创建依赖对象 改成 交给容器创建。用的时候注入即可。](#总结:把传统模式 被依赖对象创建依赖对象 改成 交给容器创建。用的时候注入即可。)
    • [1.2 IoC 介绍](#1.2 IoC 介绍)
    • [1.3 DI 介绍](#1.3 DI 介绍)
  • [2. IoC & DI 使⽤](#2. IoC & DI 使⽤)
        • [以下是一个基于Spring Boot的IoC和DI的实战代码案例:](#以下是一个基于Spring Boot的IoC和DI的实战代码案例:)
        • [⽬标: 把BookDao, BookService 交给Spring管理, 完成Controller层, Service层, Dao层的解耦](#⽬标: 把BookDao, BookService 交给Spring管理, 完成Controller层, Service层, Dao层的解耦)
  • [3. IoC 详解](#3. IoC 详解)
    • [3.1 Bean的存储](#3.1 Bean的存储)
      • [3.1.1 @Controller(控制器存储)](#3.1.1 @Controller(控制器存储))
      • [3.1.2 @Service(服务存储)](#3.1.2 @Service(服务存储))
      • [3.1.3 @Repository(仓库存储)](#3.1.3 @Repository(仓库存储))
      • [3.1.4 @Component(组件存储)](#3.1.4 @Component(组件存储))
      • [3.1.5 @Configuration(配置存储)](#3.1.5 @Configuration(配置存储))
    • [3.2 为什么要这么多类注解?](#3.2 为什么要这么多类注解?)
    • [3.3 ⽅法注解 @Bean](#3.3 ⽅法注解 @Bean)
      • [3.3.1 ⽅法注解要配合类注解使⽤](#3.3.1 ⽅法注解要配合类注解使⽤)
      • [3.3.2 定义多个对象](#3.3.2 定义多个对象)
      • [3.3.3 重命名 Bean](#3.3.3 重命名 Bean)
    • [3.4 扫描路径](#3.4 扫描路径)
  • [4. DI 详解](#4. DI 详解)
  • [5. 总结](#5. 总结)
    • [Spring, Spring Boot 和Spring MVC的关系以及区别](#Spring, Spring Boot 和Spring MVC的关系以及区别)

本节⽬标

  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 是包含了众多⼯具⽅法的 IoC 容器)

通过前⾯的学习, 我们知道了Spring是⼀个开源框架, 他让我们的开发更加简单. 他⽀持⼴泛的应⽤场景, 有着活跃⽽庞⼤的社区, 这也是Spring能够⻓久不衰的原因.但是这个概念相对来说, 还是⽐较抽象.
我们⽤⼀句更具体的话来概括Spring, 那就是: Spring 是包含了众多⼯具⽅法的 IoC 容器

那问题来了,什么是容器?什么是 IoC 容器?接下来我们⼀起来看

1.1.1 什么是容器?

容器是⽤来容纳某种物品的(基本)装置。⸺来⾃:百度百科

⽣活中的⽔杯, 垃圾桶, 冰箱等等这些都是容器.

我们想想,之前课程我们接触的容器有哪些?

  • List/Map -> 数据存储容器
  • Tomcat -> Web 容器

1.1.2 什么是 IoC?★ (IoC: Inversion of Control (控制反转))

IoC 是Spring的核⼼思想, 也是常⻅的⾯试题, 那什么是IoC呢?

其实IoC我们在前⾯已经使⽤了, 我们在前⾯讲到, 在类上⾯添加 @RestController 和 @Controller 注解, 就是把这个对象交给Spring管理, Spring 框架启动时就会加载该类. 把对象交给Spring管理, 就是IoC思想

IoC: Inversion of Control (控制反转), 也就是说 Spring 是⼀个"控制反转"的容器.

什么是控制反转呢? 也就是控制权反转. 什么的控制权发⽣了反转? 获得依赖对象的过程被反转了也就是说, 当需要某个对象时, 传统开发模式中需要⾃⼰通过 new 创建对象, 现在不需要再进⾏创建, 把创建对象的任务交给容器, 程序中只需要依赖注⼊ (Dependency Injection,DI)就可以了.

这个容器称为:IoC容器. Spring是⼀个IoC容器, 所以有时Spring 也称为Spring 容器.

总结:把传统模式 被依赖对象创建依赖对象 改成 交给容器创建。用的时候注入即可。

1.2 IoC 介绍

接下来我们通过案例来了解⼀下什么是IoC

需求: 造⼀辆⻋

1.2.1 传统程序开发 ★

我们的实现思路是这样的:

先设计轮⼦(Tire),然后根据轮⼦的⼤⼩设计底盘(Bottom),接着根据底盘设计⻋⾝(Framework),最后根据⻋⾝设计好整个汽⻋(Car)。这⾥就出现了⼀个"依赖"关系:汽⻋依赖⻋⾝,⻋⾝依赖底盘,底盘依赖轮⼦

car-framework-bottom-tire

最终程序的实现代码如下

java 复制代码
public class NewCarExample {
	public static void main(String[] args) {
		ar ar = ew ar );
		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("轮胎尺⼨:" + size);
		}
	}
}

1.2.2 问题分析

这样的设计看起来没问题,但是可维护性却很低.

接下来需求有了变更: 随着对的⻋的需求量越来越⼤, 个性化需求也会越来越多,我们需要加⼯多种尺⼨的轮胎.

那这个时候就要对上⾯的程序进⾏修改了,修改后的代码如下所⽰:

修改之后, 其他调⽤程序也会报错, 我们需要继续修改

从以上代码可以看出,以上程序的问题是:当最底层代码改动之后,整个调⽤链上的所有代码都需要修改.

程序的耦合度⾮常⾼(修改⼀处代码, 影响其他处的代码修改)

1.2.3 解决⽅案

在上⾯的程序中, 我们是根据轮⼦的尺⼨设计的底盘,轮⼦的尺⼨⼀改,底盘的设计就得修改. 同样因为我们是根据底盘设计的⻋⾝,那么⻋⾝也得改,同理汽⻋设计也得改, 也就是整个设计⼏乎都得改

我们尝试换⼀种思路, 我们先设计汽⻋的⼤概样⼦,然后根据汽⻋的样⼦来设计⻋⾝,根据⻋⾝来设计底盘,最后根据底盘来设计轮⼦. 这时候,依赖关系就倒置过来了:轮⼦依赖底盘, 底盘依赖⻋⾝,⻋⾝依赖汽⻋

这就类似我们打造⼀辆完整的汽⻋, 如果所有的配件都是⾃⼰造,那么当客⼾需求发⽣改变的时候,⽐如轮胎的尺⼨不再是原来的尺⼨了,那我们要⾃⼰动⼿来改了,但如果我们是把轮胎外包出去,那么即使是轮胎的尺⼨发⽣变变了,我们只需要向代理⼯⼚下订单就⾏了,我们⾃⾝是不需要出⼒的.

如何来实现呢:

我们可以尝试不在每个类中⾃⼰创建下级类,如果⾃⼰创建下级类就会出现当下级类发⽣改变操作,⾃⼰也要跟着修改.

此时,我们只需要将原来由⾃⼰创建的下级类,改为传递的⽅式(也就是注⼊的⽅式),因为我们不需要在当前类中创建下级类了,所以下级类即使发⽣变化(创建或减少参数),当前类本⾝也⽆需修改任何代码,这样就完成了程序的解耦.

1.2.4 IoC程序开发 ★

基于以上思路,我们把调⽤汽⻋的程序⽰例改造⼀下,把创建⼦类的⽅式,改为注⼊传递的⽅式.

具体实现代码如下:

java 复制代码
public class Main {
    public static void main(String[] args) {
        Tire tire = new Tire(19,"red");
        Bottom bottom = new Bottom(tire);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
        car.run();
    }
}
java 复制代码
public class Bottom {
    private Tire tire;

    public Bottom(Tire tire) {
        this.tire = tire;
        System.out.println("bottom init...");
    }

}
java 复制代码
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...");
    }
}
java 复制代码
public class Framework {
    private Bottom bottom;

    public Framework(Bottom bottom) {
        this.bottom = bottom;
        System.out.println("framework init...");
    }
}
java 复制代码
public class Tire {
    private int size;
    private String color;

    public Tire(int size,String color) {
        System.out.println("tire size:"+size+",color:"+color);
    }
}

代码经过以上调整,⽆论底层类如何变化,整个调⽤链是不⽤做任何改变的,这样就完成了代码之间的解耦,从⽽实现了更加灵活、通⽤的程序设计了。

1.2.5 IoC 优势

Tire -> Bottom -> Framework -> Car

在传统的代码中对象创建顺序是: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呢?

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中取出对象

当涉及到Spring Boot的IoC(控制反转)和DI(依赖注入)时,一个常见的实战案例是创建一个简单的RESTful API。

以下是一个基于Spring Boot的IoC和DI的实战代码案例:

首先,你需要创建一个简单的Maven项目,并添加Spring Boot的依赖。然后创建一个Controller类来处理RESTful请求,并创建一个Service类来处理业务逻辑。最后,通过依赖注入将Service类注入到Controller类中。

java 复制代码
// Service类
@Service
public class UserService {
    public String getUserInfo() {
        return "User information";
    }
}

// Controller类
@RestController
public class UserController {
    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/user")
    public String getUser() {
        return userService.getUserInfo();
    }
}

// Spring Boot应用入口类
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

在这个案例中,UserService类使用了@Service注解来告诉Spring它是一个Bean,而UserController类使用了@Autowired注解来告诉Spring需要将UserService注入到它的构造函数中。当Spring Boot应用启动时,它会自动扫描并创建这些Bean,并且处理它们之间的依赖关系。

通过这个案例,你可以看到IoC和DI是如何在Spring Boot应用中发挥作用的。当你发送GET请求到/user时,UserController会调用UserService来获取用户信息,并返回给客户端。这展示了IoC和DI如何帮助我们编写松耦合、可测试的代码。

⽬标: 把BookDao, BookService 交给Spring管理, 完成Controller层, Service层, Dao层的解耦

步骤:

  1. Service层及Dao层的实现类,交给Spring管理: 使⽤注解: @Component
  2. 在Controller层 和Service层 注⼊运⾏时依赖的对象: 使⽤注解 @Autowired实现:
  3. 把BookDao 交给Spring管理, 由Spring来管理对象

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 的代码如下所⽰:

java 复制代码
@Controller // 将对象存储到 Spring 中
public class UserController {
	public void sayHi(){
		System.out.println("hi,UserController...");
	}
}
如何观察这个对象已经存在Spring容器当中了呢?
接下来我们学习如何从Spring容器中获取对象 ★

在Spring框架中,你可以使用注解来实现对象的注入和获取。首先,你需要在你的类中使用@Component或者其他相关的注解来标识这个类是一个Spring容器管理的Bean。然后,你可以使用@Autowired注解来实现对象的注入,或者使用@Resource注解来指定注入的对象。

举个例子,假设你有一个名为UserService的类,你可以这样标识它是一个Bean:

java 复制代码
@Component
public class UserService {
    // ...
}

然后,在另一个类中,你可以使用@Autowired注解来注入UserService

java 复制代码
@Component
public class UserController {
    @Autowired
    private UserService userService;

    // ...
}

这样,Spring容器会在启动时自动将UserService注入到UserController中。

如果你想手动从Spring容器中获取对象,你可以使用@Autowired或者@Resource注解来注入ApplicationContext,然后通过getBean方法来获取对象。示例代码如下:

ApplicationContext.getBean(手动获取bean对象)
java 复制代码
@Component
public class SomeOtherClass {
    @Autowired
    private ApplicationContext context;

    public void doSomething() {
        UserService userService = context.getBean(UserService.class);
        // 使用userService对象进行操作
    }
}

这样,你就可以通过注解的方式实现对象的注入和获取。希望这能帮到你!

XML的方式注入的获取 ClassPathXmlApplicationContext

在Spring容器中获取对象通常需要使用ApplicationContext接口。以下是一个简单的示例代码,演示如何从Spring容器中获取对象:

java 复制代码
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        // 加载Spring配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        // 从容器中获取对象
        YourObject yourObject = (YourObject) context.getBean("yourObjectBeanName");

        // 使用获取到的对象
        yourObject.doSomething();
    }
}

在上面的示例中,假设你有一个名为"YourObject"的类,并且在Spring配置文件"applicationContext.xml"中定义了该类的bean。通过调用context.getBean("yourObjectBeanName")方法,你可以从Spring容器中获取到该对象的实例。

获取bean对象的其他⽅式(beanFactory)

上述代码是根据类型来查找对象, 如果Spring容器中, 同⼀个类型存在多个bean的话, 怎么来获取呢?

ApplicationContext 也提供了其他获取bean的⽅式, ApplicationContext 获取bean对象的功能, 是⽗类BeanFactory提供的功能

java 复制代码
public interface BeanFactory {
 
 //以上省略...
 
	 // 1. 根据bean名称获取bean
	 Object getBean(String var1) throws BeansException;
	 // 2. 根据bean名称和类型获取bean
	 <T> T getBean(String var1, Class<T> var2) throws BeansException;
	 // 3. 按bean名称和构造函数参数动态创建bean,只适⽤于具有原型(prototype)作⽤域的bean
	 Object getBean(String var1, Object... var2) throws BeansException;
	 // 4. 根据类型获取bean
	 <T> T getBean(Class<T> var1) throws BeansException;
	 // 5. 按bean类型和构造函数参数动态创建bean, 只适⽤于具有原型(prototype)作⽤域的bean
	 <T> T getBean(Class<T> var1, Object... var2) throws BeansException;
 
 //以下省略...
}

常⽤的是上述1,2,4种, 这三种⽅式,获取到的bean是⼀样的

其中1,2种都涉及到根据名称来获取对象.

bean的名称是什么呢?

Spring bean是Spring框架在运⾏时管理的对象, Spring会给管理的对象起⼀个名字.

⽐如学校管理学⽣, 会给每个学⽣分配⼀个学号, 根据学号, 就可以找到对应的学⽣.

Spring也是如此, 给每个对象起⼀个名字, 根据Bean的名称(BeanId)就可以获取到对应的对象.

Bean 命名约定

我们看下官⽅⽂档的说明: Bean Overview :: Spring Framework

程序开发⼈员不需要为bean指定名称(BeanId), 如果没有显式的提供名称(BeanId),Spring容器将为该bean⽣成唯⼀的名称.

命名约定使⽤Java标准约定作为实例字段名. 也就是说,bean名称以⼩写字⺟开头,然后使⽤驼峰式⼤⼩写

⽐如

类名: UserController, Bean的名称为: userController

类名: AccountManager, Bean的名称为: accountManager

类名: AccountService, Bean的名称为: accountService

也有⼀些特殊情况, 当有多个字符并且第⼀个和第⼆个字符都是⼤写时, 将保留原始的⼤⼩写. 这些规则与java.beans.Introspector.decapitalize (Spring在这⾥使⽤的)定义的规则相同.

⽐如

类名: UController, Bean的名称为: UController

类名: AManager, Bean的名称为: AManager

根据这个命名规则, 我们来获取Bean

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
		//从Spring上下⽂中获取对象
		//根据bean类型, 从Spring上下⽂中获取对象
		UserController userController1 = context.getBean(UserController.class);
		//根据bean名称, 从Spring上下⽂中获取对象
		UserController userController2 = (UserController) context.getBean("userCon
		//根据bean类型+名称, 从Spring上下⽂中获取对象
		UserController userController3 = context.getBean("userController",UserCont
		
		System.out.println(userController1);
		System.out.println(userController2);
		System.out.println(userController3);
	}
}

地址⼀样, 说明对象是⼀个

获取bean对象, 是⽗类BeanFactory提供的功能

ApplicationContext VS BeanFactory(常⻅⾯试题) ★
  • 继承关系和功能⽅⾯来说:Spring 容器有两个顶级的接⼝:BeanFactory 和 ApplicationContext。其中 BeanFactory 提供了基础的访问容器的能⼒,⽽ApplicationContext 属于 BeanFactory 的⼦类,它除了继承了 BeanFactory 的所有功能之外,它还拥有独特的特性,还添加了对国际化⽀持、资源访问⽀持、以及事件传播等⽅⾯的⽀持.
  • 从性能⽅⾯来说:ApplicationContext 是⼀次性加载并初始化所有的 Bean 对象,⽽BeanFactory 是需要那个才去加载那个,因此更加轻量. (空间换时间)

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(SpringIocDemoApplicatio
		//从Spring中获取UserService对象
		UserService userService = context.getBean(UserService.class);
		//使⽤对象
		userService.sayHi();
		}
	}

3.1.3 @Repository(仓库存储)

使⽤ @Repository 存储 bean 的代码如下所⽰

java 复制代码
@Repository
public class UserRepository {
	public void sayHi() {
		System.out.println("Hi, UserRepository~");
	}
}

读取 bean 的代码

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
		//从Spring上下⽂中获取对象
		UserRepository userRepository = context.getBean(UserRepository.class);
		//使⽤对象
		userRepository.sayHi();
	}
}

3.1.4 @Component(组件存储)

使⽤ @Component 存储 bean 的代码如下所⽰:

java 复制代码
@Component
public class UserComponent {
	public void sayHi() {
		System.out.println("Hi, UserComponent~");
	}
}

读取

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
		//从Spring上下⽂中获取对象
		UserComponent userComponent = context.getBean(UserComponent.class);
		//使⽤对象
		userComponent.sayHi();
	}
}

3.1.5 @Configuration(配置存储)

使⽤ @Configuration 存储 bean 的代码如下所⽰:

java 复制代码
@Configuration
public class UserConfiguration {
	public void sayHi() {
		System.out.println("Hi,UserConfiguration~");
	}
}

读取

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
		//从Spring上下⽂中获取对象
		UserConfiguration userConfiguration = context.getBean(UserConfiguration.cl
		//使⽤对象
		userConfiguration.sayHi();
	}
}

3.2 为什么要这么多类注解?

这个也是和咱们前⾯讲的应⽤分层是呼应的. 让程序员看到类注解之后,就能直接了解当前类的⽤途.

• @Controller:控制层, 接收请求, 对请求进⾏处理, 并进⾏响应.

• @Servie:业务逻辑层, 处理具体的业务逻辑.

• @Repository:数据访问层,也称为持久层. 负责数据访问操作

• @Configuration:配置层. 处理项⽬中的⼀些配置信息.

这和每个省/市都有⾃⼰的⻋牌号是⼀样的.

⻋牌号都是唯⼀的, 标识⼀个⻋辆的. 但是为什么还需要设置不同的⻋牌开头呢.

⽐如陕西的⻋牌号就是:陕X:XXXXXX,北京的⻋牌号:京X:XXXXXX,甚⾄⼀个省不同的县区也是不同的,⽐如西安就是,陕A:XXXXX,咸阳:陕B:XXXXXX,宝鸡,陕C:XXXXXX,⼀样.这样做的好处除了可以节约号码之外,更重要的作⽤是可以直观的标识⼀辆⻋的归属地.

程序的应⽤分层,调⽤流程如下:

类注解之间的关系

查看 @Controller / @Service / @Repository / @Configuration 等注解的源码发现:

其实这些注解⾥⾯都有⼀个注解 @Component ,说明它们本⾝就是属于 @Component 的"⼦类".

@Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service ,@Repository 等. 这些注解被称为 @Component 的衍⽣注解.

@Controller , @Service 和 @Repository ⽤于更具体的⽤例(分别在控制层, 业务逻辑层, 持久化层), 在开发过程中, 如果你要在业务逻辑层使⽤ @Component 或@Service,显然@Service是更好的选择

⽐如杯⼦有喝⽔杯, 刷⽛杯等, 但是我们更倾向于在⽇常喝⽔时使⽤⽔杯, 洗漱时使⽤刷⽛杯.

更多资料参考:

https://docs.spring.io/spring-framework/reference/core/beans/classpathscanning.html#beans-stereotype-annotations

3.3 ⽅法注解 @Bean

类注解是添加到某个类上的, 但是存在两个问题:

  1. 使⽤外部包⾥的类, 没办法添加类注解
  2. ⼀个类, 需要多个对象, ⽐如多个数据源

这种场景, 我们就需要使⽤⽅法注解 @Bean

我们先来看看⽅法注解如何使⽤:

java 复制代码
public class BeanConfig {
	@Bean
	public User user(){
		User user = new User();
		user.setName("zhangsan");
		user.setAge(18);
		return user;
	}
}

然⽽,当我们写完以上代码,尝试获取 bean 对象中的 user 时却发现,根本获取不到:

3.3.1 ⽅法注解要配合类注解使⽤

在 Spring 框架的设计中,⽅法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中,如下代码所⽰

java 复制代码
@Component
public class BeanConfig {
	@Bean
	public User user(){
		User user = new User();
		user.setName("zhangsan");
		user.setAge(18);
		return user;
	}
}

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(SpringIocDemoApplicatio
		//从Spring上下⽂中获取对象
		User user = context.getBean(User.class);
		//使⽤对象
		System.out.println(user);
	}
}

报错信息显⽰: 期望只有⼀个匹配, 结果发现了两个, user1, user2

从报错信息中, 可以看出来, @Bean 注解的bean, bean的名称就是它的⽅法名

接下来我们根据名称来获取bean对象

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
		//根据bean名称, 从Spring上下⽂中获取对象
		User user1 = (User) context.getBean("user1");
		User user2 = (User) context.getBean("user2");
		System.out.println(user1);
		System.out.println(user2);
	}
}

运⾏结果:

可以看到, @Bean 可以针对同⼀个类, 定义多个对象.

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(SpringIocDemoApplicatio
		//从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 扫描路径

@ComponentScan({"com.example.demo"})

Q: 使⽤前⾯学习的四个注解声明的bean,⼀定会⽣效吗?

A: 不⼀定(原因:bean想要⽣效,还需要被Spring扫描)

下⾯我们通过修改项⽬⼯程的⽬录结构,来测试bean对象是否⽣效:

再运⾏代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[] args) {
	//获取Spring上下⽂对象
	ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
	//从Spring上下⽂中获取对象
	User u1 = (User) context.getBean("u1");
	//使⽤对象
	System.out.println(u1);
	}
}

解释: 没有bean的名称为u1

为什么没有找到bean对象呢?

使⽤五⼤注解声明的bean,要想⽣效, 还需要配置扫描路径, 让Spring扫描到这些注解

也就是通过 @ComponentScan 来配置扫描路径.

java 复制代码
@ComponentScan({"com.example.demo"})
@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
		//从Spring上下⽂中获取对象
		User u1 = (User) context.getBean("u1");
		//使⽤对象
		System.out.println(u1);
	}
}

{} ⾥可以配置多个包路径

这种做法仅做了解, 不做推荐使⽤

那为什么前⾯没有配置 @ComponentScan注解也可以呢?

@ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication 中了

默认扫描的范围是SpringBoot启动类所在包及其⼦包

在配置类上添加 @ComponentScan 注解, 该注解默认会扫描该类所在的包下所有的配置类

推荐做法:

把启动类放在我们希望扫描的包的路径下, 这样我们定义的bean就都可以被扫描到

4. DI 详解

上⾯我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注⼊DI的细节。

依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象.在上⾯程序案例中,我们使⽤了 @Autowired 这个注解,完成了依赖注⼊的操作.

简单来说, 就是把对象取出来放到某个类的属性中.

在⼀些⽂章中, 依赖注⼊也被称之为 "对象注⼊", "属性装配", 具体含义需要结合⽂章的上下⽂来理解

关于依赖注⼊, Spring也给我们提供了三种⽅式:

  1. 属性注⼊(Field Injection)
  2. 构造⽅法注⼊(Constructor Injection)
  3. Setter 注⼊(Setter Injection)

接下来,我们分别来看。

下⾯我们按照实际开发中的模式,将 Service 类注⼊到Controller 类中。

4.1 属性注⼊

属性注⼊是使⽤ @Autowired 实现的,将 Service 类注⼊到 Controller 类中.

Service 类的实现代码如下:

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(SpringIocDemoApplicatio
		//从Spring上下⽂中获取对象
		UserController userController = (UserController) context.getBean("userCont
		//使⽤对象
		userController.sayHi();
	}
}

去掉@Autowired , 再运⾏⼀下程序看看结果

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修饰的属性
  • 构造函数注⼊(Spring 4.X推荐)

    • 优点:
      • 可以注⼊final修饰的属性
      • 注⼊的对象不会被修改
      • 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法是在类加载阶段就会执⾏的⽅法.
      • 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的
      • 缺点:
        • 注⼊多个对象时, 代码会⽐较繁琐
  • Setter注⼊(Spring 3.X推荐)

    • 优点: ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊
    • 缺点:
      • 不能注⼊⼀个Final修饰的属性
      • 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险.

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);
	}
}

报错的原因是,⾮唯⼀的 Bean 对象。

如何解决上述问题呢?Spring提供了以下⼏种解决⽅案:

  • @Primary
  • @Qualifier
  • @Resource
使⽤@Primary注解:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现.
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是按照名称注⼊. 相⽐于 @Autowired 来说,@Resource ⽀持更多的参数设置,例如 name 设置,根据名称获取 Bean

5. 总结

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开发的⼯作是pringMVC完成的, ⽽不是SpringBoot, 想完成数据访问, 只需要引⼊Mybatis框架即可.Spring Boot只是辅助简化项⽬开发的, 让开发变得更加简单, 甚⾄不需要额外的web服务器, 直接⽣成jar包执⾏即可.

最后⼀句话总结: Spring MVC和Spring Boot都属于Spring,Spring MVC 是基于Spring的⼀个MVC 框架,⽽Spring Boot 是基于Spring的⼀套快速开发整合包.

相关推荐
無限進步D3 小时前
Java 运行原理
java·开发语言·入门
難釋懷3 小时前
安装Canal
java
是苏浙3 小时前
JDK17新增特性
java·开发语言
不光头强3 小时前
spring cloud知识总结
后端·spring·spring cloud
GetcharZp6 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端
阿里加多7 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
likerhood7 小时前
java中`==`和`.equals()`区别
java·开发语言·python
小小李程序员7 小时前
Langchain4j工具调用获取不到ThreadLocal
java·后端·ai