单元测试和基于注释的配置中的Spring事务

时间:2016-01-15 23:14:33

标签: java database spring spring-transactions jooq

我无法在此单元测试中处理事务。 TransactionTest类包含所有必需的Spring配置。它启动,初始化数据库并并行执行两个Runnable(插入器和选择器)。从记录的输出中可以看出,测试执行,记录以正确的顺序从数据库中插入和选择,但没有事务隔离。

我希望在日志中看到的内容如下:

2016-01-16 00:29:32,447 [main] DEBUG  TransactionTest - Starting test
2016-01-16 00:29:32,619 [pool-2-thread-2] DEBUG  Selector - Select 1 returned: 0
2016-01-16 00:29:33,121 [pool-2-thread-1] DEBUG  Inserter - inserting record: 1
2016-01-16 00:29:33,621 [pool-2-thread-2] DEBUG  Selector - Select 2 returned: 0
2016-01-16 00:29:34,151 [pool-2-thread-1] DEBUG  Inserter - inserting record: 2
2016-01-16 00:29:34,624 [pool-2-thread-2] DEBUG  Selector - Select 3 returned: 2
2016-01-16 00:29:34,624 [main] DEBUG  TransactionTest - Terminated

但是,我看到的是:

2016-01-16 00:29:32,447 [main] DEBUG  TransactionTest - Starting test
2016-01-16 00:29:32,619 [pool-2-thread-2] DEBUG  Selector - Select 1 returned: 0
2016-01-16 00:29:33,121 [pool-2-thread-1] DEBUG  Inserter - inserting record: 1
2016-01-16 00:29:33,621 [pool-2-thread-2] DEBUG  Selector - Select 2 returned: 1
2016-01-16 00:29:34,151 [pool-2-thread-1] DEBUG  Inserter - inserting record: 2
2016-01-16 00:29:34,624 [pool-2-thread-2] DEBUG  Selector - Select 3 returned: 2
2016-01-16 00:29:34,624 [main] DEBUG  TransactionTest - Terminated

请考虑以下测试代码。在TransactionTest.java中,有一些注释在类主体本身之前被注释掉。当我包含这些注释时,我可以从日志中看到Spring在一个单独的事务中执行整个测试。但是我的目标是让它在单独的事务中执行Inserter.insertSeveralRecords()方法。遗憾的是,日志中没有迹象表明Spring甚至在那里看到@Transactional注释。

我试图将@EnableTransactionManagement注释也添加到TransactionTest类本身,而不是配置部分,但它没有区别。

TransactionTest.java

package program.test.db.transaction;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.flywaydb.core.Flyway;
import org.flywaydb.test.annotation.FlywayTest;
import org.flywaydb.test.junit.FlywayTestExecutionListener;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultConfiguration;
import org.jooq.impl.DefaultDSLContext;
import org.jooq.impl.DefaultExecuteListenerProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;

import program.db.JooqExceptionTranslator;
import static org.junit.Assert.assertTrue;
import static program.db.Tables.SYSTEM_LOG;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, FlywayTestExecutionListener.class})//, TransactionalTestExecutionListener.class})
//@Transactional
//@TransactionConfiguration(transactionManager="transactionManager", defaultRollback=false)
public class TransactionTest {

    private static Logger log = LogManager.getLogger(TransactionTest.class);

    @Configuration
    @PropertySource("classpath:program.properties")
    @EnableTransactionManagement
    static class ContextConfiguration {
        @Autowired
        private Environment env;
        @Bean
        public Flyway flyway(){
            Flyway flyway = new Flyway();
            flyway.setDataSource(dataSource());
            flyway.setSchemas("program_x");
            flyway.setLocations("db/migration");
            return flyway;
        }
        @Bean
        public BasicDataSource dataSource() {
            BasicDataSource result = new BasicDataSource();
            result.setDriverClassName(env.getRequiredProperty("program.database.driver"));
            result.setUrl(env.getRequiredProperty("program.database.url"));
            result.setUsername(env.getRequiredProperty("program.database.username"));
            result.setPassword(env.getRequiredProperty("program.database.password"));
            return result;
        }
        @Bean
        public DataSourceTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dataSource());
        }
        @Bean
        public TransactionAwareDataSourceProxy transactionAwareDataSource(){
            return new TransactionAwareDataSourceProxy(dataSource());
        }
        @Bean
        public DataSourceConnectionProvider connectionProvider(){
            return new DataSourceConnectionProvider(transactionAwareDataSource());
        }
        @Bean
        public JooqExceptionTranslator jooqExceptionTranslator(){
            return new JooqExceptionTranslator();
        }
        @Bean
        public DefaultConfiguration config(){
            DefaultConfiguration result = new DefaultConfiguration();
            result.set(connectionProvider());
            result.set(new DefaultExecuteListenerProvider(jooqExceptionTranslator()));
            result.set(SQLDialect.POSTGRES);
            return result;
        }
        @Bean
        public DefaultDSLContext db(){
            return new DefaultDSLContext(config());
        }
        @Bean
        public Inserter inserter(){
            return new Inserter();
        }
        @Bean
        public Selector selector(){
            return new Selector();
        }
    }

    @Autowired
    private DSLContext db;
    @Autowired
    private Selector selector;
    @Autowired
    private Inserter inserter;

    private final ThreadPoolExecutor THREAD_POOL = (ThreadPoolExecutor) Executors.newCachedThreadPool();

    @Test
    @FlywayTest
    public void runTest() throws InterruptedException {
        log.debug("Starting test");
        int count0 = db.selectCount().from(SYSTEM_LOG).fetchOne(0, int.class);
        assertTrue(count0 == 0);

        THREAD_POOL.execute(inserter);
        THREAD_POOL.execute(selector);

        THREAD_POOL.shutdown();
        THREAD_POOL.awaitTermination(5, TimeUnit.SECONDS);
        log.debug("Terminated");
    }

}

