SpringBoot学习-SpringCore

SpringBoot学习-SpringCore

IoC和DI

Spring Container 是 Spring 框架的核心部分,主要负责管理对象的生命周期和依赖关系。它的两个主要功能是控制反转(Inversion of Control, IoC)和依赖注入(Dependency Injection, DI)。这两个概念是紧密相关且经常一起使用的。

配置:

控制反转(Inversion of Control, IoC):

  • 意义: IoC 是一种设计原则,用于减少代码间的耦合。传统的程序设计中,组件间的依赖关系通常由组件自身在内部管理和控制,这会导致代码之间的强耦合和难以维护。在 IoC 中,这种控制被反转了,即不是由组件自身控制依赖关系,而是将这种控制权交给了外部容器(比如 Spring Container)。
  • 作用: IoC 使得组件间的依赖关系更加灵活,容易管理和解耦。它使得组件更加独立,易于测试和维护。

例子

我们来举一个例子,比如 A 对象中需要使用 B 对象的某个方法,那么我们通常的实现方法是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
public void init() {
// 调用 B 类中的 init 方法
B b = new B();
b.init();
}
}
class B {
public B() {
}

public void init() {
System.out.println("你好,世界。");
}
}

然而此时对象 A 和对象 B 是存在耦合的,因为一旦修改了 B 对象构造方法的参数之后,那么 A 对象里面的写法也要跟着改变,比如当我们将构造方法改为以下代码时:

1
2
3
4
5
6
7
8
class B {
public B(String name) {
System.out.println("姓名:" + name);
}
public void init() {
System.out.println("你好,世界。");
}
}

此时构造方法已经从原本无参构造方法变成了有参的构造方法,这里不考虑构造方法重载的情况,因为实际业务中,很可能是 B 类的构造方法写错了,忘记加参数了,于是后面又补充了一个参数,此时是不需要对构造方法进行重载的,那么此时,之前对象 A 里面的调用就会报错.

这就是开发中经常遇到的一个问题,那怎么解决呢?

我们可以通过将对象传递而并 new 对象的方式来解决,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A {
// 先定义一个需要依赖的 B 对象
private B b;
// 通过构造方法实现赋值(初始化)
public A(B b) {
this.b = b;
}
public void init() {
// 调用 B 类中的 init 方法
b.init();
}
}
class B {
public B(String name) {
System.out.println("姓名:" + name);
}
public void init() {
System.out.println("你好,世界。");
}
}

这样改造之后,无论构造方法怎么修改,即使需要加更多的参数,而调用它的 A 类都无需做任何修改,这样就实现了对象的解耦。

那这个解耦的示例和 IoC 有什么关系呢?

IoC 实现的思路和上述示例一样,就是通过将对象交给 Spring 中 IoC 容器管理,在其他类中不直接 new 对象,而是通过将对象传递到当前类的方式来实现解耦的。

依赖注入(Dependency Injection, DI):

  • 意义: DI 是实现 IoC 的一种方法。在这种模式下,组件的依赖不是由组件本身在内部创建或查找,而是由外部容器(比如 Spring Container)在创建组件的时候,将依赖项“注入”到组件中。这种依赖可以是对象、资源或者其他必需的元素。
  • 作用: DI 降低了组件之间的耦合度,增加了代码的可重用性和可测试性。通过 DI,组件不需要知道如何创建它们的依赖,这些依赖可以通过配置文件或注解来动态指定,从而使得组件更加灵活和模块化。

Spring 框架中两种常用的依赖注入方式:构造器注入和设值注入,以及在何种情况下推荐使用它们。

  1. 构造器注入(Constructor Injection):
    • 使用场景: 当你有必需的依赖时,也就是说,你的类不能在没有这些依赖的情况下正常工作。
    • 推荐使用: Spring 开发团队通常推荐使用构造器注入作为首选,因为它可以确保所需的依赖在类实例化时被提供,这有助于保证实例的不变性和依赖对象的不可变性。
  2. 设值注入(Setter Injection):
    • 使用场景: 当你有可选的依赖时,即使这些依赖没有被提供,你的类也能通过一些合理的默认逻辑来正常工作。
    • 特点: 设值注入允许在类实例化后某个时间点设置依赖,这提供了更大的灵活性,但也可能导致对象进入一个没有正确设置依赖的不完整状态。

