vlambda博客
学习文章列表

读书笔记《hands-on-high-performance-with-spring-5》Spring最佳实践和Bean布线配置

Spring Best Practices and Bean Wiring Configurations

在上一章中,我们了解了 Spring Framework 如何实现 Inversion of Control (IoC) 原理。 Spring IoC 是实现对象依赖之间松耦合的机制。 Spring IoC 容器是将依赖项注入对象并使其准备好供我们使用的程序。 Spring IoC 也称为依赖注入。在 Spring 中,应用程序的对象由 Spring IoC 容器管理,也称为 beans。 bean 是由 Spring IoC 容器实例化、组装和管理的对象。因此,Spring 容器负责在应用程序中创建 bean 并通过依赖注入协调这些对象之间的关系。但是,告诉 Spring 要创建哪些 bean 以及如何将它们配置在一起是开发人员的责任。在传递 bean 布线配置时,Spring 非常灵活,提供不同的编写配置。

在本章中,我们首先开始探索不同的 bean 布线配置。这包括使用 Java、XML 和注释进行配置,还包括学习 bean 连接配置的不同最佳实践。我们还将了解不同配置的性能评估,以及依赖注入陷阱。

本章将涵盖以下主题:

  • Dependency injection configurations
  • Performance assessment with different configurations
  • Dependency injection pitfalls

Dependency injection configurations

在任何应用程序中,对象与其他对象协作以执行一些有用的任务。任何应用程序中一个对象与另一个对象之间的这种关系会产生依赖关系,而对象之间的这种依赖关系会在应用程序中产生紧密耦合的编程。 Spring 为我们提供了一种将紧耦合编程转换为松耦合编程的机制。这种机制称为依赖注入 (DI)。 DI 是一种概念或设计模式,它描述了如何创建松散耦合的类,其中对象的设计方式是从其他代码片段接收对象的实例,而不是在内部构造它们。这意味着对象在运行时而不是编译时被赋予它们的依赖关系。因此,使用 DI,我们可以获得一个解耦结构,为我们提供简化的测试、更高的可重用性和更高的可维护性。

在下一节中,我们将了解不同类型的 DI 配置,您可以根据业务需求在应用程序的任何配置中使用这些配置。

Types of DI patterns

在 Spring 中,执行以下类型的 DI:

  • Constructor-based DI
  • Setter-based DI
  • Field-based DI

我们将在接下来的部分中了解有关这些的更多信息。

Constructor-based DI

基于构造函数的 DI 是一种设计模式,用于解决依赖对象的依赖关系。在基于构造函数的 DI 中,构造函数用于注入依赖对象。它是在容器调用带有多个参数的构造函数时完成的。

让我们看一下基于构造函数的 DI 的以下示例。在下面的代码中,我们展示了如何使用构造函数在 BankingService 类中注入 CustomerService 对象:

@Component
public class BankingService {

  private CustomerService customerService;

  // Constructor based Dependency Injection
  @Autowired
  public BankingService(CustomerService customerService) {
    this.customerService = customerService;
  }

  public void showCustomerAccountBalance() {
    customerService.showCustomerAccountBalance();
  }

}

下面是另一个依赖类文件CustomerServiceImpl.java的内容:

public class CustomerServiceImpl implements CustomerService {

  @Override
  public void showCustomerAccountBalance() {
    System.out.println("This is call customer services");
  }
}

CustomerService.java接口的内容如下:

public interface CustomerService {  
  public void showCustomerAccountBalance(); 
}

Advantages of the constructor-based DI

以下是 Spring 应用程序中基于构造函数的 DI 的优点:

  • It's suitable for mandatory dependencies. In a constructor-based DI, you can be sure that the object is ready to be used the moment it is constructed.
  • The code structure is very compact and clear to understand.
  • When you need an immutable object then, through constructor-based dependency, you can ensure you get the immutable nature of the object.

Disadvantages of the constructor-based DI

基于构造函数的注入唯一的缺点是它可能导致对象之间的循环依赖。循环依赖意味着两个对象相互依赖。为了解决这个问题,我们应该使用 setter 注入而不是构造函数注入。

让我们看看 Spring 中另一种类型的 DI,它是基于 setter 的注入。

Setter-based DI

在基于构造函数的 DI 中,我们看到一个依赖对象通过构造函数参数注入。 在基于setter的DI中,依赖对象由依赖类中的一个 setter 方法提供。基于setter的DI是通过调用bean之后调用setter方法来完成的< kbd>no-args 构造函数通过容器。

在下面的代码中,我们展示了如何使用 setter 方法在 BankingService 类中注入 CustomerService 对象:

@Component
public class BankingService {

  private CustomerService customerService;  

  // Setter-based Dependency Injection
  @Autowired
  public void setCustomerService(CustomerService customerService) {
  this.customerService = customerService;
  }

