我无法在此单元测试中处理事务。 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);
}
}
我可能在这里遗漏了一些基本的东西 - 我做错了什么?
答案 0 :(得分:1)
问题中的问题是由:
造成的使用@Transactional注释的Inserter.insertSeveralRecords()方法是一个私有方法。
- 只应使用@Transactional
注释公共方法
使方法Inserter.insertSeveralRecords()公共仍然没有启动事务。这是因为该方法是从Inserter.run()方法内部调用的(而不是从其他类的外部调用)。
- 当添加对@Transactional的支持时,Spring使用代理在调用带注释的方法之前和之后添加代码。如果 实现接口的类,这些将是dynamic proxies。这意味着只有外部方法调用才能进入 通过代理将被拦截
- 类Inserter实现了Runnable接口 - 因此只有在调用带注释的方法时才会获取@Transactional 直接来自外面
将@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()时使用事务。 (可能是因为动态代理也是在界面上创建的?)
以下是上述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");
}
测试现在可以使用事务隔离正确执行,从而在原始问题中生成所需的日志输出。
使用过的资源: