1. 概述

在这个简短的教程中,我们将重点讨论Spring为调整JPA仓库实例化方式提供的不同BootstrapMode类型。

在启动时,Spring Data会扫描仓库并注册它们的单例bean定义。初始化期间,仓库会立即获取一个EntityManager。具体来说,它们会获取JPA元模型并验证声明的查询。

默认情况下,JPA是同步启动的,因此在启动过程中,仓库的实例化会被阻塞,直到启动过程完成。随着仓库数量的增长,应用程序可能需要很长时间才能开始接受请求。

2. 仓库的不同启动选项

首先,我们添加spring-data-jpa依赖。由于使用了Spring Boot,我们将使用相应的spring-boot-starter-data-jpa依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

我们可以使用配置属性告诉Spring使用默认的仓库启动行为:

spring.data.jpa.repositories.bootstrap-mode=default

我们也可以通过注解式配置实现相同的目标:

@SpringBootApplication
@EnableJpaRepositories(bootstrapMode = BootstrapMode.DEFAULT)
public class Application {
    // ...
}

另一种方法,仅限于单个测试类,是使用@DataJpaTest注解:

@DataJpaTest(bootstrapMode = BootstrapMode.LAZY)
class BootstrapmodeLazyIntegrationTest {
    // ...
}

对于接下来的例子,我们将使用@DataJpaTest注解,并探索不同的仓库启动选项。

2.1. 默认模式

BootstrapMode的默认值会即时创建仓库。因此,就像其他Spring Bean一样,它们的初始化会在注入时发生。

让我们创建一个Todo实体:

@Entity
public class Todo {
    @Id
    private Long id;
    private String label;

    // standard setters and getters
}

接下来,我们需要其关联的仓库。让我们创建一个继承自CrudRepository的仓库:

public interface TodoRepository extends CrudRepository<Todo, Long> {
}

最后,让我们添加一个使用我们仓库的测试:

@DataJpaTest
class BootstrapmodeDefaultIntegrationTest {

    @Autowired
    private TodoRepository todoRepository;

    @Test
    void givenBootstrapmodeValueIsDefault_whenCreatingTodo_shouldSuccess() {
        Todo todo = new Todo("Something to be done");

        assertThat(todoRepository.save(todo)).hasNoNullFieldsOrProperties();
    }
}

启动测试后,我们在日志中检查Spring如何初始化我们的TodoRepository

[2022-03-22 14:46:47,597]-[main] INFO  org.springframework.data.repository.config.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
[2022-03-22 14:46:47,737]-[main] TRACE org.springframework.data.repository.config.RepositoryConfigurationDelegate - Spring Data JPA - Registering repository: todoRepository - Interface: com.baeldung.boot.bootstrapmode.repository.TodoRepository - Factory: org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean
[2022-03-22 14:46:49,718]-[main] DEBUG org.springframework.data.repository.core.support.RepositoryFactorySupport - Initializing repository instance for com.baeldung.boot.bootstrapmode.repository.TodoRepository…
[2022-03-22 14:46:49,792]-[main] DEBUG org.springframework.data.repository.core.support.RepositoryFactorySupport - Finished creation of repository instance for com.baeldung.boot.bootstrapmode.repository.TodoRepository.
[2022-03-22 14:46:49,858]-[main] INFO  com.baeldung.boot.bootstrapmode.BootstrapmodeDefaultIntegrationTest - Started BootstrapmodeDefaultIntegrationTest in 3.547 seconds (JVM running for 4.877)

在示例中,我们提前初始化仓库,并在应用启动后使其可用。

2.2. 懒惰模式

通过将JPA仓库的BootstrapMode设置为懒惰,Spring将注册仓库的bean定义,但不会立即实例化它。因此,使用懒惰选项,第一次使用会触发其初始化。

修改我们的测试并为bootstrapMode应用懒惰选项:

@DataJpaTest(bootstrapMode = BootstrapMode.LAZY)

然后,使用新的配置启动测试,并查看相应的日志:

[2022-03-22 15:09:01,360]-[main] INFO  org.springframework.data.repository.config.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in LAZY mode.
[2022-03-22 15:09:01,398]-[main] TRACE org.springframework.data.repository.config.RepositoryConfigurationDelegate - Spring Data JPA - Registering repository: todoRepository - Interface: com.baeldung.boot.bootstrapmode.repository.TodoRepository - Factory: org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean
[2022-03-22 15:09:01,971]-[main] INFO  com.baeldung.boot.bootstrapmode.BootstrapmodeLazyIntegrationTest - Started BootstrapmodeLazyIntegrationTest in 1.299 seconds (JVM running for 2.148)
[2022-03-22 15:09:01,976]-[main] DEBUG org.springframework.data.repository.config.RepositoryConfigurationDelegate$LazyRepositoryInjectionPointResolver - Creating lazy injection proxy for com.baeldung.boot.bootstrapmode.repository.TodoRepository…
[2022-03-22 15:09:02,588]-[main] DEBUG org.springframework.data.repository.core.support.RepositoryFactorySupport - Initializing repository instance for com.baeldung.boot.bootstrapmode.repository.TodoRepository…

这里需要注意一些潜在问题:

  • Spring可能会在未初始化仓库的情况下开始接收请求,这可能导致处理第一个请求时增加延迟。
  • 全局设置BootstrapMode为懒惰容易出错。Spring将不验证未包含在测试中的仓库中的查询和元数据。

我们应该只在开发阶段使用懒加载,以避免在生产环境中部署可能带有初始化错误的应用程序。我们可以优雅地使用Spring Profile来达到这个目的。

2.3. 延迟模式

当异步启动JPA时,延迟模式是正确的选择。因此,仓库不需要等待EntityManagerFactory的初始化。

在配置类中,我们声明一个AsyncTaskExecutor,使用Spring实现之一的ThreadPoolTaskExecutor,并重写submit方法,返回一个Future

@Bean
AsyncTaskExecutor delayedTaskExecutor() {
    return new ThreadPoolTaskExecutor() {
        @Override
        public <T> Future<T> submit(Callable<T> task) {
            return super.submit(() -> {
                Thread.sleep(5000);
                return task.call();
            });
        }
    };
}

接下来,按照我们的Spring和JPA指南,向我们的配置添加一个EntityManagerFactory bean,并指示我们希望使用异步执行器进行后台启动:

@Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, AsyncTaskExecutor delayedTaskExecutor) {
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();

    factory.setPackagesToScan("com.baeldung.boot.bootstrapmode");
    factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    factory.setDataSource(dataSource);
    factory.setBootstrapExecutor(delayedTaskExecutor);
    Map<String, Object> properties = new HashMap<>();
    properties.put("hibernate.hbm2ddl.auto", "create-drop");
    factory.setJpaPropertyMap(properties);
    return factory;
}

最后,修改我们的测试以启用延迟启动模式:

@DataJpaTest(bootstrapMode = BootstrapMode.DEFERRED)

再次启动测试并查看日志:

[2022-03-23 10:31:16,513]-[main] INFO  org.springframework.data.repository.config.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFERRED mode.
[2022-03-23 10:31:16,543]-[main] TRACE org.springframework.data.repository.config.RepositoryConfigurationDelegate - Spring Data JPA - Registering repository: todoRepository - Interface: com.baeldung.boot.bootstrapmode.repository.TodoRepository - Factory: org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean
[2022-03-23 10:31:16,545]-[main] DEBUG org.springframework.data.repository.config.RepositoryConfigurationDelegate - Registering deferred repository initialization listener.
[2022-03-23 10:31:17,108]-[main] INFO  org.springframework.data.repository.config.DeferredRepositoryInitializationListener - Triggering deferred initialization of Spring Data repositories…
[2022-03-23 10:31:22,538]-[main] DEBUG org.springframework.data.repository.core.support.RepositoryFactorySupport - Initializing repository instance for com.baeldung.boot.bootstrapmode.repository.TodoRepository…
[2022-03-23 10:31:22,572]-[main] INFO  com.baeldung.boot.bootstrapmode.BootstrapmodeDeferredIntegrationTest - Started BootstrapmodeDeferredIntegrationTest in 6.769 seconds (JVM running for 7.519)

在这个例子中,应用上下文的启动完成触发仓库的初始化。简而言之,Spring将仓库标记为懒惰,并注册一个DeferredRepositoryInitializationListener bean。当ApplicationContext发出一个ContextRefreshedEvent时,它会初始化所有仓库。

因此,Spring Data在应用启动前初始化仓库并验证其中包含的查询和元数据

3. 总结

在这篇文章中,我们探讨了初始化JPA仓库的不同方式以及在哪些情况下使用它们。
如往常一样,本文中使用的所有代码示例都可以在GitHub上找到。