  public void showCustomerAccountBalance() {
    customerService.showCustomerAccountBalance();
  }

}

Advantages of the setter-based DI

以下是 Spring 应用中基于 setter 的 DI 的优点:

  • It's more readable than the constructor injection.
  • This is useful for non-mandatory dependencies.
  • It solves the circular dependency problem in the application.
  • It helps us to inject the dependency only when it is required.
  • It's possible to reinject dependencies. It is not possible in a constructor-based injection.

Disadvantages of the setter-based DI

虽然基于 setter 的 DI 比基于构造函数的 DI 具有更高的优先级,但以下是前者的缺点:

  • There is no guarantee in a setter-based DI that the dependency will be injected.
  • One can use a setter-based DI to override another dependency. This can cause security issues in a Spring application.

Field-based DI

在前面的部分中,我们看到了如何在应用程序中使用基于构造函数和基于 setter 的依赖项。在以下示例中,我们将看到基于字段的 DI。实际上,基于字段的 DI 易于使用,与其他两种注入方法相比,它具有干净的代码; 但是,它有几个严重的权衡,通常应该避免。

让我们看一下基于字段的 DI 的以下示例。在下面的代码中,我们将看到如何在 BankingService 类中使用字段来注入 CustomerService 对象:

@Component
public class BankingService {

  //Field based Dependency Injection
  @Autowired
  private CustomerService customerService;

  public void showCustomerAccountBalance() {
    customerService.showCustomerAccountBalance();
  }

}

正如我们所讨论的,这种类型的 DI 具有消除基于 setter 或基于构造函数的依赖项的杂乱代码的好处,但它有许多缺点,例如从外部看不到依赖项。在基于构造函数和基于 setter 的依赖项中,类使用 public 接口或 setter 方法清楚地公开这些依赖项。在基于字段的 DI 中,该类本质上对外界隐藏了依赖关系。另一个困难是字段注入不能用于将依赖项分配给最终/不可变字段,因为这些字段必须在类实例化时实例化。

Generally, Spring discourages the use of the field-based dependency.

这是一个图表,其中包含我们迄今为止所了解的不同类型的 DI:

读书笔记《hands-on-high-performance-with-spring-5》Spring最佳实践和Bean布线配置

Constructor versus setter injection

如我们所见,Spring 支持三种类型的 DI 方法;但是,Spring 不推荐基于字段的依赖。因此,基于构造函数和基于 setter 的 DI 是在应用程序中注入 bean 的标准方法。构造函数或设置方法的选择取决于您的应用程序要求。在这张表中,我们将看到构造函数和 setter 注入的不同用例,以及一些有助于我们决定何时使用 setter 注入而不是构造函数注入的最佳实践,反之亦然:

构造函数注入

Setter 注入

强制依赖时的最佳选择。

当依赖不是强制性的时的合适选择。

构造函数注入使 bean 类对象不可变。

Setter 注入使 bean 类对象可变。

构造函数注入不能覆盖 setter 注入的值。

当我们对同一属性同时使用构造函数和 setter 注入时,Setter 注入会覆盖构造函数注入。

构造函数注入不可能实现部分依赖,因为我们必须在构造函数中传递所有参数,否则会出错。

使用 setter 注入可以实现部分依赖。假设我们有 intstringlong 三个依赖项,那么在 setter 注入的帮助下,我们只能注入所需的依赖;其他依赖项将被视为这些原语的默认值。

在对象之间创建循环依赖。

解决应用程序中的循环依赖问题。在循环依赖的情况下,最好使用 setter 而不是构造函数注入。

Configuring the DI with Spring

在本节中,我们将学习配置 DI 的不同类型的流程。下图是 Spring 中配置过程如何工作的高级视图:

读书笔记《hands-on-high-performance-with-spring-5》Spring最佳实践和Bean布线配置

根据上图,Spring 容器负责在您的应用程序中创建 bean,并通过 DI 模式在这些 bean 之间建立关系;然而,正如我们之前所讨论的,开发人员有责任通过元数据告诉 Spring 容器如何创建 bean 以及如何将它们连接在一起。

以下是配置应用程序元数据的三种技术:

  • XML-based configuration: An explicit configuration
  • Java-based configuration: An explicit configuration
  • Annotation-based configuration: An implicit configuration

在 Spring Framework 中,可以使用上述三种配置机制,但是您必须使用其中一种配置过程来连接您的 bean。在下一节中,我们将通过示例详细了解每种配置技术,并了解每种情况或条件下哪种技术比其他技术更适合;但是,您可以使用最适合您的任何技术或方法。

现在让我们详细了解具有基于 XML 的配置的 DI 模式。

XML-based configuration

基于 XML 的配置自 Spring 开始以来一直是主要的配置技术。在本节中,我们将看到与 DI 模式中讨论的相同示例,并了解如何通过基于 XML 的配置将 CustomerService 对象注入到 BankingService 类中.

