JPA Repository的引导模式

2023/05/11

1. 概述

在这个简短的教程中,我们将重点介绍Spring为JPA Repository提供的不同类型的BootstrapMode,用于更改其实例化的编排

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

默认情况下,JPA是同步启动的。因此,在引导过程完成之前,Repository的实例化将被阻塞。随着Repository数量的增加,应用程序可能需要很长时间才能开始接受请求。

2. 引导Repository的不同选项

让我们从添加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使用默认的Repository启动行为:

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

我们可以通过使用基于注解的配置来实现相同的目的:

@SpringBootApplication
@EnableJpaRepositories(bootstrapMode = BootstrapMode.DEFAULT)
public class BootstrapModeApplication {

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

第三种方法(仅限于单个测试类)是使用@DataJpaTest注解:

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

对于以下示例,我们将使用@DataJpaTest注解并说明不同的Repository启动选项

2.1 Default

启动模式的默认值将急切地实例化Repository。因此,与任何其他Spring bean一样,它们的初始化将在注入时发生

让我们创建一个Todo实体:

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

    // standard setters and getters
}

接下来,我们需要创建其关联的Repository:

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

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

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

20:59:54.705 [main] INFO  [o.s.d.r.c.RepositoryConfigurationDelegate] >>> Bootstrapping Spring Data JPA repositories in DEFAULT mode. 
20:59:54.739 [main] INFO  [o.s.d.r.c.RepositoryConfigurationDelegate] >>> Finished Spring Data repository scanning in 27 ms. Found 1 JPA repository interfaces. 
20:59:54.965 [main] INFO  [o.s.j.d.e.EmbeddedDatabaseFactory] >>> Starting embedded database: url='jdbc:h2:mem:e9b57c1c-d852-428d-b6ca-ba86bbc7517e;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa' 
21:00:00.930 [delayedTaskExecutor-1] INFO  [o.h.e.t.j.p.i.JtaPlatformInitiator] >>> HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] 
21:00:00.936 [delayedTaskExecutor-1] INFO  [o.s.o.j.LocalContainerEntityManagerFactoryBean] >>> Initialized JPA EntityManagerFactory for persistence unit 'default' 
21:00:01.130 [main] INFO  [c.t.t.b.b.BootstrapModeDefaultIntegrationTest] >>> Started BootstrapModeDefaultIntegrationTest in 7.167 seconds (JVM running for 7.951) 

在我们的示例中,我们尽早地初始化Repository并在应用程序启动后使其可用

2.2 Lazy

通过对JPA Repository使用惰性BootstrapMode,Spring会注册Repository的bean定义,但不会立即实例化它。因此,使用LAZY选项,只会在第一次使用时触发其初始化

让我们修改我们的测试并将LAZY选项应用于bootstrapMode:

@DataJpaTest(bootstrapMode = BootstrapMode.LAZY)

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

21:04:01.892 [main] INFO  [o.s.d.r.c.RepositoryConfigurationDelegate] >>> Bootstrapping Spring Data JPA repositories in LAZY mode. 
21:04:01.928 [main] INFO  [o.s.d.r.c.RepositoryConfigurationDelegate] >>> Finished Spring Data repository scanning in 29 ms. Found 1 JPA repository interfaces. 
21:04:01.972 [main] INFO  [o.s.b.t.a.j.TestDatabaseAutoConfiguration$EmbeddedDataSourceBeanFactoryPostProcessor] >>> Replacing 'dataSource' DataSource bean with embedded version 
21:04:02.158 [main] INFO  [o.s.j.d.e.EmbeddedDatabaseFactory] >>> Starting embedded database: url='jdbc:h2:mem:8e4088ba-71de-49fc-a7ad-238f07485be9;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa' 
21:04:02.504 [main] INFO  [c.t.t.b.b.BootstrapModeLazyIntegrationTest] >>> Started BootstrapModeLazyIntegrationTest in 1.332 seconds (JVM running for 2.119) 

我们应该注意这里的几个缺点:

  • Spring可能会在没有初始化Repository的情况下开始接收请求,从而在处理第一个Repository时增加延迟。
  • 将BootstrapMode全局设置为LAZY很容易出错,Spring不会验证我们测试中未包含的Repository中包含的查询和元数据。

我们应该只在开发期间使用LAZY模式,以避免在生产中部署应用程序时出现潜在的初始化错误。为此,我们可以优雅地使用Spring Profile

2.3 Deferred

Deferred是异步启动JPA时使用的正确方式。因此,Repository不会等待EntityManagerFactory的初始化

让我们使用ThreadPoolTaskExecutor(它的Spring实现之一)在配置类中声明一个AsyncTaskExecutor,并覆盖返回Future的submit方法:

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

接下来,让我们将EntityManagerFactory bean添加到我们的配置中,并指示我们想要使用异步执行器进行后台启动:

@Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, AsyncTaskExecutor delayedTaskExecutor) {
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setPackagesToScan("cn.tuyucheng.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;
}

最后,让我们修改测试以使用DEFERRED启动模式:

@DataJpaTest(bootstrapMode = BootstrapMode.DEFERRED)

再次运行测试并观察日志:

21:10:46.442 [main] INFO  [o.s.d.r.c.RepositoryConfigurationDelegate] >>> Bootstrapping Spring Data JPA repositories in DEFERRED mode. 
21:10:46.475 [main] INFO  [o.s.d.r.c.RepositoryConfigurationDelegate] >>> Finished Spring Data repository scanning in 28 ms. Found 1 JPA repository interfaces. 
21:10:46.519 [main] INFO  [o.s.b.t.a.j.TestDatabaseAutoConfiguration$EmbeddedDataSourceBeanFactoryPostProcessor] >>> Replacing 'dataSource' DataSource bean with embedded version 
21:10:46.709 [main] INFO  [o.s.j.d.e.EmbeddedDatabaseFactory] >>> Starting embedded database: url='jdbc:h2:mem:8156978a-0778-4c1f-b617-957358609b82;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa' 
21:10:47.032 [main] INFO  [o.s.d.r.c.DeferredRepositoryInitializationListener] >>> Triggering deferred initialization of Spring Data repositories… 

在此示例中,应用程序上下文启动完成会触发Repository的初始化。简而言之,Spring将Repository标记为惰性,并注册一个DeferredRepositoryInitializationListener bean。当ApplicationContext触发ContextRefreshedEvent事件时,它会初始化所有Repository。

因此,Spring Data在应用程序启动之前初始化Repository并验证其包含的查询和元数据

3. 总结

在本文中,我们研究了初始化JPA Repository的各种方法以及在哪些情况下使用它们。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章