如何构建Spring Boot Atomikos测试配置?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何构建Spring Boot Atomikos测试配置?相关的知识,希望对你有一定的参考价值。

如何创建适当的测试环境,以便能够在同一个应用程序中使用数据库层测试和REST端点测试?

我有一个带有两个数据源的Spring Boot应用程序。管理使用的Atomikos transactios。这个配置工作正常。

现在我需要创建测试。我构建了一个测试配置,每个测试工作正常,但是当我运行所有测试时它失败了。在我看来(参见堆栈跟踪)问题是Atomikos无法正常工作,如果很少有Atomikos bean实例化。

我尝试了两个解决方案来使Atomikos bean只实例化一次:

  1. 创建一个用于所有测试的测试配置(因为Spring缓存测试上下文)。但这不起作用。我认为这是因为@Controller中的Mock bean破解了重用Spring测试上下文的能力。我在测试中使用的持久性Mapper组件在一次测试中被模拟,同时在其他测试中使用实例。所以我看到每个测试类都在它自己的测试环境中运行。
  2. 在数据库@Configuration类中使用@Lazy注释。我认为这将确保bean只在第一次调用时才会被实例化,并将在进一步调用时重用。但这也行不通。

这里是我用来说明问题的示例项目链接。存储库包括mysql数据库转储:https://github.com/pavelmorozov/AtomikosConfig

在这篇文章中,我将只显示两个数据库模型,映射器和配置类中的一个,因为它们对于第二个数据库几乎相同。

java.lang.IllegalStateException: Failed to load ApplicationContext
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
    at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83)
    at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:189)
    at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:131)
    at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:230)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:228)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:287)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:289)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:247)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:539)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:761)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:461)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:207)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoController': Unsatisfied dependency expressed through field 'firstMapper'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstMapper' defined in file [/home/pm/Documents/workspace-sts-3.9.1.RELEASE/AtomikosConfig/target/classes/com/example/demo/persistence/mapper/first/FirstMapper.class]: Cannot resolve reference to bean 'firstSqlSessionFactory' while setting bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'firstSqlSessionFactory' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Unsatisfied dependency expressed through method 'firstSqlSessionFactory' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstDataSource' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Invocation of init method failed; nested exception is com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588)
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:120)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116)
    ... 25 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstMapper' defined in file [/home/pm/Documents/workspace-sts-3.9.1.RELEASE/AtomikosConfig/target/classes/com/example/demo/persistence/mapper/first/FirstMapper.class]: Cannot resolve reference to bean 'firstSqlSessionFactory' while setting bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'firstSqlSessionFactory' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Unsatisfied dependency expressed through method 'firstSqlSessionFactory' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstDataSource' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Invocation of init method failed; nested exception is com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:359)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:108)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1531)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1276)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:208)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1138)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:585)
    ... 43 more
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'firstSqlSessionFactory' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Unsatisfied dependency expressed through method 'firstSqlSessionFactory' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstDataSource' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Invocation of init method failed; nested exception is com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:467)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1173)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1067)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:513)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:351)
    ... 56 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstDataSource' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Invocation of init method failed; nested exception is com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:208)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1138)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
    ... 66 more
Caused by: com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at com.atomikos.jdbc.AtomikosSQLException.throwAtomikosSQLException(AtomikosSQLException.java:46)
    at com.atomikos.jdbc.AbstractDataSourceBean.init(AbstractDataSourceBean.java:306)
    at org.springframework.boot.jta.atomikos.AtomikosDataSourceBean.afterPropertiesSet(AtomikosDataSourceBean.java:49)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624)
    ... 77 more
Caused by: javax.naming.NamingException: Another resource already exists with name firstDataSource - pick a different name
    at com.atomikos.util.IntraVmObjectFactory.createReference(IntraVmObjectFactory.java:94)
    at com.atomikos.jdbc.AbstractDataSourceBean.getReference(AbstractDataSourceBean.java:388)
    at com.atomikos.jdbc.AbstractDataSourceBean.init(AbstractDataSourceBean.java:295)
    ... 80 more

.

@SpringBootApplication
@EnableAutoConfiguration(
        exclude = DataSourceAutoConfiguration.class, 
                DataSourceTransactionManagerAutoConfiguration.class, 
                MybatisAutoConfiguration.class)
public class AtomikosConfigApplication 

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

.

@Configuration
@Lazy
public class FirstDatabaseConfiguration 

    @Bean
    public MapperScannerConfigurer firstMapperScannerConfigurer() 
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setBasePackage("com.example.demo.persistence.mapper.first");
        configurer.setSqlSessionFactoryBeanName("firstSqlSessionFactory");
        return configurer;    
    

    /**
     * This bean uses Atomikos
     * to get transaction atomicity for 
     * few data sources (distributed transaction)
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.first")
    public DataSource firstDataSource() 
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setPoolSize(10);     
        atomikosDataSourceBean.setMaxLifetime(3600); 
        return atomikosDataSourceBean;
    

    @Bean
    public SqlSessionFactory firstSqlSessionFactory(
            @Qualifier("firstDataSource") DataSource dataSource) throws Exception 
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        return sessionFactory.getObject();
    


.

@Mapper
public interface FirstMapper 

    @Select("SELECT * from first WHERE id = #id")
    @Results(value = 
            @Result(property = "id", column = "id"),
            @Result(property = "name", column = "name")
    )
    FirstModel selectById(@Param("id") long id);


.

public class FirstModel 
    private long id;
    private String name;
    public long getId() 
        return id;
    
    public void setId(long id) 
        this.id = id;
    
    public String getName() 
        return name;
    
    public void setName(String name) 
        this.name = name;
    

.

@RestController
public class DemoController 

    @Autowired
    FirstMapper firstMapper;

    @Autowired
    SecondMapper secondMapper;

    @GetMapping("/demo-controller")
    @Transactional
    public  String getDemoData() 

        String firstName = firstMapper.selectById(1).getName();
        String secondName = secondMapper.selectById(1).getName();

        String response = "\"first\":"+firstName+", \"secondName\":"+secondName+"";

        return response; 
    


.

@RunWith(SpringRunner.class)
@SpringBootTest
@EnableAutoConfiguration(exclude =  DataSourceAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class, MybatisAutoConfiguration.class )
@AutoConfigureMockMvc

public class DemoControllerTest 

    @MockBean
    FirstMapper firstMapper;

    @MockBean
    SecondMapper secondMapper;

    @Autowired
    MockMvc mvc;

    @Test
    public void getDemoDataTest() throws Exception 

        FirstModel firstModel = new FirstModel();
        firstModel.setId(1);
        firstModel.setName("first name");
        given(firstMapper.selectById(1l)).willReturn(firstModel);

        SecondModel secondModel = new SecondModel();
        secondModel.setId(1);
        secondModel.setName("second name");
        given(secondMapper.selectById(1l)).willReturn(secondModel);

        mvc.perform(get("/demo-controller").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
                .andExpect(jsonPath("$.first", is("first name")));

    


.

@RunWith(SpringRunner.class)
@SpringBootTest
@EnableAutoConfiguration(
        exclude = DataSourceAutoConfiguration.class, 
                DataSourceTransactionManagerAutoConfiguration.class, 
                MybatisAutoConfiguration.class
    )

public class FirstMapperTest 
    @Autowired
    FirstMapper firstMapper;

    @Test
    public void selectByIdTest() 
        FirstModel first = firstMapper.selectById(1);
        assertEquals("first name", first.getName());
    

答案

所以我碰到了这个,也无法在网上找到解决方案。这是我最终的工作:

我从spring-boot-autoconfigure中复制了很多AtomikosJtaConfiguration,因为自动配置的东西没有进入我的测试。我发现Spring上下文在运行我的集成测试时被刷新了很多次,并且在一些(但不是全部)刷新时调用了@Bean方法来重新创建bean。数据源方法失败,因为之前的数据源由于某种原因没有被清理,所以我决定使用静态变量来保存数据源并且每次都返回相同的数据源。剩下的就行了。您应该做的另一件事是将Atomikos设置为不在测试期间编写事务日志,这样测试可以并行运行而不会相互踩踏。

 @Bean
 public DataSource dataSource() throws Exception 
   if (ATOMIKOS_DATA_SOURCE_BEAN == null) 
     if (xaDataSource == null) 
       xaDataSource = createXaDataSource();
     
     ATOMIKOS_DATA_SOURCE_BEAN = new AtomikosXADataSourceWrapper().wrapDataSource(xaDataSource);
   
   return ATOMIKOS_DATA_SOURCE_BEAN;
 

以上是关于如何构建Spring Boot Atomikos测试配置?的主要内容,如果未能解决你的问题,请参考以下文章

spring-boot-starter-jta-atomikos 和 spring-boot-starter-batch

spring3.0+Atomikos 构建jta的分布式事务

spring3.0+Atomikos 构建jta的分布式事务

(附源码gitHub下载地址)spring boot -jta-atomikos分布式事务

使用springboot+jta+atomikos 分布式事物管理

如何删除/隐藏 Atomikos 启动错误消息?