Spring从零开始学使用系列(三)--依赖注入(DI)

目录

1.DI的核心概念

1.1优势

2. Spring中的DI实现

2.1 构造器注入

2.1.2 优势和缺点

2.2 设置器注入

 2.2.1 如何使用设置器注入

2.2.2 示例代码

2.2.3优势和使用场景

 2.3 字段注入

2.4 方法注入

2.4.1 方法注入的概念

2.4.2 找方法注入

2.4.3 @Lookup 注解的作用

2.4.4 结论和最佳实践

2.5 总结

3.Spring事务管理概述

3.1 声明式事务管理

3.1.1 使用@Transactional注解

3.1.2 @Transactional注解的参数解释

3.1.3 Spring Data JPA 和事务

3.2 事务传播行为

3.3事务失效的场景

4.总结


1.DI的核心概念

        依赖注入(DI)是一种设计模式,用于实现对象间的依赖关系的自动满足。在传统的编程模式中,每个对象负责管理其依赖项的创建和绑定,而在使用DI的模式中,这些依赖项是由外部系统(在Spring中是IoC容器)在创建对象时自动注入的。

1.1优势
  • 降低耦合度:对象不需要知道如何创建它们所需的依赖对象。
  • 增强模块化:便于管理大型应用中的依赖关系。
  • 提高可测试性:依赖可以在测试中被替换或模拟。
2. Spring中的DI实现

        Spring提供了多种方式来实现DI,包括构造器注入、设置器注入和字段注入。

2.1 构造器注入

        通过构造器注入,依赖项在对象创建时通过构造函数传入。这是推荐的注入方式,因为它可以保证所需的依赖项在使用前就被正确提供。

@Component
public class ProductService {
    private final InventoryService inventoryService;

    public ProductService(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }
}

        在Spring 4.3之后,如果目标组件只定义了一个构造器,Spring会自动将其作为用于依赖注入的构造器,无需显式使用@Autowired注解。然而,在更早的版本中或者当存在多个构造器时,@Autowired是必须的,以标识哪个构造器应该被用来自动注入。

2.1.2 优势和缺点

优点

  • 依赖不可变,确保所需的依赖在使用前已经提供。
  • 便于发现缺失的依赖,因为在对象创建时就会出错。
  • 最适合用于必需依赖。

缺点

  • 如果依赖项很多,构造器可能会显得笨重。
  • 对于可选依赖不是很灵活。
2.2 设置器注入

        设置器注入,顾名思义,是通过类的设置方法(setter方法)来注入依赖。这种方式的主要特点是它不强制在构造对象时立即提供依赖,而是允许在对象构造后、使用前的任何时间点注入依赖。这提供了更大的灵活性,尤其是在需要重新配置或更换依赖的场景中。

 2.2.1 如何使用设置器注入

        在Spring中使用设置器注入非常简单。首先,你需要在你的类中提供一个公开的设置方法,然后使用@Autowired注解标注这个方法。Spring容器在创建这个类的Bean时,会自动调用这个设置方法,将相应的依赖注入进去。

2.2.2 示例代码

        假设你有一个ProductService类,它依赖于一个名为InventoryService的服务。下面是如何通过设置器注入来注入这个依赖:

        

@Component
public class ProductService {
    private InventoryService inventoryService;

    // 使用@Autowired注解标注设置方法
    @Autowired
    public void setInventoryService(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }

    public void processProduct() {
        // 使用inventoryService进行一些操作
    }
}
2.2.3优势和使用场景

优势:

  • 灵活性:可以在对象的生命周期中的任何时点注入依赖,甚至可以重新注入不同的依赖。
  • 适合可选依赖:如果某个依赖是可选的,使用设置器注入可以避免在构造函数中处理复杂的逻辑。
  • 易于重新配置:适用于那些可能需要重新配置的组件。