Autowiring

什么是 Spring Autowiring:

  • Spring Autowiring 是依赖注入的一个功能,它允许 Spring 容器自动注入依赖关系。
  • Spring 容器会查找和需要被注入的属性匹配的类,匹配可以根据类型,即类或接口来完成。
  • 一旦找到匹配的类,Spring 将自动将其注入到需要的组件中,这就是为什么它被称为“自动装配”。

举例

  • 假设我们要注入一个实现了 Coach 接口的类的实例。
  • Spring 会扫描带有 @Component 注解的类,这些类会被标记为 Spring 管理的组件。
  • 如果找到任何实现了 Coach 接口的类,Spring 将选择一个来注入。例如,如果 CricketCoach 类实现了 Coach 掌握并且被标记为 @Component,Spring 就会创建 CricketCoach 的一个实例,并将其注入到 DemoController 中。

这里的关键点是,使用 Autowiring,开发者不需要在配置文件中手动指定依赖关系,Spring 会自动处理这些依赖关系。这简化了配置过程,也减少了配置错误的可能性。

自动装配可以通过不同的方式进行,例如:

  • 使用 @Autowired 注解在构造器、设值方法或字段上。
  • 在 XML 配置文件中使用 autowire 属性。

这两种方法都可以达到同样的目的,即让 Spring 自动解析依赖关系并进行注入,但注解方式是目前最常用和推荐的方式,因为它提供了更清晰、更简洁的依赖关系声明。

Constructor Injection

现在我们来实现一个Constructor Injecton的例子

  1. 定义依赖接口和类:
    • 这一步要求开发者定义一个接口,以及实现该接口的类。接口定义了需要的方法,而类提供了这些方法的具体实现。在 Spring 中,这通常意味着创建一个或多个类,并用 @Component 注解标记它们,以便 Spring 容器可以在运行时创建和管理它们的实例。

  1. 创建 Demo REST 控制器:
    • 接下来,创建一个 REST 控制器类,并用 @RestController 注解标记。这个控制器将处理入站的 HTTP 请求,并返回相应的 HTTP 响应。

  1. 为注入创建一个构造器:
    • 在控制器类中,创建一个构造器,并使用 @Autowired 注解(如果使用 Spring 4.3 以上版本,这个注解可以省略,只要该类只有一个构造器)。构造器的参数应该是需要注入的依赖的接口类型。Spring 容器将使用这个构造器来自动注入所需的依赖。

  1. 添加 @GetMapping/dailyworkout:
    • 最后,定义一个方法来处理特定的 HTTP GET 请求。使用 @GetMapping 注解和请求的路径(在本例中为 /dailyworkout)。这个方法将调用注入的依赖的方法,并返回一个结果,通常是一个字符串或一个对象,后者将被自动转换为 JSON。

工作流程

  1. 在 Spring 容器启动时,它会创建 DemoController 类的一个实例。
  2. 如果 DemoController 类的构造器需要一个 Coach 类型的参数,Spring 容器会查找实现了 Coach 接口的类的实例。
  3. 一旦找到匹配的 Coach 实现,Spring 容器会创建这个实现类的实例(比如 CricketCoach),如果它还未被创建。
    1. 接着,Spring 容器会通过 DemoController 的构造器将 CricketCoach 的实例(或其他 Coach 实现)注入到 DemoController 中。这通常是在构造器参数上使用 @Autowired 注解(在 Spring 4.3 之后,如果构造器只有一个参数,可以省略 @Autowired 注解)。
  4. 当请求 /dailyworkout 路径时,Spring MVC 框架会调用 getDailyWorkout() 方法。
  5. getDailyWorkout() 方法中,你会调用 myCoach.getDailyWorkout()。因为 myCoach 已经是注入的 Coach 实现类的实例,所以它会调用这个实例的 getDailyWorkout() 方法,并返回结果。

所以,myCoach 对象是在控制器被创建时通过构造器注入进来的,而不是在调用 getDailyWorkout() 方法时创建的。

Setter Injection

通过 Setter 方法实现依赖注入的编程步骤和工作流程:

编程步骤:

  1. 创建 Setter 方法:
    • 在你的类中,创建一个公共的 setter 方法,这个方法将用来注入依赖。例如,在 DemoController 类中创建一个名为 setCoach 的方法,该方法接受一个 Coach 类型的参数。
    • 事实上,只要这个方法背 autoWired注释,那么它可以是任何名字
  2. 使用 @Autowired 注解:
    • 在 setter 方法上使用 @Autowired 注解,以指示 Spring 自动装配依赖。这个注解告诉 Spring,当创建 DemoController 类的实例时,需要自动注入一个 Coach 类型的对象。

工作流程

  1. Spring 启动并创建容器:
    • 当 Spring 应用程序启动时,它会创建一个 Spring 容器,并开始组件扫描过程。
  2. 组件扫描:
    • Spring 会扫描注解了 @Component(及其特殊化版本如 @Service@Repository@Controller)的类,并为这些类创建 bean 定义。
  3. 创建 CricketCoach 实例:
    • 例如,CricketCoach 类实现了 Coach 接口,并且使用 @Component 注解标记,因此 Spring 会创建这个类的实例。
  4. 创建 DemoController 实例:
    • DemoController 类使用 @RestController 注解标记,Spring 会创建这个类的实例。
  5. 依赖注入:
    • 因为 DemoController 有一个使用 @Autowired 注解的 setCoach 方法,Spring 会调用这个方法,并将步骤 3 中创建的 CricketCoach 实例注入到 DemoController 实例中。
  6. 使用注入的依赖:
    • 一旦依赖注入完成,DemoController 就可以使用 myCoach 实例来调用 getDailyWorkout 方法,并返回相应的训练信息。

通过 setter 注入,Spring 允许开发者在不改变类构造器的情况下注入依赖,这提供了更多的灵活性。例如,如果你想要在运行时通过某种方式改变依赖,setter 方法可以很容易地重新注入一个新的依赖实例。这种方式在需要重新配置组件或是可选依赖的场景下特别有用。

@Qualifier

当有多个实现同一接口的组件时,Spring 的自动装配(Autowiring)机制需要更多的信息来决定注入哪一个实现。在默认情况下,如果没有其他指示,Spring 将无法选择多个匹配候选中的一个,这将导致 NoUniqueBeanDefinitionException

为了解决这个问题,可以使用 @Qualifier 注解来指定应该注入哪个实现。@Qualifier 注解与 @Autowired 注解一起使用,提供了一种方式来进一步细化自动装配过程。

假设你有两个实现了 Coach 接口的组件,CricketCoachFootballCoach

1
2
3
4
5
6
7
8
9
10

@Component
public class CricketCoach implements Coach {
// ...
}

@Component
public class FootballCoach implements Coach {
// ...
}

在你的 DemoController 中,如果你想要注入 CricketCoach 的实例,你需要如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13

@RestController
public class DemoController {

private Coach myCoach;

@Autowired
public void setCoach(@Qualifier("cricketCoach") Coach theCoach) {
myCoach = theCoach;
}

// ...
}

在这个例子中,@Qualifier 注解的值必须与你想要注入的 Coach 实现的 bean 名称相匹配。默认情况下,bean 的名称是其类名的首字母小写,除非你在 @Component 注解中明确指定了不同的名字。因此,@Qualifier("cricketCoach") 告诉 Spring 要注入 CricketCoach 的实例。

如果不使用 @Qualifier 注解,Spring 将不知道应该选择哪个 Coach 实现,这将导致上述的异常。通过使用 @Qualifier,开发者可以明确指示 Spring 使用哪个特定的 bean,解决了多个符合条件组件的歧义性问题。

@Primary

@Primary 注解在 Spring 中用于给多个相同类型的 bean 中的一个标记为首选的 bean。当自动装配一个特定类型的 bean 时,如果存在多个候选者,并且其中一个候选者被标记为 @Primary,Spring 将优先选择这个被标记的 bean 进行注入。

例如,如果你有两个实现了 Coach 接口的类 CricketCoachFootballCoach,并且你想要 FootballCoach 作为主要的 Coach 实现被注入,你可以这样做:

1
2
3
4
5
6
7
8
9
10
11

@Component
public class CricketCoach implements Coach {
// ...
}

@Component
@Primary
public class FootballCoach implements Coach {
// ...
}

在这个例子中,即使 CricketCoach 也是一个有效的 Coach 类型的 bean,FootballCoach 会被作为首选注入,因为它被标记了 @Primary

当在一个组件中自动装配 Coach 类型的 bean 时:

1
2
3
4
5
6
7
8
9

@RestController
public class DemoController {

@Autowired
private Coach myCoach;

// ...
}

在这里,由于 FootballCoach 被标记为 @PrimarymyCoach 将会引用 FootballCoach 的实例,即使没有使用 @Qualifier 注解。

但是,即使一个组件被标记为 @Primary,仍然可以使用 @Qualifier 注解。实际上,@Qualifier 注解优先级高于 @Primary,这意味着 @Qualifier 提供了一种方式来覆盖 @Primary 的首选项。

关于多个组件同时使用 @Primary 的问题,如果在同一类型的多个bean上使用了 @Primary,这将导致冲突,因为 Spring 不会知道在不使用 @Qualifier 注解的情况下应该选择哪一个。这通常会导致 Spring 在启动时抛出异常,因为它不能解决多个 @Primary bean 之间的歧义。

@Primary 注解非常有用,特别是当我们正在编写不能修改的代码(比如,正在使用一个第三方库),或者想要在大部分情况下使用一个默认实现,但在某些特定情况下覆盖这个默认实现。通过结合 @Primary@Qualifier 注解,我们可以获得灵活而强大的依赖注入能力。

@Lazy

@Lazy 注解在 Spring 框架中用于控制 bean 的加载行为。当在一个 bean 上标注 @Lazy 注解时,这个 bean 不会在启动时立即创建,而是在第一次请求这个 bean 时才创建。这可以加快应用程序启动的速度,尤其是在有很多单例 bean 时,因为它们不会在启动时全部初始化。

作用:

  • 提高启动性能:延迟加载bean可以减少应用程序启动时的初始化负载,特别是当某些bean的创建非常耗时,或者依赖于启动后才可用的资源时。
  • 按需使用资源:如果应用中有些bean用得不多,使用 @Lazy 可以确保只有在实际需要时才创建这些bean,从而节省资源。

使用示例:

在单个 bean 上使用 @Lazy:

1
2
3
4
5
6
javaCopy code
@Component
@Lazy
public class LazyBean {
// ...
}

在这个例子中,LazyBean 只有在首次被注入或检索时才会被创建和初始化。

在依赖注入时使用 @Lazy:

1
2
3
4
5
6
7
8
9
javaCopy code
public class SomeClass {

@Autowired
@Lazy
private LazyBean lazyBean;

// ...
}

在这里,lazyBean 的实例化将会延迟到 SomeClass 首次使用 lazyBean 时。

全局的 Lazy 初始化可以通过在 application.properties 文件中设置一个属性来实现:

1
2
arduinoCopy code
spring.main.lazy-initialization=true

当这个属性设置为 true 时,它会影响 Spring 应用程序上下文中所有 bean 的默认行为,使得所有的 bean 都采用懒加载策略。这意味着 Spring 容器在启动时不会创建任何 bean 的实例,除非它们被显式地请求,例如,一个 REST 控制器的端点被访问时。

Component Scaning