对于基于 XML 的配置,我们需要创建一个带有 <beans> 元素的 applicationContext.xml 文件。 Spring 容器必须能够管理应用程序中的一个或多个 bean。 Bean 使用顶级 元素内的 元素进行描述。

以下是 applicationContext.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
      
    <!-- Bean Configuration definition describe here -->
    <bean class=""/>
    
</beans> 

前面的 XML 文件是基于 XML 的配置元数据的基本结构,我们需要在其中定义我们的 bean 配置。正如我们之前所了解的,我们的 bean 配置模式可能是基于构造函数的,也可能是基于 setter 的,这取决于应用程序的需求。现在,我们将看到如何使用两种设计模式一一配置 bean。

以下是具有基于 XML 配置的基于构造函数的 DI 的示例:

<!-- CustomerServiceImpl Bean -->
<bean id="customerService" class="com.packt.springhighperformance.ch2.bankingapp.service.Impl.CustomerServiceImpl" />

<!-- Inject customerService via constructor argument -->
<bean id="bankingService" class="com.packt.springhighperformance.ch2.bankingapp.model.BankingService">
<constructor-arg ref="customerService" />
</bean>

在前面的示例中,我们使用构造函数 DI 模式在 BankingServices 类中注入了 CustomerService 对象。 元素的ref 属性用于传递CustomerServiceImpl 对象的引用。

以下是具有基于 XML 配置的基于 setter 的 DI 的示例:

<!-- CustomerServiceImpl Bean -->
<bean id="customerService" class="com.packt.springhighperformance.ch2.bankingapp.service.Impl.CustomerServiceImpl" />

<!-- Inject customerService via setter method -->
<bean id="bankingService" class="com.packt.springhighperformance.ch2.bankingapp.model.BankingService"> 
<property name="customerService" ref="customerService"></property></bean>

</property> 元素的 ref 属性用于将 CustomerServiceImpl 对象的引用传递给 setter 方法。

以下是MainApp.java文件的内容:

public class MainApp {

public static void main(String[] args) {
    @SuppressWarnings("resource")
    ApplicationContext context = new               
    ClassPathXmlApplicationContext("applicationContext.xml");
    BankingService bankingService = 
    context.getBean("bankingService",                            
    BankingService.class);
    bankingService.showCustomerAccountBalance(); 
  }
}

Java-based configuration

在上一节中,我们看到了如何使用基于 XML 的配置来配置 bean。在本节中,我们将看到基于 Java 的配置。与 XML 一样,基于 Java 的配置也显式地注入依赖项。以下示例定义了 Spring bean 及其依赖项:

@Configuration
public class AppConfig { 
  
  @Bean
  public CustomerService showCustomerAccountBalance() {
    return new CustomerService();
  }
  
  @Bean
  public BankingService getBankingService() {
    return new BankingService();
  }  
}

在基于Java的配置中,我们必须用@Configuration注解类,而bean的声明可以通过@Bean注解来实现。前面的基于 Java 的配置示例等价于基于 XML 的配置,如下代码所示:

<beans>
<bean id="customerService" class="com.packt.springhighperformance.ch2.bankingapp.service.Impl.CustomerServiceImpl" /> 

<bean id="bankingService" class="com.packt.springhighperformance.ch2.bankingapp.model.BankingService/">
</beans>

前面的 AppConfig 类使用 @Configuration 注释进行注释,该注释描述它是包含 bean 定义详细信息的应用程序的配置类。该方法使用 @Bean 注释来描述它负责实例化、配置和初始化一个新的 bean,该 bean 将由 Spring IoC 容器管理。在 Spring 容器中,每个 bean 都有一个唯一的 ID。无论哪个方法使用 @Bean 注释,那么默认情况下,该方法名称将是 bean ID;但是,您也可以使用 @Bean 注释的 name 属性覆盖该默认行为,如下所示:

@Bean(name="myBean")
  public CustomerService showCustomerAccountBalance() {
    return new CustomerService();
  }

Spring 应用程序上下文将加载 AppConfig 文件并为应用程序创建 bean。

以下是 MainApp.java 文件:

public class MainApp {

  public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new                                                 
    AnnotationConfigApplicationContext(AppConfig.class);
    BankingService bankingService = 
    context.getBean(BankingService.class);
    bankingService.showCustomerAccountBalance();
    context.close();     
  }
}

Annotation-based configuration

在上一节中,我们看到了基于 Java 和基于 XML 的两种 bean 配置技术。这两种技术都显式地注入了依赖关系。在基于 Java 的环境中,我们使用 AppConfig Java 文件中的 @Bean 注释方法,而在基于 XML 的环境中,我们使用 元素标记。 基于注解的配置是另一种创建 bean 的方式,我们可以使用相关类、方法或字段声明上的注解将 bean 配置移动到组件类本身。在这里,我们将看看如何通过注解配置 bean,以及 Spring Framework 中可用的不同注解。

在 Spring 中,基于注解的配置默认是关闭的,所以首先,您必须通过在 Spring XML 文件中输入 元素来打开它,如下所示.添加后,您就可以在代码中使用注释了。

applicationContext.xml 中需要进行的更改(正如我们在前面部分中使用的那样)突出显示如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<!-- Enable Annotation based configuration -->
<context:annotation-config />
<context:component-scan base-package="com.packt.springhighperformance.ch2.bankingapp.model"/><context:component-scan base- package="com.packt.springhighperformance.ch2.bankingapp.service"/>
    
<!-- Bean Configuration definition describe here -->
<bean class=""/>
    
</beans>
基于 XML 的配置将覆盖注解,因为基于 XML 的配置将在注解之后注入。

前面基于 XML 的配置表明,一旦您配置了 <context:annotation-config/> 元素,它就表示开始注释您的代码。 Spring 应该自动扫描 中定义的包,并根据模式识别 bean 并连接它们。让我们了解一些重要的注释,以及它们是如何工作的。

The @Autowired annotation

@Autowired 注释隐式地注入对象依赖。我们可以在构造函数设置器和基于字段的依赖模式上使用 @Autowired 注释。 @Autowired 注释表示应该为此 bean 执行自动连接。

让我们看一个在基于构造函数的依赖项上使用 @Autowired 注释的示例:

public class BankingService {  

  private CustomerService customerService;
  
  @Autowired
  public BankingService(CustomerService customerService) {
    this.customerService = customerService;
  }
  ......
}

在前面的示例中,我们有 BankingService,它具有 CustomerService 的依赖项。它的构造函数用 @Autowired 注释,表明 Spring 使用带注释的构造函数实例化 BankingService bean,并将 CustomerService bean 作为依赖项传递BankingService bean。

从 Spring 4.3 开始, @Autowired 注释在具有单个构造函数的类上变为可选。在前面的例子中,Spring 仍然会注入一个 CustomerService 类,如果你跳过 @Autowired 注释。

让我们看一个在基于 setter 的依赖项上使用 @Autowired 注释的示例:

public class BankingService {
  
  private CustomerService customerService; 
    
  @Autowired
  public void setCustomerService(CustomerService customerService) {
    this.customerService = customerService;
  }
  ......
}

在前面的示例中,我们看到 setter 方法 setCustomerService 使用 @Autowired 注释进行了注释。在这里,注解按类型解析依赖关系。 @Autowire 注释可用于任何传统的 setter 方法。

让我们看一个在基于字段的依赖项上使用 @Autowired 注释的示例:

public class BankingService {
  
  @Autowired
  private CustomerService customerService; 

}

根据前面的示例,我们可以看到 @Autowire 注释也可以添加到公共和私有属性上。 Spring 在属性上添加时使用反射 API 来注入依赖项,这就是私有属性也可以注释的原因。

@Autowired with required = false

默认情况下,@Autowired 注释暗示依赖项是必需的。这意味着当依赖项未解决时将引发异常。您可以使用带有 @Autowired(required=false) 选项覆盖该默认行为。让我们看看下面的代码:

public class BankingService {
  
  private CustomerService customerService; 
    
  @Autowired (required=false)
  public void setCustomerService(CustomerService customerService) {
    this.customerService = customerService;
  }
  ......
}

在前面的代码中,如果我们将 required 的值设置为 false,那么在 bean 连接的时候,如果依赖没有解决,Spring 就会让 bean 不连接。根据 Spring 的最佳实践,我们应该避免将 required 设置为 false,直到绝对需要。

The @Primary annotation

默认情况下,在 Spring Framework 中,DI 是按类型完成的,也就是说当存在多个同类型依赖时,会抛出 NoUniqueBeanDefinitionException 异常。这表明 Spring 容器无法为 DI 选择 bean,因为有多个符合条件的候选者。在这种情况下,我们可以使用 @Primary 注释并控制选择过程。让我们看看下面的代码:

public interface CustomerService {  
  public void customerService(); 
}

@Component
public class AccountService implements CustomerService {
      ....
}
@Component
@Primary
public class BankingService implements CustomerService { 
     ....
}

在前面的示例中,有两个可用的客户服务:BankingServiceAccountService。由于 @Primary 注释,组件只能使用 BankingService 来连接对 CustomerService 的依赖。

The @Qualifier annotation

当只能为多个 autowire 候选者确定一个主要候选者时,使用 @Primary 处理多个 autowire 候选者会更有效。 @Qualifier 注释使您可以更好地控制选择过程。它允许您提供与特定 bean 类型关联的引用。该引用可用于限定需要自动装配的依赖项。我们来看下面的代码:

@Component
public class AccountService implements CustomerService {

}
@Component
@Qualifier("BankingService")
public class BankingService implements CustomerService { 

}

