2016-01-15 2 views
3

У меня возникли проблемы с получением транзакций, работающих в этом модульном тесте. Класс TransactionTest содержит всю необходимую конфигурацию Spring. Он запускается, инициализирует базу данных и одновременно запускает две Runnables (Inserter and Selector). Из зарегистрированного вывода видно, что тест выполняется, записи вставляются и выбираются из базы данных в правильном порядке, но нет изоляции транзакций.Весенние транзакции в модульных тестах и ​​настройка на основе аннотаций

Что я ожидал увидеть в журнале что-то вроде:

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

Проблема в вопросе был вызван:

  1. В Inserter.insertSeveralRecords() метода, который был аннотированный с @Transactional был частный способом.

    • Only public methods should be annotated with @Transactional
  2. Выполнение метода Inserter.insertSeveralRecords() общественность все еще не начал транзакцию. Это связано с тем, что метод вызывается внутри метода Inserter.run() (в отличие от внешнего класса из другого класса).

    • When adding support for @Transactional, Spring uses proxies to add code before and after the invocation of the annotated method. In case of classes which implement an interface, these will be dynamic proxies . This means that only external method calls that come in through the proxy will be intercepted
    • The class Inserter implements the Runnable interface - thus @Transactional is only picked up if the annotated method is called directly from outside
  3. Перемещение @Transactional аннотации к методу Inserter.run() устанавливает тот класс, но все еще не достаточно, чтобы успешно запустить тест. При запуске он теперь выдает ошибку:

    «Не удалось создать поле autwire: TransactionTest.inserter; NoSuchBeanDefinitionException: Отсутствие квалификационного компонента типа [program.test.db.transaction.Inserter] найдено зависимость»

    This is caused by the fact that TransactionTest.inserter field is of type Inserter and not Runnable, whereas the @Transactional annotation is added to the method of the Runnable interface. I could not find any reference as to why this works like this, but changing the @Autowired field type from Inserter to Runnable allows Spring to start up correctly and use transactions when the executer calls Inserter.run(). (Probably because the dynamic proxy is created on the interface also?)


Ниже приведены соответствующие разделы кода для 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