Поддержка транзакций при весенней загрузке Tomcat, с использованием AspectJ Weaving (LTW) - PullRequest
0 голосов
/ 24 сентября 2019

Мне сложно настроить поддержку транзакций в Spring-Boot 2.0.3 с AspectJ LTW (время загрузки).Мой весенний бот запускает встроенный контейнер сервлета Tomcat.На моем уровне персистентности я не использую JPA, а вместо этого Spring JDBC Template.

Я выбрал режим AspectJ для управления транзакциями, потому что мы используем довольно большой проект с вложенными транзакциями, и иногда трудно отследитьиз всех приложений @Transactional аннотации.Поэтому, когда эта аннотация используется, я хочу получить предсказуемый результат - атомарную работу с БД.Я не хочу думать о том, есть ли у нас самопризыв, или метод, который помечен как транзакционный, является общедоступным.

Я прочитал кучу документации, касающейся поддержки транзакций весной и как настроить LTW AspectJ ткачество,К сожалению, я не могу заставить это работать.Я создал тест (класс тестовой загрузки), который предназначен для имитации различных сбоев в коде, который должен быть транзакционным (см. Ниже).Кроме того, я не вижу, чтобы ткачество действительно происходило.Я явно что-то упускаю, не могу понять, что.

Мой тестовый класс:

@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
        }

    }

}

Вот мой класс конфигурации:

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;
        }
    }

Вот частьМой pom.xml, который добавляет интересующие зависимости:

<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>

Любая помощь будет принята с благодарностью.Я знаю, что это немного сложная тема, но я не думаю, что это должно быть так сложно.Я думаю, что в документации Spring отсутствуют примеры того, как правильно выполнить такую ​​конфигурацию.Кроме того, я не нашел никаких историй успеха с подобной настройкой.

...