Skip to content

@Transactional does not work in spring-boot running Tomcat, with AspectJ load-time weaving (LTW) #23689

@imochurad

Description

@imochurad

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() {
            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>org.springframework</groupId>
    <artifactId>spring-instrument-tomcat</artifactId>
    <version>4.3.25.RELEASE</version>
</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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    for: stackoverflowA question that's better suited to stackoverflow.com

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions