2

SpringBoot学习-SpringCore

 7 months ago
source link: https://jasonxqh.github.io/2023/12/11/SpringBoot%E5%AD%A6%E4%B9%A0-SpringCore/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 对象的某个方法,那么我们通常的实现方法是这样的:

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 对象里面的写法也要跟着改变,比如当我们将构造方法改为以下代码时:

class B {
public B(String name) {
System.out.println("姓名:" + name);
}
public void init() {
System.out.println("你好,世界。");
}
}

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

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

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

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

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

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

4.png
  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() 方法,并返回结果。

5.png

所以,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


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

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

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


@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 实现被注入,你可以这样做:


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

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

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

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


@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:

javaCopy code
@Component
@Lazy
public class LazyBean {
// ...
}

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

在依赖注入时使用 @Lazy:

javaCopy code
public class SomeClass {

@Autowired
@Lazy
private LazyBean lazyBean;

// ...
}

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

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

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 请求。

扫描非启动类所在包

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

6.png

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

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

@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 被注入多少次,或者从容器中多少次检索,总是返回相同的对象实例。

    7.png
  2. prototype:

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

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

      8.png
  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 的创建或销毁过程中执行必要的资源分配或清理。例如,可以在初始化方法中打开文件资源或网络连接,在销毁方法中释放这些资源。

10.png

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

11.png

@Bean

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

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

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

  1. 创建 @Configuration:

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

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

    13.png

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

  3. 将 bean 注入控制器:

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

14.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK