0

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.

Ihor M.
  • 2,728
  • 3
  • 44
  • 70
  • Stop mixing spring versions. You are mixing 4.3.25 with 5.x which will lead to all sorts of weird issues. It also looks like your test class contains the other classes, which makes lead-time weaving not work as the classes to weave are already loaded. – M. Deinum Sep 24 '19 at 18:36
  • It seems I do not need that dependency at all. Removing from `pom.xml` and updating the question. – Ihor M. Sep 24 '19 at 19:37
  • I have found this URL: https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/tomcat/InstrumentableClassLoader.html I am using the class that implements `InstrumentableClassLoader`: `WebappClassLoader`. – Ihor M. Sep 24 '19 at 19:48
  • @M.Denium see my comment ^ – Ihor M. Sep 24 '19 at 21:01
  • Are you starting your JVM with `-javaagent:/path/to/aspectjweaver.jar`? I know the documentation says it is not necessary if Spring is configured correctly for the container, but I never got it to work without the Java agent. Try that and if it works either keep doing that or find out how to fix the container or Spring configuration. Sorry, I am an AspectJ expert, I never use Spring in real life. – kriegaex Sep 25 '19 at 02:29
  • As stated your classes are inner classes and thus are already loaded, things that are already loaded cannot be woven. So your test/sample will never work. – M. Deinum Sep 25 '19 at 10:06

0 Answers0