应用启动流程

  1. 启动 Spring 应用程序:
    • main 方法调用 SpringApplication.run(SpringcoredemoApplication.class, args);,这是启动 Spring 应用程序的标准方式。它启动了 Spring 的上下文。
  2. 处理 @SpringBootApplication 注解:
    • @SpringBootApplication 是一个方便的注解,它包含了 @EnableAutoConfiguration@ComponentScan@Configuration 这三个注解。Spring Boot 会处理这个复合注解,并激活它包含的三个注解的功能。
    • 自动配置 (@EnableAutoConfiguration):
      • Spring Boot 会尝试根据添加到 classpath 中的 jar 依赖来自动配置你的 Spring 应用。例如,如果 classpath 下有 H2 数据库的 jar,它可能会自动配置一个内存数据库。
    • 组件扫描 (@ComponentScan):
      • Spring Boot 会扫描启动类 SpringcoredemoApplication 所在的包以及子包,默认情况下不扫描其他包。并查找带有 @Component@Service@Repository@Controller 等注解的类,并将它们注册为 Spring 应用程序上下文中的 bean。
    • 额外的配置 (@Configuration):
      • 这允许在应用程序中定义额外的配置类。这些类中可以使用 @Bean 注解来注册更多的 bean 到 Spring 应用程序上下文中,或者使用 @Import 注解来引入其他配置类。
  3. 创建和注册 Bean:
    • 在自动配置和组件扫描的基础上,Spring Boot 会创建和注册所有识别出的 bean,包括从配置类中定义的 bean。
  4. 解决 Bean 之间的依赖关系:
    • 在所有的 bean 都被创建和注册后,Spring 容器会解决它们之间的依赖关系,并通过构造器、设值方法或字段注入完成自动装配。
  5. 应用程序准备就绪:
    • 一旦上下文被创建并且所有的 bean 都被正确装配,应用程序就准备好可以接受请求了。对于 web 应用程序,这通常意味着内嵌的 Tomcat、Jetty 或 Undertow 服务器已经启动并且开始监听 HTTP 请求。

扫描非启动类所在包

当应用程序的组件不仅仅位于启动类所在的包和其子包时,会出错,如下所示:

此时,我们需要告诉 Spring Boot 去扫描其他的包。图片中展示了如何使用 @SpringBootApplication 注解的 scanBasePackages 属性来明确列出 Spring Boot 在启动时应该扫描的基础包。

如果有组件分布在不同的包中,例如 com.luv2code.utilorg.acme.cart,和 edu.cmu.srs,您可以按如下方式配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication(scanBasePackages={
"com.luv2code.springcoredemo",
"com.luv2code.util",
"org.acme.cart",
"edu.cmu.srs"
})
public class SpringcoredemoApplication {

public static void main(String[] args) {
SpringApplication.run(SpringcoredemoApplication.class, args);
}

}

在这个例子中,scanBasePackages 属性包含了一个数组,列出了所有需要被 Spring Boot 组件扫描过程所考虑的包。这样,Spring Boot 就会在启动时扫描这些包以及它们的子包,查找带有 @Component@Service@Repository@Controller 等注解的类,并自动注册为 Spring 应用程序上下文中的 bean。

这使得您可以组织和管理位于不同包中的 Spring 组件,无论它们是否位于启动类所在的包的外部。

Bean Scope

Bean Scope 在 Spring 框架中定义了一个 bean 的生命周期和可见性。它决定了一个 bean 实例是如何被创建、复用以及管理。

不同类型的 Spring Bean Scopes:

  1. singleton:

    • 这是 Spring 默认的 scope。当一个 bean 定义为 singleton,Spring IoC 容器将只为这个 bean 定义创建一个共享的实例。无论给定的 bean 被注入多少次,或者从容器中多少次检索,总是返回相同的对象实例。

  2. prototype:

    • 如果一个 bean 定义为 prototype,Spring IoC 容器每次请求都会创建一个新的 bean 实例。方法如下所示,即11目标bean用@Scope(ConfigurbleBeanFactory.SCOPE_PROTOTYPE)修饰

    • 这意味着如果你对同一个 bean 进行了多次请求,每次都会得到一个新创建的对象。

  3. request:

    • 这个 scope 仅适用于 web 应用程序。在 request scope 中,每个 HTTP 请求都会创建一个新的 bean 实例。这意味着在同一个请求内部,相同的 bean 将返回相同的实例,但不同请求会导致创建新的实例。
  4. session:

    • session scope 也是专门为 web 应用程序设计的。在 session scope 中,每个 HTTP session 都会创建一个新的 bean 实例。这个 bean 与用户的 HTTP session 相关联,并在 session 的生命周期内共享。
  5. global-session:

    • global-session scope 用于 portlet 应用程序,并且通常用于 Spring 的 Portlet framework。它类似于 session scope,但是它提供了跨多个 servlet context 的全局 HTTP session(通常在 Portlet 环境中使用)。