Selector.java

package program.test.db.transaction;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import static program.db.Tables.SYSTEM_LOG;

@Component
public class Selector implements Runnable {

    private static Logger log = LogManager.getLogger(Selector.class);

    @Autowired
    private DSLContext db;

    @Override
    public void run() {
        try {
            int count1 = db.selectCount().from(SYSTEM_LOG).fetchOne(0, int.class);
            log.debug("Select 1 returned: " + count1);
            Thread.sleep(1000);
            int count2 = db.selectCount().from(SYSTEM_LOG).fetchOne(0, int.class);
            log.debug("Select 2 returned: " + count2);
            Thread.sleep(1000);
            int count3 = db.selectCount().from(SYSTEM_LOG).fetchOne(0, int.class);
            log.debug("Select 3 returned: " + count3);
        } catch (InterruptedException e) {
            log.error("Selects were interrupted", e);
        }
    }

}

Inserter.java

package program.test.db.transaction;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joda.time.DateTime;
import org.jooq.DSLContext;
import org.jooq.InsertQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import program.db.tables.records.SystemLogRecord;
import static org.junit.Assert.assertTrue;
import static program.db.Tables.SYSTEM_LOG;

@Component
public class Inserter implements Runnable {

    private static Logger log = LogManager.getLogger(Inserter.class);

    @Autowired
    private DSLContext db;

    @Override
    public void run() {
        insertSeveralRecords();
    }

    @Transactional
    private void insertSeveralRecords(){
        try {
            Thread.sleep(500);
            insertRecord(1);
            Thread.sleep(1000);
            insertRecord(2);
        } catch (InterruptedException e) {
            log.error("Inserts were interrupted", e);
        }
    }

    private void insertRecord(int i){
        log.debug("inserting record: " + i);
        InsertQuery<SystemLogRecord> insertQuery = db.insertQuery(SYSTEM_LOG);
        insertQuery.addValue(SYSTEM_LOG.SERVICE, "Service " + i);
        insertQuery.addValue(SYSTEM_LOG.MESSAGE, "Message " + i);
        insertQuery.addValue(SYSTEM_LOG.SYS_INSERT_TIME, DateTime.now());
        int result = insertQuery.execute();
        assertTrue(result == 1);
    }
}

我可能在这里遗漏了一些基本的东西 - 我做错了什么?

1 个答案:

答案 0 :(得分:1)

问题中的问题是由:

造成的
  1. 使用@Transactional注释的Inserter.insertSeveralRecords()方法是一个私有方法。

      
        
    • 只应使用@Transactional
    • 注释公共方法   
  2. 使方法Inserter.insertSeveralRecords()公共仍然没有启动事务。这是因为该方法是从Inserter.run()方法内部调用的(而不是从其他类的外部调用)。

      
        
    • 当添加对@Transactional的支持时,Spring使用代理在调用带注释的方法之前和之后添加代码。如果   实现接口的类,这些将是dynamic proxies。这意味着只有外部方法调用才能进入   通过代理将被拦截
    •   
    • 类Inserter实现了Runnable接口 - 因此只有在调用带注释的方法时才会获取@Transactional   直接来自外面
    •   
  3. 将@Transactional注释移动到方法Inserter.run()修复了该类,但仍然不足以成功运行测试。启动时,它现在抛出一个错误:

    &#34;无法自动装配字段:TransactionTest.inserter; NoSuchBeanDefinitionException:找不到类型为[program.test.db.transaction.Inserter]的限定bean,用于依赖&#34;

      

    这是因为TransactionTest.inserter字段的类型为Inserter而不是Runnable,而@Transactional注释被添加到Runnable接口的方法中。我找不到任何关于为什么这样工作的参考,但是将@Autowired字段类型从Inserter更改为Runnable允许Spring正确启动并在执行器调用Inserter.run()时使用事务。 (可能是因为动态代理也是在界面上创建的?)

  4. 以下是上述3个更改的相关代码部分:

    <强> Inserter.java

    @Override
    @Transactional
    public void run() {
        insertSeveralRecords();
    }
    
    private void insertSeveralRecords(){
        try {
            Thread.sleep(500);
            insertRecord(1);
            Thread.sleep(1000);
            insertRecord(2);
        } catch (InterruptedException e) {
            log.error("Inserts were interrupted", e);
        }
    }
    

    <强> TransactionTest.java

    @Autowired
    private DSLContext db;
    @Autowired
    private Runnable selector;
    @Autowired
    private Runnable inserter;
    
    private final ThreadPoolExecutor THREAD_POOL = (ThreadPoolExecutor) Executors.newCachedThreadPool();
    
    @Test
    @FlywayTest
    public void runTest() throws InterruptedException {
        log.debug("Starting test");
        int count0 = db.selectCount().from(SYSTEM_LOG).fetchOne(0, int.class);
        assertTrue(count0 == 0);
    
        THREAD_POOL.execute(inserter);
        THREAD_POOL.execute(selector);
    
        THREAD_POOL.shutdown();
        THREAD_POOL.awaitTermination(5, TimeUnit.SECONDS);
        log.debug("Terminated");
    }
    

    测试现在可以使用事务隔离正确执行,从而在原始问题中生成所需的日志输出。

    使用过的资源:

    1. http://www.baeldung.com/2011/12/26/transaction-configuration-with-jpa-and-spring-3-1/
    2. http://www.nurkiewicz.com/2011/10/spring-pitfalls-proxying.html
    3. http://www.ibm.com/developerworks/java/library/j-jtp08305/index.html