@Component
public class SomeService {
  
  private CustomerService customerService;

  @Autowired
  @Qualifier("bankingservice")
  public BankingService(CustomerService customerService) {
    this.customerService = customerService;
  }
  .....
}

在前面的示例中,有两个可用的客户服务:BankingServiceAccountService;但是,由于 SomeService 类中使用了 @Qualifier("bankingservice"),因此将选择 BankingService 进行自动连接。

Automatic bean detection with stereotype annotations

在上一节中,我们了解了仅处理连线的 @Autowired 注释。您仍然必须自己定义 bean,以便容器知道它们并可以为您注入它们。 Spring Framework 为我们提供了一些特殊的注解。这些注解用于在应用程序上下文中自动创建 Spring bean。因此,无需使用基于 XML 或基于 Java 的配置显式配置 bean。

以下是 Spring 中的构造型注解:

  • @Component
  • @Service
  • @Repository
  • @Controller

我们来看下面的CustomerService 实现类。它的实现用 @Component 注释。请参考以下代码:

@Component
public class CustomerServiceImpl implements CustomerService {

  @Override
  public void customerService() {
    System.out.println("This is call customer services");

  }

}

在前面的代码中,CustomerServiceImpl 类使用 @Component 注释进行注释。这意味着标有 @Component 注释的类被视为 bean,Spring 的组件扫描机制扫描该类,创建该类的 bean,并将其拉入应用程序上下文.因此,无需显式配置该类,因为 bean 使用 XML 或 Java。 Spring 自动创建 CustomerServiceImpl 类的 bean,因为它是用 @Component 注释的。

在 Spring 中,@Service@Repository@Controller@Component 注释的元注释。从技术上讲,所有注解都是相同的,并提供相同的结果,例如在 Spring 上下文中创建一个 bean;但是我们应该在应用程序的不同层使用更具体的注释,因为它更好地指定了意图,并且将来可能会依赖其他行为。

下图描述了带有适当层的 stereotype 注解:

读书笔记《hands-on-high-performance-with-spring-5》Spring最佳实践和Bean布线配置

根据前面的示例,@Component 足以创建 CustomerService 的 bean。但是 CustomerService 是一个服务层类,因此根据 bean 配置最佳实践,我们应该使用 @Services 而不是通用注解 @Component。让我们看一下使用 @Service 注释注释的同一类的以下代码:

@Service
public class CustomerServiceImpl implements CustomerService {

  @Override
  public void customerService() {
    System.out.println("This is call customer services");
  }

}

让我们看看 @Repository 注释的另一个例子:

@Repository
public class JdbcCustomerRepository implements CustomerRepository {

}

在前面的示例中,该类使用 @Repository 注释进行注释,因为 CustomerRepository 接口在 Data Access Object( DAO) 应用程序层。根据 bean 配置最佳实践,我们使用了 @Repository 注释而不是 @Component 注释。

在现实生活中,您可能会遇到需要使用 @Component 注释。大多数情况下,您将使用 @Controller, @Service,以及 @Repository 注释。 @Component 应该在你的类不属于以下三个类别时使用:服务, 控制器, DAO

The @ComponentScan annotation

Spring 需要知道哪些包包含 Spring bean,否则,您必须单独注册每个 bean。这就是 @ComponentScan 的用途。在 Spring 中,默认情况下不启用组件扫描。我们需要使用 @ComponentScan 注释来启用它。此注解与 @Configuration 注解一起使用,以允许 Spring 知道要扫描注解组件并从中创建 bean 的包。让我们看一下 @ComponentScan 的以下简单示例:

@Configuration
@ComponentScan(basePackages="com.packt.springhighperformance.ch2.bankingapp.model")
public class AppConfig {

}

@ComponentScan注解中,如果未定义basePackages属性,则扫描将从声明该注解的类的包开始。在前面的示例中,Spring 将扫描 com.packt.springhighperformance.ch2.bankingapp.model 的所有类,以及该包的子包。 basePackages 属性可以接受字符串数组,这意味着我们可以定义多个基础包来扫描应用程序中的组件类。让我们看一个如何在 basePackage 属性中声明多个包的示例:

@Configuration
@ComponentScan(basePackages={"com.packt.springhighperformance.ch2.bankingapp.model","com.packt.springhighperformance.ch2.bankingapp.service"})
public class AppConfig {
  
}

The @Lazy annotation

默认情况下,所有自动装配的依赖项都是在启动时创建和初始化的,这意味着 Spring IoC 容器在应用程序启动时创建所有 bean;但是,我们可以通过使用 @Lazy 注释在启动时控制 bean 的预初始化。

@Lazy 注释可用于任何直接或间接使用 @Component 注释的类,或使用 @Bean 注释的方法。当我们使用 @Lazy 注释时,这意味着 bean 仅在第一次被请求时才会被创建和初始化。

