I am having a hard time configuring transactional support in spring-boot 2.0.3 with AspectJ LTW (load-time weaving). My spring-boot is running embedded Tomcat servlet container. In my persistence layer, I am not using JPA, but Spring JDBC Template instead.
I opted for AspectJ mode for transaction management because we are leveraging a rather big project with nested transactions and sometimes it is hard to keep track of all the applications of @Transactional
annotation. So that when this annotation is being used I want to have a predictable result - atomic DB operation. I do not want to think about whether we have a self-invocation or method that is marked to be transactional is public.
I have read a bunch of documentation regarding transaction support in spring and how to configure LTW AspectJ weaving. Unfortunately, I cannot make it work. I have created a test (spring-boot test class) that is meant to mimic different failures in a code that should be transactional (see it below). Also, I cannot see the weaving actually happening. I am clearly missing something, cannot figure out what.
My test class:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = TestConfig.class)
@ActiveProfiles("TEST")
public class TransactionalIT {
@SpyBean
private JdbcTemplate jdbcTemplate;
// we need this guy in order to perform a cleanup in static @AfterClass method
private static JdbcTemplate jdbcTemplateInStaticContext;
@Autowired
private PlatformTransactionManager txManager;
@Spy
private NestedTransactionsJdbcDao dao;
@Before
public void setUp() {
if (jdbcTemplateInStaticContext == null) {
// Making sure we're working with the proper tx manager
assertThat(txManager).isNotNull();
assertThat(txManager.getClass()).isEqualTo(DataSourceTransactionManager.class);
jdbcTemplateInStaticContext = jdbcTemplate;
jdbcTemplateInStaticContext.execute("CREATE TABLE entity_a (id varchar(12) PRIMARY KEY, name varchar(24), description varchar(255));");
jdbcTemplateInStaticContext.execute("CREATE TABLE entity_b (id varchar(12) PRIMARY KEY, name varchar(24), description varchar(255));");
jdbcTemplateInStaticContext.execute("CREATE TABLE entity_a_to_b_assn (entity_a_id varchar(12) NOT NULL, entity_b_id varchar(12) NOT NULL, " +
"CONSTRAINT fk_entity_a FOREIGN KEY (entity_a_id) REFERENCES entity_a(id), " +
"CONSTRAINT fk_entity_b FOREIGN KEY (entity_b_id) REFERENCES entity_b(id), " +
"UNIQUE (entity_a_id, entity_b_id));");
}
}
@AfterClass
public static void cleanup() {
if (jdbcTemplateInStaticContext != null) {
jdbcTemplateInStaticContext.execute("DROP TABLE entity_a_to_b_assn;");
jdbcTemplateInStaticContext.execute("DROP TABLE entity_a;");
jdbcTemplateInStaticContext.execute("DROP TABLE entity_b;");
}
}
@Test
public void createObjectGraph_FailsDuring_AnAttemptToCreate3rdEntityA() {
doThrow(new RuntimeException("blah!")).when(jdbcTemplate).update(eq("INSERT INTO entity_a (id, name, description) VALUES(?, ?, ?);"),
eq("a3"), eq("entity a3"), eq("descr_a_3"));
try {
dao.createObjectGraph(getObjectGraph());
fail("Should never reach this point");
} catch (RuntimeException e) {
assertThat(e.getMessage()).isEqualTo("blah!");
assertDbCounts(0L, 0L, 0L);
}
}
private void assertDbCounts(long expectedACount, long expectedBCount, long expectedAToBCount) {
Long actualACount = jdbcTemplate.queryForObject("SELECT count(*) count_a FROM entity_a", new LongRowMapper());
assertThat(actualACount).isEqualTo(expectedACount);
Long actualBCount = jdbcTemplate.queryForObject("SELECT count(*) count_b FROM entity_b", new LongRowMapper());
assertThat(actualBCount).isEqualTo(expectedBCount);
Long actualAToBCount = jdbcTemplate.queryForObject("SELECT count(*) count_a_to_b FROM entity_b", new LongRowMapper());
assertThat(actualAToBCount).isEqualTo(expectedAToBCount);
}
private final class LongRowMapper implements RowMapper<Long> {
@Override
public Long mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getLong(1);
}
}
private ObjectGraph getObjectGraph() {
EntityA a1 = new EntityA("a1", "entity a1", "descr_a_1");
EntityA a2 = new EntityA("a2", "entity a2", "descr_a_2");
EntityA a3 = new EntityA("a3", "entity a3", "descr_a_3");
EntityB b1 = new EntityB("b1", "entity b1", "descr_b_1");
EntityB b2 = new EntityB("b2", "entity b2", "descr_b_2");
EntityB b3 = new EntityB("b3", "entity b3", "descr_b_3");
AtoBAssn a1b1 = new AtoBAssn("a1", "b1");
AtoBAssn a1b3 = new AtoBAssn("a1", "b3");
AtoBAssn a2b2 = new AtoBAssn("a2", "b2");
AtoBAssn a2b3 = new AtoBAssn("a2", "b3");
AtoBAssn a3b1 = new AtoBAssn("a3", "b1");
return new ObjectGraph(
Lists.newArrayList(a1, a2, a3),
Lists.newArrayList(b1, b2, b3),
Lists.newArrayList(a1b1, a1b3, a2b2, a2b3, a3b1));
}
@Data
@AllArgsConstructor
private class EntityA {
private String id;
private String name;
private String description;
}
@Data
@AllArgsConstructor
private class EntityB {
private String id;
private String name;
private String description;
}
@Data
@AllArgsConstructor
private class AtoBAssn {
private String idA;
private String idB;
}
@Data
@AllArgsConstructor
private class ObjectGraph {
private List<EntityA> aList;
private List<EntityB> bList;
List<AtoBAssn> aToBAssnList;
}
@Repository
public class NestedTransactionsJdbcDao {
@Transactional
public void createObjectGraph(ObjectGraph og) {
createEntitiesA(og.getAList());
createEntitiesB(og.getBList());
createAtoBAssn(og.getAToBAssnList());
doSomethingElse();
}
@Transactional
public void createEntitiesA(List<EntityA> aList) {
aList.forEach(a ->
jdbcTemplate.update("INSERT INTO entity_a (id, name, description) VALUES(?, ?, ?);",
a.getId(), a.getName(), a.getDescription()));
}
@Transactional
public void createEntitiesB(List<EntityB> bList) {
bList.forEach(b ->
jdbcTemplate.update("INSERT INTO entity_b (id, name, description) VALUES(?, ?, ?);",
b.getId(), b.getName(), b.getDescription()));
}
@Transactional
/**
* Intentionally access is set to package-private
*/
void createAtoBAssn(List<AtoBAssn> aToBAssnList) {
aToBAssnList.forEach(aToB ->
jdbcTemplate.update("INSERT INTO entity_a_to_b_assn (entity_a_id, entity_b_id) VALUES(?, ?);",
aToB.getIdA(), aToB.getIdB()));
}
void doSomethingElse() {
// Intentionally left blank
}
}
}
Here is my configuration class:
import org.apache.catalina.loader.WebappClassLoader;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableLoadTimeWeaving;
import org.springframework.context.annotation.Primary;
import org.springframework.instrument.classloading.LoadTimeWeaver;
import org.springframework.instrument.classloading.tomcat.TomcatLoadTimeWeaver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.aspectj.AnnotationTransactionAspect;
@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
@EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.ENABLED)
public class EventCoreConfig {
@Bean
public LoadTimeWeaver loadTimeWeaver() {
// https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/tomcat/InstrumentableClassLoader.html
return new TomcatLoadTimeWeaver(new WebappClassLoader());
}
@Bean
@Primary
public PlatformTransactionManager txManager(DataSource dataSource) {
DataSourceTransactionManager txManager = new DataSourceTransactionManager(dataSource);
AnnotationTransactionAspect aspect = new AnnotationTransactionAspect();
aspect.setTransactionManager(txManager);
return txManager;
}
}
Here is the portion of my pom.xml that is adding dependencies of interest:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ch.vorburger.mariaDB4j</groupId>
<artifactId>mariaDB4j</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
Any help will be greatly appreciated. I know this is a little advanced topic, but I do not think it should be that complicated. I think Spring's documentation lacks examples of how to properly perform this kind of configuration. Also, I haven't found any success stories over there with a similar setup.