spring 发生事件后如何使用自定义参数启动@Bean 进行测试?

Posted

技术标签:

【中文标题】spring 发生事件后如何使用自定义参数启动@Bean 进行测试?【英文标题】:How to start a @Bean with custom parameters after an event had happened with spring for tests? 【发布时间】:2021-11-26 19:59:52 【问题描述】:

我正在为使用 R2dbc 的项目添加带有 TestContainers 框架的 RepositoryTests,我遇到了以下情况:

1 - 在主项目中,我在 application.yaml 文件中设置了 r2dbc url(带有端口和主机名),spring 数据管理一切,一切正常。

2 - 然而,在测试中,我使用的是 TestContainers 框架,更具体地说是 DockerComposeContainer,我用它来使用 docker-compose.test.yaml 文件和我需要的数据库创建一个模拟容器。

3 - 这个容器在旅途中创建了一个port 编号我在我的 docker-compose 文件中定义了一个端口号,但是DockerComposeContainer 将提供给我的端口号是随机的,并且每次我运行测试时都会发生变化,这使得在application-test.yaml 上拥有静态网址不再是一个选项。

所以我需要在运行时动态地创建这个bean R2dbcEntityTemplate,并且只有在DockerComposeContainer 会给我端口号之后。所以我的应用程序可以连接到正确的端口,一切都应该按预期工作。

我尝试创建这个类:

    package com.wayfair.samworkgroupsservice.adapter

    import io.r2dbc.mssql.MssqlConnectionConfiguration
    import io.r2dbc.mssql.MssqlConnectionFactory
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.beans.factory.config.ConstructorArgumentValues
    import org.springframework.beans.factory.support.BeanDefinitionRegistry
    import org.springframework.beans.factory.support.GenericBeanDefinition
    import org.springframework.context.ApplicationContext
    import org.springframework.context.annotation.Profile
    import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy
    import org.springframework.data.r2dbc.core.R2dbcEntityTemplate
    import org.springframework.data.r2dbc.dialect.SqlServerDialect
    import org.springframework.r2dbc.core.DatabaseClient
    import org.springframework.stereotype.Component
    
    @Component
    @Profile("test")
    class TemplateFactory(
        @Autowired val applicationContext: ApplicationContext
    ) 
        private val beanFactory = applicationContext.autowireCapableBeanFactory as BeanDefinitionRegistry
    
        fun registerTemplateBean(host: String, port: Int) 
            val beanDefinition = GenericBeanDefinition()
            beanDefinition.beanClass = R2dbcEntityTemplate::class.java
            val args = ConstructorArgumentValues()
            args.addIndexedArgumentValue(
                0,
                DatabaseClient.builder()
                    .connectionFactory(connectionFactory(host, port))
                    .bindMarkers(SqlServerDialect.INSTANCE.bindMarkersFactory)
                    .build()
            )
            args.addIndexedArgumentValue(1, DefaultReactiveDataAccessStrategy(SqlServerDialect.INSTANCE))
    
            beanDefinition.constructorArgumentValues = args
            beanFactory.registerBeanDefinition("R2dbcEntityTemplate", beanDefinition)
        
    
    //    fun entityTemplate(host: String = "localhost", port: Int = 1435) =
    //        R2dbcEntityTemplate(
    //            DatabaseClient.builder()
    //                .connectionFactory(connectionFactory(host, port))
    //                .bindMarkers(SqlServerDialect.INSTANCE.bindMarkersFactory)
    //                .build(),
    //            DefaultReactiveDataAccessStrategy(SqlServerDialect.INSTANCE)
    //        )
    
        private fun connectionFactory(host: String, port: Int) =
            MssqlConnectionFactory(
                MssqlConnectionConfiguration.builder()
                    .host(host)
                    .port(port)
                    .username("sa")
                    .password("Password123@#?")
                    .build()
            )
    

这就是我的数据库启动器的样子:

    package com.wayfair.samworkgroupsservice.adapter.note

    import com.wayfair.samworkgroupsservice.adapter.DBInitializerInterface
    import com.wayfair.samworkgroupsservice.adapter.TemplateFactory
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.data.r2dbc.core.R2dbcEntityTemplate
    import org.testcontainers.containers.DockerComposeContainer
    import org.testcontainers.containers.wait.strategy.Wait
    import org.testcontainers.junit.jupiter.Container
    import org.testcontainers.junit.jupiter.Testcontainers
    import java.io.File
    
    @Testcontainers
    class NoteTagDBInitializer : DBInitializerInterface 
        @Autowired
        override lateinit var client: R2dbcEntityTemplate
    
        @Autowired
        lateinit var factory: TemplateFactory
    
        override val sqlScripts = listOf(
            "db/note/schema.sql",
            "db/note/reset.sql",
            "db/note/data.sql"
        )
    
        init 
            factory.registerTemplateBean(
                cont.getServiceHost("test-db-local_1", 1433),
                cont.getServicePort("test-db-local_1", 1433)
            )
        
    
        companion object 
            @Container
            val cont: KDockerComposerContainer = KDockerComposerContainer("docker-compose.test.yml")
                .withExposedService(
                    "test-db-local_1", 1433,
                    Wait.forListeningPort()
                )
                .withLocalCompose(true)
                .also 
                    it.start()
                    val porttt = it.getServicePort("test-db-local_1", 1433)
                    print(porttt)
                
    
            class KDockerComposerContainer(yamlFile: String) :
                DockerComposeContainer<KDockerComposerContainer>(File(yamlFile))
        
    

尝试启动此模板工厂时没有收到任何有用的错误消息, 但老实说,我不知道我是否正在努力寻找正确的解决方案,有没有人知道如何解决这个问题,或者我在这里做错了什么?

所以总结一下生产应用程序很好,它从 application.yaml 文件上的 url 开始,就是这样,但对于测试,我需要一些动态的端口,每次都会改变。

提前谢谢你))

【问题讨论】:

【参考方案1】:

Spring 已经为您的问题提供了解决方案。 如果您使用的是最新的 Spring 版本 (>= 5.2.5),您应该使用 @DynamicPropertySource 以便使用容器数据库端口的动态值调整测试配置属性。阅读official spring documentation 了解更多详细信息和 kotlin 代码示例。

如果您使用旧的 Spring 版本,您需要的接口是ApplicationContextInitializer。请参阅此spring github issue 以获取一个小示例。

【讨论】:

以上是关于spring 发生事件后如何使用自定义参数启动@Bean 进行测试?的主要内容,如果未能解决你的问题,请参考以下文章

Spring中如何通过自定义用户事件管理日志文件切换?

Spring AOP 实现写事件日志功能

带有自定义 ReactiveAuthenticationManager 的 Spring 启动执行器审计事件

Spring云配置刷新后如何执行自定义逻辑?

java自定义事件,线程a如何每一秒钟触发一个事件,然后另一个线程b监听之,并作出反应?

自定义SpringBoot Starter 通过注解启动装配