我们知道注解需要更少的代码,因为我们不需要编写代码来显式注入依赖项。它也有助于我们减少开发时间。虽然 annotation 提供了很多优点,但它也有其缺点。

注解的缺点如下:

  • Less documentation than explicit wiring
  • If we have a lot of dependency in a program, then it's hard to find it by using the autowire attribute of bean
  • Annotation makes the process of debugging hard
  • It might give unexpected results in case of ambiguity
  • Annotation can be overridden by explicit configuration, such as Java or XML

Spring bean scopes

在上一节中,我们学习了各种 DI 模式,并了解了如何在 Spring 容器中创建 bean。我们还学习了各种 DI 配置,例如 XML、Java 和注解。在本节中,我们将了解有关 Spring 容器中可用的 bean 生命和范围的更多详细信息。 Spring 容器允许我们在配置级别控制 bean。这是在配置级别而不是在 Java 类级别定义对象范围的一种非常灵活的方式。在 Spring 中,bean 是通过 scope 属性控制的,该属性定义了必须创建和返回的对象类型。当您描述 <bean> 时,您可以选择为该 bean 定义 scope。 bean scope 描述了该bean 在其使用的上下文中的生命周期和可见性。在本节中,我们将看到 Spring Framework 中不同类型的 bean scope

这是在基于 XML 的配置中定义 bean scope 的示例:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
      
<!-- Here scope is not defined, it assume default value 'singleton'. It creates only one instance per spring IOC. -->
<bean id="customerService" class="com.packt.springhighperformance.ch2.bankingapp.service.Impl.CustomerServiceImpl" />
     
<!-- Here scope is prototype, it creates and returns bankingService object for every call-->
<bean id="bankingService" class="com.packt.springhighperformance.ch2.bankingapp.model.BankingService" scope="prototype">

<bean id="accountService" class="com.packt.springhighperformance.ch2.bankingapp.model.AccountService" scope="singleton">
        
</beans>

这是使用 @Scope 注释定义 bean scope 的示例:

@Configuration
public class AppConfig { 
  
  @Bean
  @Scope("singleton")
  public CustomerService showCustomerAccountBalance() {
    return new CustomerServiceImpl();

  }
}

我们还可以通过以下方式使用常量而不是字符串值:

@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

以下是 Spring Framework 中可用的 bean 范围:

  • The singleton bean scope: As we saw in the previous example of bean configuration in XML-based, if scope is not defined in the configuration, then the Spring container considers scope as singleton. The Spring IoC container creates exactly only one single instance of the object, even if there are multiple references to a bean. Spring stores all singleton bean instances in a cache, and all subsequent requests of that named bean return the cached object. This is needed to understand that the Spring bean singleton scope is a little different from the typical singleton design pattern that we are using in Java. In Spring singleton, scope creates one object of that bean per one Spring container, meaning if there are multiple Spring containers in single JVM then multiple instances of that bean will be created.
  • The prototype bean scope: When scope is set to prototype, the Spring IoC container creates a new bean instance of object every time a bean is requested. Prototype-scoped beans are mostly used for stateful beans.
作为一项规则,对所有有状态 bean 使用 prototype scope,对无状态 bean 使用 singleton scope .
  • The request bean scope: The request bean scope is only available in a web-aware application context. The request scope creates a bean instance for each HTTP request. The bean is discarded as soon as the request processing is done.
  • The session bean scope: The session bean scope is only available in a web-aware application context. The session scope creates a bean instance for every HTTP session.
  • The application bean scope: The application bean scope is only available in a web-aware application context. The application scope creates a bean instance per web application.

Performance assessment with different configurations

在本节中,我们将了解不同类型的 bean 配置如何影响应用程序性能,并且我们将看到 bean 配置的最佳实践。

让我们看看 @ComponentScan 注释配置如何影响 Spring 应用程序的启动时间:

@ComponentScan (( {{ "org", "com" }} ))

按照前面的配置,Spring 会扫描 comorg 的所有包,因此会增加应用程序的启动时间。所以,我们应该只扫描那些有注释类的包,因为没有注释的类需要时间来扫描。我们应该只使用一个 @ComponentScan 并列出所有包,如下所示:

@ComponentScan(basePackages={"com.packt.springhighperformance.ch2.bankingapp.model","com.packt.springhighperformance.ch2.bankingapp.service"})

上述配置被认为是定义 @ComponentScan 注释的最佳实践。我们应该指定哪些包作为 basePackage 属性具有注释类。它将减少应用程序的启动时间。

Lazy loading versus preloading

延迟加载可确保在请求时即时加载 bean,而 预加载可确保在使用 bean 之前加载它们。 Spring IoC 容器默认使用预加载。因此,在开始时加载所有类,即使它们没有被使用也不是一个明智的决定,因为某些 Java 实例会非常消耗资源。我们应该根据应用要求使用所需的方法。