理解不同的 bean scopes 对于设计合适的 Spring 应用程序是非常重要的,因为不同的 scope 影响了应用程序的行为和性能。例如,使用 singleton scope 可以减少内存的使用,因为它限制了实例的数量;而使用 prototype scope 可以确保每个组件使用一个全新的实例,这在某些特定的业务场景下可能是必需的。对于 web 应用程序来说,request 和 session scopes 允许 bean 的状态和生命周期与用户的交互周期相匹配。

Bean LifeCycle

在 Spring 框架中,Bean 生命周期指的是从创建 Bean 到 Bean 被销毁的整个过程。在这个过程中,可以通过定义特定的方法来钩入生命周期的特定点,这些方法可以在 Bean 的初始化和销毁时执行自定义的逻辑。

Bean 生命周期及其自定义的钩子方法(即生命周期回调方法)可以如下描述:

  1. Bean 定义:
    • Bean 的定义由 Spring 容器通过读取配置文件、注解或 Java 配置类进行加载。
  2. Bean 实例化:
    • Spring 容器创建 Bean 的实例,通常是通过调用构造函数来完成。
  3. 依赖注入:
    • 如果 Bean 依赖于其他 Bean,则这些依赖关系被注入到当前 Bean 中。
  4. 内部 Spring 处理:
    • Bean 可能会被 AOP 代理包装,并且可能会应用 Bean 后置处理器(BeanPostProcessor)。
  5. 初始化回调:
    • 容器调用自定义的初始化方法。这些方法可以通过实现 InitializingBean 接口或通过 @PostConstruct 注解来指定。
  6. Bean 可用:
    • 此时,Bean 完全初始化,并准备好被应用程序使用。
  7. 容器关闭:
    • 当应用程序关闭时,Spring 容器会被关闭。
  8. 销毁回调:
    • 在容器关闭过程中,如果 Bean 实现了 DisposableBean 接口或定义了 @PreDestroy 注解的方法,容器将调用这些销毁方法。

通过这个生命周期,您可以在 Bean 的创建或销毁过程中执行必要的资源分配或清理。例如,可以在初始化方法中打开文件资源或网络连接,在销毁方法中释放这些资源。

例子:

可以看到,在CricketCoach被初始化之后,会调用doMyStarupStuff钩子函数

@Bean

在 Spring 框架中,@Component 注解通常用于自定义的类,它告诉 Spring 容器这个类是一个 Spring 组件,需要作为一个 bean 进行实例化和管理。使用 @Component 的前提是你可以修改类的源码来添加这个注解。

然而,在某些情况下,特别是当使用第三方库或者框架时,你没有权限去修改源代码,也就不能直接在这些类上添加 @Component 注解。在这种情况下,你需要使用 @Bean 注解来配置这些类的实例。

使用 Java 代码配置 beans 的步骤如下:

  1. 创建 @Configuration:

    • 创建一个类,并使用 @Configuration 注解标记它。这个类将作为配置信息的来源,Spring 容器将会扫描这个类以了解相关的 bean 定义。
  2. 定义 @Bean 方法:

    • 在这个配置类内部,定义一个或多个方法,并使用 @Bean 注解。每个方法都将创建一个 bean,并且方法名默认为 bean 的名称。这些方法应该返回你想要由 Spring 容器管理的对象的实例。每个这样的方法实质上都是一个工厂方法,它告诉 Spring 容器如何创建这个 bean。

    在这个例子中,@Bean 注解使用 "aquatic" 作为 bean 的名字,这意味着你可以使用这个名字来引用创建的 bean 实例。

  3. 将 bean 注入控制器:

    • 在你的控制器(或其他需要这些 beans 的组件)中,你可以通过自动装配(使用 @Autowired)来注入步骤 2 中创建的 beans。Spring 容器会在运行时自动处理这些依赖关系。在需要依赖注入的类中,比如 DemoController,可以使用 @Autowired@Qualifier 注解来注入特定的 Coach 实例。@Qualifier 注解的值应该与 @Bean 方法中定义的名称相匹配。

-------------本文结束,感谢您的阅读-------------