使用场景:

  • 当依赖项可能在运行时改变时,例如基于某些业务规则更换策略对象。
  • 在需要通过配置来调整依赖关系的企业应用中,设置器注入提供了一种简单的方式来调整这些依赖。
  • 当某些依赖项是可选的,不是必需的,这时使用设置器注入可以避免在对象创建时就必须提供这些依赖。

潜在的缺点

  • 可能的不完整状态:如果在依赖注入之前就使用了对象,那么可能会导致运行时错误。
  • 过度使用可能导致代码混乱:如果一个类有很多通过设置器注入的依赖,这可能会使类变得难以管理和理解。
 2.3 字段注入

        定义:字段注入是最简单的注入方式,直接在需要的字段上标注@Autowired。这个也是我们最常见的使用,不过现在如果使用的是新的版本的话,idea更支持的是构造器注入了

代码示例

@Component
public class ProductService {
    @Autowired
    private InventoryService inventoryService;
}

优点

  • 写法简单,减少了样板代码。

缺点

  • 测试困难,因为没有公开的方法可以用来替换依赖。
  • 可能导致代码难以追踪,特别是在存在多个依赖的情况下。
2.4 方法注入
2.4.1 方法注入的概念

        方法注入主要用于当一个单例Bean需要引用一个原型Bean的实例时。由于单例Bean在Spring容器的生命周期内只创建一次,其依赖的原型Bean也只会注入一次。这就导致了单例Bean不能再次获取原型Bean的新实例,除非使用方法注入。

        Spring提供了一种方法注入的机制来解决这个问题。它主要通过两种方式实现:

  1. 查找方法注入:使用@Lookup注解来标注一个方法,这告诉Spring每次调用这个方法时都要在容器中查找并返回新的Bean实例。
  2. 方法替换:使用replaced-method元素来动态替换方法的实现。
2.4.2 找方法注入
@Component
public abstract class ProductService {

    public void processProduct() {
        PricingService pricingService = createPricingService();
        // 对pricingService执行一些操作
    }

    @Lookup
    protected abstract PricingService createPricingService();
}

        这里的 @Lookup 注解标注在 createPricingService() 方法上。Spring在运行时将动态生成 ProductService 的一个子类,并覆盖这个方法以从Spring容器中返回PricingService的新实例。

2.4.3 @Lookup 注解的作用

   @Lookup注解告诉Spring容器在每次调用该方法时,都应从Spring IoC容器中获取指定Bean的新实例。这对于处理原型依赖于单例Bean的情况非常有用。

2.4.4 结论和最佳实践

        方法注入特别适合于需要将设计模式如工厂方法模式整合到Spring管理的Bean中的场景。虽然方法注入在某些特定场景下非常有用,但应当谨慎使用,因为它增加了配置的复杂性并可能对性能产生影响。

2.5 总结

       尽管字段注入在某些情况下看似方便,但从长远来看,它可能会引入多种问题,特别是在大型项目中。  构造器注入是推荐的方式,特别是对于必需依赖,因为它能够确保依赖在使用前被正确设置。这也是最符合依赖注入原理的方法,因为它支持不可变性并且确保了对象始终处于有效状态。

3.Spring事务管理概述

         Spring的事务管理支持提供了一致的编程模型,既适用于声明式事务管理也适用于编程式事务管理。它与底层的事务基础设施进行了良好的抽象,使得开发者可以不用关心具体的事务管理API,就能实现跨不同事务管理API的事务操作。

3.1 声明式事务管理

        声明式事务管理是Spring推荐的事务管理方式。它将事务管理代码从业务代码中分离出来,通过配置方式来管理事务。这种方式使用@Transactional注解,非常简单而且高效。

3.1.1 使用@Transactional注解

        在Spring中,你可以通过在类或方法上添加@Transactional注解来声明一个事务。这个注解可以告诉Spring哪些方法是事务性的,以及这些事务应该如何被管理。

@Service
public class ProductService {
    @Transactional
    public void updateProductStock(Long productId, int newStock) {
        // 业务逻辑代码,例如更新库存
        // 这个方法中的所有数据库操作都会在同一个事务中执行
    }
}
3.1.2 @Transactional注解的参数解释
  1. propagation:定义了事务的传播行为。这决定了事务方法是如何相互作用的。例如,Propagation.REQUIRED表示当前方法必须在一个具有事务的上下文中执行,如果当前没有事务上下文,就会启动一个新的事务。

  2. isolation:定义了事务的隔离级别,这是一个关键设置,因为它防止了多个事务同时执行时可能产生的问题,如脏读、不可重复读和幻读。常用的隔离级别包括 READ_COMMITTEDREPEATABLE_READ 等。

  3. timeout:定义了事务在被回滚前可以运行的时间(秒)。如果事务超过这个时间还没有完成,就会自动回滚。这有助于防止长时间运行的事务占用资源。

  4. readOnly:标记事务是否为只读事务。设置为true可以帮助数据库优化事务。对于只查询数据的事务,设置为只读可以提高性能。

  5. rollbackFor:定义了哪些异常类会触发事务回滚。默认情况下,事务只对RuntimeExceptionError进行回滚。如果业务逻辑中检测到其他异常也需要回滚事务,需要显式设置。

  6. noRollbackFor:定义了哪些异常类不会触发事务回滚。这在特定情况下非常有用,比如当某些异常不应该导致事务失败时。

3.1.3 Spring Data JPA 和事务

        `Spring Data JPA简化了基于JPA的数据访问层的开发。事务管理是这一模块的核心组成部分,因为任何JPA操作几乎都需要在事务的上下文中执行。

事务的关键应用:

  • Repository层事务Spring Data JPA的Repository接口默认就支持声明式事务。通常不需要手动配置,但可以通过@Transactional注解自定义事务行为。这里很需要注意
  • 复杂查询:对于执行复杂的数据操作或查询,合适的事务管理确保了在执行过程中数据的一致性和完整性。
3.2 事务传播行为

        事务传播行为定义了一个事务性方法如何相对于调用它的方法事务进行交互。Spring定义了几种传播行为,以下是最常用的几种:

  1. REQUIRED(默认): 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. REQUIRES_NEW: 创建一个新事务,如果当前存在事务,则暂停当前事务。
  3. SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
  4. NOT_SUPPORTED: 以非事务方式执行操作,如果当前存在事务,则暂停当前事务。
  5. MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  6. NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。
  7. NESTED: 如果当前存在事务,则执行一个嵌套的子事务;如果当前没有事务,则表现如REQUIRED
3.3事务失效的场景

        Spring事务管理虽然强大,但在某些情况下可能会失效,导致事务不生效。以下是一些常见的场景:

  1. 私有方法调用:在同一个类中,一个非事务性的公共方法内部调用了一个标注为@Transactional的私有方法,事务是不会生效的,因为Spring事务是通过代理实现的,只能应用于公共接口方法。

  2. 自调用问题:一个类中的一个方法调用另一个标注为@Transactional的方法,如果是通过this方法调用的,事务是不会启动的,因为事务的开启需要通过代理对象来实现。

  3. 异常类型不正确@Transactional注解中可以指定一个异常类型,事务只有在抛出指定异常时才会回滚。如果抛出的异常类型不是事务注解中配置的回滚异常,事务也不会回滚。

  4. 数据库支持不足:如果数据库或数据库驱动不支持事务或当前隔离级别,那么标注了@Transactional的方法也无法正确管理事务。

  5. 方法是非公开的:如前所述,由于Spring的事务管理是通过AOP代理实现的,只有目标方法是公开的接口方法时,代理才能管理其事务。

4.总结

        依赖注入是Spring框架中一个核心的功能,它不仅提高了代码的可维护性和灵活性,还极大地简化了企业级应用的开发。理解和正确使用DI可以帮助开发者构建出更加健壮、可测试且易于维护的Java应用程序。

上一篇:混合现实(MR)技术的应用场景


下一篇:Docker资源管理-数据管理