如果我们需要尽可能快地加载我们的应用程序,那就去延迟加载。如果我们需要我们的应用程序尽可能快地运行并更快地服务客户端请求,那么就去预加载。

Singleton versus prototype bean

在 Spring 中,默认情况下,所有定义的 bean 都是 singleton;但是,我们可以更改默认行为并使我们的 bean prototype。当 bean scope 设置为 prototype 时,Spring IoC 容器会在每次请求 bean 时创建一个对象的新 bean 实例。原型 bean 在创建过程中会影响性能,因此当 prototype bean 使用资源时,例如网络和数据库连接,应完全避免;或者,仔细设计动作。

Spring bean configuration best practices

在本节中,我们将看到一些在 Spring 中配置 bean 的最佳实践:

  • Use ID as bean identifiers:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
      
    <!-- Bean Configuration definition describe here -->
    <bean id="xxx" name="xxx" class=""/>
    
</beans>

在前面的示例中,我们使用 idname 识别 bean。我们应该使用 id 而不是 name 来选择 bean。通常,它既不会增加可读性,也不会提高任何性能,但这只是我们需要遵循的行业标准做法。

  • Prefer type over index for constructor argument matching. The constructor argument with index attribute is shown as follows:
<constructor-arg index="0" value="abc"/>
<constructor-arg index="1" value="100"/>
  • The constructor argument with the type attribute is shown as follows:
<constructor-arg type="java.lang.String" value="abc"/>
<constructor-arg type="int" value="100"/>

根据前面的示例,我们可以使用 indextype 作为构造函数参数。最好在构造函数参数中使用 type 属性而不是 index,因为它更具可读性且不易出错。但有时,当构造函数具有多个相同 type 的参数时,基于类型的参数可能会产生歧义问题。在这种情况下,我们需要使用 index 或基于名称的参数。

  • Use dependency check at the development phase: In bean definition, we should use the dependency-check attribute. It ensures that the container performs explicit dependency validation. It is useful when all or some of the properties of a bean must be set explicitly, or through auto wiring.
  • Do not specify version numbers in Spring schema references: in Spring configuration files, we specify the schema reference for different Spring modules. In schema references, we mention the XML namespace and its version number. Specifying the version number is not mandatory in the configuration file, so you can skip it. In fact, you should skip it all the time. Consider it as a best practice to follow. Spring automatically picks the highest version from the project dependencies (jars). A typical Spring configuration file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
      
    <!-- Bean Configuration definition describe here -->
    <bean class=""/>
    
</beans>

根据最佳实践,可以这样写:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
      
    <!-- Bean Configuration definition describe here -->
    <bean class=""/>
    
</beans>

给每个配置文件添加头注释;最好添加一个描述配置文件中定义的 bean 的配置文件头。 description标签的代码如下:

<beans>
<description>
This file defines customer service
related beans and it depends on
accountServices.xml, which provides
service bean templates...
</description>
...
</beans>

description 标签的优点是一些工具可能会从这个标签中获取描述以在其他地方帮助您。

DI pitfalls

众所周知,Spring 应用程序中存在三种 DI 模式:constructor-setter-和 field-based。每种类型都有不同的优点和缺点。只有基于字段的 DI 是不正确的方法,Spring 甚至不推荐。

以下是基于场的注入示例:

@Autowired
private ABean aBean;

根据 Spring bean 最佳实践,我们不应在 Spring 应用程序中使用基于字段的依赖项。主要原因是没有Spring上下文就无法测试。由于我们无法从外部提供依赖项,因此无法独立实例化对象。根据我的观点,这是基于现场的注入的唯一问题。

正如我们在前一节中所了解的,基于构造函数的依赖更适合强制字段,并且我们可以确保获得对象的不可变性质;然而,基于构造函数的依赖的主要缺点是它会在您的应用程序中创建循环依赖,并且根据 Spring 文档,通常建议不要依赖 bean 之间的循环依赖。所以,现在我们有这样的问题,为什么不依赖循环依赖?如果我们的应用程序中有循环依赖会发生什么? 那么,这些问题的答案是它可能会造成两个重要且不幸的是无声的陷阱。让我们讨论一下。

First pitfall

当您调用 ListableBeanFactory.getBeansOfType() 方法时,您无法确定将返回哪些 bean。我们看一下 DefaultListableBeanFactory.java类中getBeansOfType()方法的代码:

@Override
@SuppressWarnings("unchecked")
public <T> Map<String, T> getBeansOfType(@Nullable Class<T> type, boolean includeNonSingletons, boolean allowEagerInit)
      throws BeansException {
  
      ......
    
      if (exBeanName != null && isCurrentlyInCreation(exBeanName)) {
        if (this.logger.isDebugEnabled()) {
          this.logger.debug("Ignoring match to currently created bean 
          '" + 
          exBeanName + "': " +
          ex.getMessage());
        }
        onSuppressedException(ex);
        // Ignore: indicates a circular reference when auto wiring 
        constructors.
        // We want to find matches other than the currently created 
        bean itself.
        continue;
      }

      ......

}

在前面的代码中,您可以看到 getBeansOfType() 方法静默地跳过正在创建的 bean,并且只返回那些已经存在的。因此,当 bean 之间存在循环依赖时,不建议在容器启动期间使用 getBeansOfType() 方法。这是因为,根据前面的代码,如果您没有使用 DEBUGTRACE 日志级别,那么 Spring 跳过特定的日志中的信息将为零正在创建的bean。

让我们用下面的例子来看看前面的陷阱。根据下图,我们有三个 bean,AccountCustomerBank,以及它们之间的循环依赖关系:

读书笔记《hands-on-high-performance-with-spring-5》Spring最佳实践和Bean布线配置

根据上图,以下是 AccountCustomerBank 类:

@Component
public class Account {
  
  private static final Logger LOGGER = Logger.getLogger(Account.class);

  static {
    LOGGER.info("Account | Class loaded");
  }

  @Autowired
  public Account(ListableBeanFactory beanFactory) {
    LOGGER.info("Account | Constructor");
    LOGGER.info("Constructor (Customer?): {}" + 
    beanFactory.getBeansOfType(Customer.class).keySet());
    LOGGER.info("Constructor (Bank?): {}" + 
    beanFactory.getBeansOfType(Bank.class).keySet());
  }

}

@Component
public class Customer {
  
  private static final Logger LOGGER = Logger.getLogger(Customer.class);

  static {
    LOGGER.info("Customer | Class loaded");
  }

  @Autowired
  public Customer(ListableBeanFactory beanFactory) {
    LOGGER.info("Customer | Constructor");
    LOGGER.info("Account (Account?): {}" + 
    beanFactory.getBeansOfType(Account.class).keySet());
    LOGGER.info("Constructor (Bank?): {}" + 
    beanFactory.getBeansOfType(Bank.class).keySet());
  }

}

@Component
public class Bank {
  
  private static final Logger LOGGER = Logger.getLogger(Bank.class);

  static {
    LOGGER.info("Bank | Class loaded");
  }

  public Bank() {
    LOGGER.info("Bank | Constructor");
  }

}

以下是 Main 类:

public class MainApp {

  public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new 
    AnnotationConfigApplicationContext(AppConfig.class);
    Account account = context.getBean(Account.class);
    context.close();
  }
}

以下是我们可以展示 Spring 如何在内部加载 bean 和解析类的日志:

Account | Class loaded
Account | Constructor
Customer | Class loaded
Customer | Constructor
Account (Account?): {}[]
Bank | Class loaded
Bank | Constructor
Constructor (Bank?): {}[bank]
Constructor (Customer?): {}[customer]
Constructor (Bank?): {}[bank]

Spring Framework 首先加载 Account 并尝试实例化一个 bean;但是,当运行 getBeansOfType(Customer.class) 时,它会发现 Customer,因此继续加载和实例化那个。在 Customer 内部,我们可以立即发现问题:当 Customer 询问 beanFactory.getBeansOfType(Account.class) 时,它没有得到任何结果(< kbd>[])。 Spring 将默默地忽略 Account,因为它当前正在创建中。在这里可以看到 Bank 加载后,一切都如预期的那样。

现在我们可以理解了,当我们有循环依赖时,我们无法预测 getBeansOfType() 方法的输出。但是,我们可以通过正确使用 DI 来避免它。在循环依赖中,getBeansOfType() 会根据因素给出不同的结果,我们无法控制它。

Second pitfall (with AOP)

我们将在下一章详细学习 AOP。现在,我们没有详细介绍这个陷阱。我只是想让你明白,如果你在一个 bean 上有 Aspect,那么确保 bean 之间没有循环依赖;否则,Spring 将创建该 bean 的两个实例,一个没有 Aspect,另一个具有适当的方面,而不通知您。

Summary

在本章中,我们了解了 DI,它是 Spring Framework 的关键特性。 DI 帮助我们使我们的代码松散耦合和可测试。我们学习了各种 DI 模式,包括 constructor-setter 和 field-based。我们可以根据我们的要求在我们的应用程序中使用任何 DI 模式,因为每种类型都有自己的优点和缺点。

我们还学习了如何显式和隐式配置 DI。我们可以使用基于 XML 和基于 Java 的配置显式地注入依赖项。注解用于隐式注入依赖。 Spring 为我们提供了一种特殊类型的注解,称为stereotype annotation。 Spring 会自动注册带有原型注解的类。这使得该类可用于其他类中的 DI,这对于构建我们的应用程序至关重要。

在下一章中,我们将研究 Spring AOP 模块。 AOP 是一种强大的编程模型,可以帮助我们实现可重用的代码。