[Из песочницы] Spring: Реализация TaskExecutor c поддержкой транзакций
Spring, позаботившись о разработчиках, предлагает удобный и простой фасад для взаимодействия с менеджером транзакций. Однако всегда ли стандартного механизма будет достаточно для реализации изощрённых архитектурных идей? Очевидно — нет.В этом посте пойдёт речь о возможностях Spring —
взглянем на примеры стандартного управления транзакциями с помощью аннотаций,
поймём — когда решить задачу с помощью аннотаций не получится,
и, судя по заголовку статьи, дадим пример реализации транзакционного исполнения кода в новом потоке, создавемых с помощью Spring TaskExecutor.
Наиболее общеупотребимо задание транзакций с помощью аннотации @Transactional. Что бы воспользоваться этим механизмом достаточно сконфигурировать предпочитаемый TransactionManager и включить обработку аннотаций. В случае конфигурации с помощью XML файлов это выглядит примерно так:
…
@Service public class FooServiceImpl implements FooService { @Autowired private FooDao fooDao;
@Transactional @Override public void update (Foo entity) { fooDao.merge (entity); } } Однако, что бы эффективно использовать этот механизм нужно помнить о нескольких, невсегда очевидных тонкостях: Класс должен быть объявлен как bean (c помощью аннотаций, code-based или в xml конфигурации контейнера) Класс должен реализовывать интерфейс, иначе контейнеру будет сложно создать прокси-объект, с помощью которого и выполняется управление транзакцией. Вызов транзакционных методов из другого метода того же класса не приведёт к созданию транзакции! (следствие из предыдущего пункта) Подобный механизм позволяет не писать код управления транзакцией каждый раз! Однако, могут ли быть ситуации, когда этого механизма будет недостаточно? или возможен ли контекст, в котором аннотациями обойтись не удастся? Боюсь, ответ очевиден. К примеру, мы можем захотеть выполнить часть кода в одной транзакции, а другую часть — во второй (тут скорее архитектурно верным будет разделить метод на два). У читателей, думаю, есть и свои примеры.
Более реалистичен пример, когда часть кода нужно выполнить асинхронно:
@Service public class FooServiceImpl implements FooService { @Autowired private TaskExecutor taskExecutor; @Autowired private FooDao fooDao;
@Transactional @Override public void update (Foo entity) { fooDao.merge (entity); taskExecutor.run (new Runnable () { public void run () { someLongTimeOperation (entity); } }); } @Transactional @Override public void someLongTimeOperation (Foo entity) { // тут набор ресурсоёмких операций } } Что же получается: до старта метода update () создаётся транзакция, затем выполняются операции из тела, а по выходу из метода транзакция закрывается. Но в нашем случае создаётся новый поток, в котором будет исполнен код. И весьма очевидно, что на момент выхода из метода update () и сопутствующего уничтожения транзакции, выполнение кода во втором запущенном потоке может/будет продолжаться. Как итог, по завершению метода, во втором потоке получим исключение и вся транзакция «ролбэкнится».
К предудщему примеру добавим ручное создание транзакции:
@Service public class FooServiceImpl implements FooService { @Autowired private TaskExecutor taskExecutor; @Autowired private FooDao fooDao;
@Transactional @Override public void update (final Foo entity) { fooDao.merge (entity); final TransactionTemplate transactionTemplate = new TransactionTemplate (transactionManager); taskExecutor.execute (new Runnable () { @Override public void run () { transactionTemplate.execute (new TransactionCallbackWithoutResult () { @Override protected void doInTransactionWithoutResult (TransactionStatus status) { someLongTimeOperation (entity); } }); } }); }
@Transactional @Override public void someLongTimeOperation (Foo entity) { // тут набор ресурсоёмких операций } } Теперь someLongTimeOperation () исполняется асинхронно и в выделенной транзакции. Однако, хочется обощённой реализации, что бы не дублировать громоздкий код ручного управления.
Что ж… вот и она:
public interface TransactionalAsyncTaskExecutor extends AsyncTaskExecutor { void execute (Runnable task, Integer propagation, Integer isolationLevel); } public class DelegatedTransactionalAsyncTaskExecutor implements InitializingBean, TransactionalAsyncTaskExecutor { private PlatformTransactionManager transactionManager; private AsyncTaskExecutor delegate; private TransactionTemplate sharedTransactionTemplate;
public DelegatedTransactionalAsyncTaskExecutor () { }
public DelegatedTransactionalAsyncTaskExecutor (PlatformTransactionManager transactionManager, AsyncTaskExecutor delegate) { this.transactionManager = transactionManager; this.delegate = delegate; }
@Override public void execute (final Runnable task, Integer propagation, Integer isolationLevel) { final TransactionTemplate transactionTemplate = new TransactionTemplate (transactionManager); transactionTemplate.setPropagationBehavior (propagation); transactionTemplate.setIsolationLevel (isolationLevel); delegate.execute (new Runnable () { @Override public void run () { transactionTemplate.execute (new TransactionCallbackWithoutResult () { @Override protected void doInTransactionWithoutResult (TransactionStatus status) { task.run (); } }); } }); }
@Override public void execute (final Runnable task) { execute (task, TransactionDefinition.PROPAGATION_REQUIRED, TransactionDefinition.ISOLATION_DEFAULT); }
@Override public void execute (final Runnable task, long startTimeout) { final TransactionTemplate transactionTemplate = getSharedTransactionTemplate (); delegate.execute (new Runnable () { @Override public void run () { transactionTemplate.execute (new TransactionCallbackWithoutResult () { @Override protected void doInTransactionWithoutResult (TransactionStatus status) { task.run (); } }); } }, startTimeout);
}
@Override public Future> submit (final Runnable task) { final TransactionTemplate transactionTemplate = getSharedTransactionTemplate (); return delegate.submit (new Runnable () { @Override public void run () { transactionTemplate.execute (new TransactionCallbackWithoutResult () { @Override protected void doInTransactionWithoutResult (TransactionStatus status) { task.run (); } }); } }); }
@Override
public
public PlatformTransactionManager getTransactionManager () { return transactionManager; }
public void setTransactionManager (PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; }
public AsyncTaskExecutor getDelegate () { return delegate; }
public void setDelegate (AsyncTaskExecutor delegate) { this.delegate = delegate; }
public TransactionTemplate getSharedTransactionTemplate () { return sharedTransactionTemplate; }
public void setSharedTransactionTemplate (TransactionTemplate sharedTransactionTemplate) { this.sharedTransactionTemplate = sharedTransactionTemplate; }
@Override public void afterPropertiesSet () { if (transactionManager == null) { throw new IllegalArgumentException («Property 'transactionManager' is required»); } if (delegate == null) { delegate = new SimpleAsyncTaskExecutor (); } if (sharedTransactionTemplate == null) { sharedTransactionTemplate = new TransactionTemplate (transactionManager); } } } Это реализация есть обёртка, в итоге делигирующая вызовы к любому TaskExecutor коих несколько в составе Spring. При этом каждый вызов «завёрнут» в транзакцию. Вручную управлять транзакциями в Spring можно используя TransactionTemplate, а вот EntityManager#getTransaction () выдаёт исключение.
Ну и наконец около практический пример в действии:
Конфигурируем TaskExecutor:
@Service public class FooServiceImpl implements FooService { @Autowired private TransactionalAsyncTaskExecutor trTaskExecutor; @Autowired private FooDao fooDao;
@Transactional @Override public void update (Foo entity) { fooDao.merge (entity); // Выполнится в транзакции созданной Spring’ом (tr_1). trTaskExecutor.run (new Runnable () { // Запустится новый поток и новая транзакция (tr_2), метод run () выполнится паралельно текущему потоку и в рамках транзакции tr_2. public void run () { someLongTimeOperation (); } }); } // Выход из метода и вместе с этим tr_1 завершится. Обрабока tr_2 осуществится с помощью TransactionTemplate. @Transactional @Override public void someLongTimeOperation (Foo entity) { // тут набор ресурсоёмких операций } } Таким образом, имеем вполне обобщённую реализацию обёртки для TaskExecutor, позволяющую избежать дублирования кода создания транзакций.