I have this scenario:
- fetch (read and delete) a record from IncomingMessage table
- read record content
- insert something to some tables
- if an error (any exception) occurred in steps 1-3, insert an error-record to OutgoingMessage table
- otherwise, insert an success-record to OutgoingMessage table
So steps 1,2,3,4 should be in a transaction, or steps 1,2,3,5
My process starts from here (it is a scheduled task):
public class ReceiveMessagesJob implements ScheduledJob {
// ...
@Override
public void run() {
try {
processMessageMediator.processNextRegistrationMessage();
} catch (Exception e) {
e.printStackTrace();
}
}
// ...
}
My main function (processNextRegistrationMessage) in ProcessMessageMediator:
public class ProcessMessageMediatorImpl implements ProcessMessageMediator {
// ...
@Override
@Transactional
public void processNextRegistrationMessage() throws ProcessIncomingMessageException {
String refrenceId = null;
MessageTypeEnum registrationMessageType = MessageTypeEnum.REGISTRATION;
try {
String messageContent = incomingMessageService.fetchNextMessageContent(registrationMessageType);
if (messageContent == null) {
return;
}
IncomingXmlModel incomingXmlModel = incomingXmlDeserializer.fromXml(messageContent);
refrenceId = incomingXmlModel.getRefrenceId();
if (!StringUtil.hasText(refrenceId)) {
throw new ProcessIncomingMessageException(
"Can not proceed processing incoming-message. refrence-code field is null.");
}
sqlCommandHandlerService.persist(incomingXmlModel);
} catch (Exception e) {
if (e instanceof ProcessIncomingMessageException) {
throw (ProcessIncomingMessageException) e;
}
e.printStackTrace();
// send error outgoing-message
OutgoingXmlModel outgoingXmlModel = new OutgoingXmlModel(refrenceId,
ProcessResultStateEnum.FAILED.getCode(), e.getMessage());
saveOutgoingMessage(outgoingXmlModel, registrationMessageType);
return;
}
// send success outgoing-message
OutgoingXmlModel outgoingXmlModel = new OutgoingXmlModel(refrenceId, ProcessResultStateEnum.SUCCEED.getCode());
saveOutgoingMessage(outgoingXmlModel, registrationMessageType);
}
private void saveOutgoingMessage(OutgoingXmlModel outgoingXmlModel, MessageTypeEnum messageType)
throws ProcessIncomingMessageException {
String xml = outgoingXmlSerializer.toXml(outgoingXmlModel, messageType);
OutgoingMessageEntity entity = new OutgoingMessageEntity(messageType.getCode(), new Date());
try {
outgoingMessageService.save(entity, xml);
} catch (SaveOutgoingMessageException e) {
throw new ProcessIncomingMessageException("Can not proceed processing incoming-message.", e);
}
}
// ...
}
As i said If any exception occurred in steps 1-3, i want insert an error-record:
catch (Exception e) {
if (e instanceof ProcessIncomingMessageException) {
throw (ProcessIncomingMessageException) e;
}
e.printStackTrace();
//send error outgoing-message
OutgoingXmlModel outgoingXmlModel = new OutgoingXmlModel(refrenceId,ProcessResultStateEnum.FAILED.getCode(), e.getMessage());
saveOutgoingMessage(outgoingXmlModel, registrationMessageType);
return;
}
It’s SqlCommandHandlerServiceImpl.persist() method:
public class SqlCommandHandlerServiceImpl implements SqlCommandHandlerService {
// ...
@Override
@Transactional
public void persist(IncomingXmlModel incomingXmlModel) {
Collections.sort(incomingXmlModel.getTables());
List<ParametricQuery> queries = generateSqlQueries(incomingXmlModel.getTables());
for (ParametricQuery query : queries) {
queryExecuter.executeQuery(query);
}
}
// ...
}
But when sqlCommandHandlerService.persist() throws exception (here a org.hibernate.exception.ConstraintViolationException exception), after inserting an error-record in OutgoingMessage table, when the transaction want to be committed , i get UnexpectedRollbackException. I can’t figure out where is my problem:
Exception in thread "null#0" org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:717)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:394)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:622)
at ir.tamin.branch.insuranceregistration.services.schedular.ReceiveMessagesJob$$EnhancerByCGLIB$$63524c6b.run(<generated>)
at ir.asta.wise.core.util.timer.JobScheduler$ScheduledJobThread.run(JobScheduler.java:132)
I’m using hibernate-4.1.0-Final, My database is oracle, and Here is my transaction-manager bean:
<bean id="transactionManager"
class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager"
proxy-target-class="true" />
Транзакция помечена только как откат: как найти причину
у меня возникли проблемы с фиксацией транзакции в моем @Transactional методе:
methodA() {
methodB()
}
@Transactional
methodB() {
...
em.persist();
...
em.flush();
log("OK");
}
когда я вызываю methodB () из methodA (), метод успешно проходит, и я вижу «ОК» в своих журналах. Но тогда я получаю
Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:521)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:723)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:393)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:622)
at methodA()...
- контекст methodB полностью отсутствует в исключении — что нормально, я полагаю?
- что-то в methodB() помечено транзакцией только как откат? Как я могу это узнать? Есть например способ проверить что-то вроде
getCurrentTransaction().isRollbackOnly()?— как это я мог бы пройти через метод и найти причину.
3177
7
7 ответов:
когда вы отмечаете свой способ как
@Transactional, возникновение любого исключения внутри вашего метода будет отмечать окружающий TX только как откат (даже если вы их поймаете). Вы можете использовать другие атрибуты@Transactionalаннотация, чтобы предотвратить его откат, как:@Transactional(rollbackFor=MyException.class, noRollbackFor=MyException2.class)
Я, наконец, понял проблему:
methodA() { methodB() } @Transactional(noRollbackFor = Exception.class) methodB() { ... try { methodC() } catch (...) {...} log("OK"); } @Transactional methodC() { throw new ...(); }что происходит, что хотя
methodBимеет право аннотацииmethodCнет. Когда исключение выбрасывается, второй@Transactionalпервый транзакции, отката в любом случае.
чтобы быстро получить вызывающее исключение без необходимости перекодировать или перестроить установите точку останова на
org.hibernate.ejb.TransactionImpl.setRollbackOnly() // Hibernate < 4.3, or org.hibernate.jpa.internal.TransactionImpl() // as of Hibernate 4.3и поднимайтесь в стек, обычно к какому-нибудь перехватчику. Там вы можете прочитать вызывающее исключение из некоторого блока catch.
Я боролся с этим исключением во время работы моего приложения.
наконец-то проблема была на SQL-запрос. я имею в виду, что запрос неверен.
пожалуйста, проверьте ваш запрос. Это мое предложение
ищите исключения, которые были брошены и пойманы в
...разделы вашего кода. Во время выполнения и для отката на предыдущую версию исключений приложения вызвать откат, когда выгоняют из бизнес-метод, даже если поймали на каком-то другом месте.вы можете использовать контекст, чтобы узнать, помечена ли транзакция для отката.
@Resource private SessionContext context; context.getRollbackOnly();
отключите transactionmanager в вашем компоненте.xml
<tx:annotation-driven proxy-target-class="true" transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean>закомментируйте эти строки, и вы увидите исключение, вызывающее откат 😉
нашел хорошее объяснение с решениями: https://vcfvct.wordpress.com/2016/12/15/spring-nested-transactional-rollback-only/
1) Удалите @Transacional из вложенного метода, если он действительно не требует управления транзакциями. Так что даже у него есть исключение, он просто пузырится и не влияет на транзакционные вещи.
или:
2) Если вложенный метод требует управления транзакциями, сделайте его как REQUIRE_NEW для политики распространения, которая способ даже если выбрасывает исключение и помечается только как откат, вызывающий объект не будет затронут.
Home
>
Java
>
Detail page
In spring transaction management, many people will encounter such an exception:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:718) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:475) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:270) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:96) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:260) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94)
The scene of the above problem is similar to the following code:
ITestAService:
package com.gigamore.platform.ac.service; import com.onlyou.framework.exception.BusinessException; public interface ITestAService { void testA() throws BusinessException; }
TestAService:
package com.gigamore.platform.ac.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.gigamore.platform.base.service.impl.BaseServiceImpl; import com.onlyou.framework.exception.BusinessException; @Service public class TestAService extends BaseServiceImpl implements ITestAService{ @Autowired private TestBService testBService; @Transactional public void testA(){ try{ testBService.testB(); }catch(BusinessException e){ logger.info(e.getMessage()); }catch(Exception e){ logger.info(e.getMessage()); } } }
TestBService:
package com.gigamore.platform.ac.service; import java.util.Date; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.gigamore.platform.ac.entity.LoanProjectEntity; import com.gigamore.platform.base.service.impl.BaseServiceImpl; import com.onlyou.framework.exception.BusinessException; @Service public class TestBService extends BaseServiceImpl{ @Transactional public void testB(){ LoanProjectEntity project = this.selectByPrimaryKey(LoanProjectEntity.class, "2c9483e748321d4601485e1714d31412"); project.setUpdDataTm(new Date()); this.update(project); throw new BusinessException(" Throwing anomaly "); } }
Test case:
@Autowired private ITestAService testAService; @Test public void testA() { testAService.testA(); }
TestAService calls the testB() method of testBService, and throws a BusinessException exception in the testB() method, but testAService catches the exception with the try{}catch{} and does not throw it to the upper level.
It doesn’t seem to be a problem. the exception has been caught. In fact, when the testAService calls testBService testB() method, will go through a spring transaction control section, transaction itself will catch the exception from testB() method of testBService: TransactionAspectSupport.invokeWithinTransaction
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls. TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } commitTransactionAfterReturning(txInfo); return retVal; }
«completeTransactionAfterThrowing(txInfo, ex)» called «txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus())», the transaction manager rollback, the transaction is set to rollback-only.
The solution:
change the transaction annotation of the testB () method of TestBService to
@Transactional(propagation = Propagation.NESTED)
and indeed achieve the effect of avoiding exceptions.
Posted by seakwen
in Java
at Jul 26, 2017 — 9:57 AM
Tag:
Spring

Rollback по умолчанию
Предположим, что у нас есть сервис, который создает трех пользователей в рамках одной транзакции. Если что-то идет не так, выбрасывается java.lang.Exception.
@Service
public class PersonService {
@Autowired
private PersonRepository personRepository;
@Transactional
public void addPeople(String name) throws Exception {
personRepository.saveAndFlush(new Person("Jack", "Brown"));
personRepository.saveAndFlush(new Person("Julia", "Green"));
if (name == null) {
throw new Exception("name cannot be null");
}
personRepository.saveAndFlush(new Person(name, "Purple"));
}
}
А вот простой unit-тест.
@SpringBootTest
@AutoConfigureTestDatabase
class PersonServiceTest {
@Autowired
private PersonService personService;
@Autowired
private PersonRepository personRepository;
@BeforeEach
void beforeEach() {
personRepository.deleteAll();
}
@Test
void shouldRollbackTransactionIfNameIsNull() {
assertThrows(Exception.class, () -> personService.addPeople(null));
assertEquals(0, personRepository.count());
}
}
Как думаете, тест завершится успешно, или нет? Логика говорит нам, что Spring должен откатить транзакцию из-за исключения. Следовательно personRepository.count() должен вернуть 0, так ведь? Не совсем.
expected: <0> but was: <2>
Expected :0
Actual :2
Здесь требуются некоторые объяснения. По умолчанию Spring откатывает транзакции только в случае непроверяемого исключения. Проверяемые же считаются «восстанавливаемыми» из-за чего Spring вместо rollback делает commit. Поэтому personRepository.count() возращает 2.
Самый простой исправить это — заменить Exception на непроверяемое исключение. Например, NullPointerException. Либо можно переопределить атрибут rollbackFor у аннотации.
Например, оба этих метода корректно откатывают транзакцию.
@Service
public class PersonService {
@Autowired
private PersonRepository personRepository;
@Transactional(rollbackFor = Exception.class)
public void addPeopleWithCheckedException(String name) throws Exception {
addPeople(name, Exception::new);
}
@Transactional
public void addPeopleWithNullPointerException(String name) {
addPeople(name, NullPointerException::new);
}
private <T extends Exception> void addPeople(String name, Supplier<? extends T> exceptionSupplier) throws T {
personRepository.saveAndFlush(new Person("Jack", "Brown"));
personRepository.saveAndFlush(new Person("Julia", "Green"));
if (name == null) {
throw exceptionSupplier.get();
}
personRepository.saveAndFlush(new Person(name, "Purple"));
}
}
@SpringBootTest
@AutoConfigureTestDatabase
class PersonServiceTest {
@Autowired
private PersonService personService;
@Autowired
private PersonRepository personRepository;
@BeforeEach
void beforeEach() {
personRepository.deleteAll();
}
@Test
void testThrowsExceptionAndRollback() {
assertThrows(Exception.class, () -> personService.addPeopleWithCheckedException(null));
assertEquals(0, personRepository.count());
}
@Test
void testThrowsNullPointerExceptionAndRollback() {
assertThrows(NullPointerException.class, () -> personService.addPeopleWithNullPointerException(null));
assertEquals(0, personRepository.count());
}
}

Rollback при «глушении» исключения
Не все исключения должны быть проброшены вверх по стеку вызовов. Иногда вполне допустимо отловить его внутри метода и залогировать информацию об этом.
Предположим, что у нас есть еще один транзакционный сервис, который проверяет, может ли быть создан пользователь с переданным именем. Если нет, выбрасывается IllegalArgumentException.
@Service
public class PersonValidateService {
@Autowired
private PersonRepository personRepository;
@Transactional
public void validateName(String name) {
if (name == null || name.isBlank() || personRepository.existsByFirstName(name)) {
throw new IllegalArgumentException("name is forbidden");
}
}
}
Давайте добавим валидацию в наш PersonService.
@Service
@Slf4j
public class PersonService {
@Autowired
private PersonRepository personRepository;
@Autowired
private PersonValidateService personValidateService;
@Transactional
public void addPeople(String name) {
personRepository.saveAndFlush(new Person("Jack", "Brown"));
personRepository.saveAndFlush(new Person("Julia", "Green"));
String resultName = name;
try {
personValidateService.validateName(name);
}
catch (IllegalArgumentException e) {
log.error("name is not allowed. Using default one");
resultName = "DefaultName";
}
personRepository.saveAndFlush(new Person(resultName, "Purple"));
}
}
Если валидация не проходит, создаем пользователя с именем по умолчанию.
Окей, теперь нужно протестировать новую функциональность.
@SpringBootTest
@AutoConfigureTestDatabase
class PersonServiceTest {
@Autowired
private PersonService personService;
@Autowired
private PersonRepository personRepository;
@BeforeEach
void beforeEach() {
personRepository.deleteAll();
}
@Test
void shouldCreatePersonWithDefaultName() {
assertDoesNotThrow(() -> personService.addPeople(null));
Optional<Person> defaultPerson = personRepository.findByFirstName("DefaultName");
assertTrue(defaultPerson.isPresent());
}
}
Однако результат оказывается довольно неожиданным.
Unexpected exception thrown:
org.springframework.transaction.UnexpectedRollbackException:
Transaction silently rolled back because it has been marked as rollback-only
Странно. Мы отловили исключение. Почему же Spring откатил транзакцию? Прежде всего нужно разобраться с тем, как Spring работает с транзакциями.
Под капотом Spring применяет паттерн аспектно-ориентированного программирования. Опуская сложные детали, идея заключается в том, что bean оборачивается в прокси, который генерируются в процессе старта приложения. Внутри этого прокси выполняется требуемая логика. В нашем случае, управление транзакциями. Когда какой-нибудь bean указывает транзакционный сервис в качестве DI зависимости, Spring на самом деле внедряет прокси.
Ниже представлен workflow вызова вышенаписанного метода addPeople.

Параметр propagation у @Transactional по умолчанию имеет значение REQUIRED. Это значит, что новая транзакция создается, если она отсутствует. Иначе выполнение продолжается в текущей. Так что в нашем случае весь запрос выполняется в рамках единственной транзакции.
Однако здесь есть нюанс. Если RuntimeException был выброшен из-за границ transactional proxy, то Spring отмечает текущую транзакцию как rollback-only. Здесь у нас именно такой случай. PersonValidateService.validateName выбросил IllegalArgumentException. Transactional proxy выставил флаг rollback. Дальнейшие операции уже не имеют значения, так как в конечном итоге транзакция не закоммитится.
Каково решение проблемы? Вообще их несколько. Например, мы можем добавить атрибут noRollbackFor в PersonValidateService.
@Service
public class PersonValidateService {
@Autowired
private PersonRepository personRepository;
@Transactional(noRollbackFor = IllegalArgumentException.class)
public void validateName(String name) {
if (name == null || name.isBlank() || personRepository.existsByFirstName(name)) {
throw new IllegalArgumentException("name is forbidden");
}
}
}
Есть вариант поменять propagation на REQUIRES_NEW. В этом случае PersonValidateService.validateName будет выполнен в отдельной транзакции. Так что родительская не будет отменена.
@Service
public class PersonValidateService {
@Autowired
private PersonRepository personRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validateName(String name) {
if (name == null || name.isBlank() || personRepository.existsByFirstName(name)) {
throw new IllegalArgumentException("name is forbidden");
}
}
}
Возможные проблемы с Kotlin
У Kotlin много схожестей с Java. Но управление исключениями не является одной из них.
Kotlin убрал понятия проверяемых и непроверяемых исключений. Строго говоря, все исключения в Kotlin непроверяемые, потому что нам не требуется указывать конструкции throws SomeException в сигнатурах методов, а также оборачивать их вызовы в try-catch. Обсуждение плюсов и минусов такого решения — тема для отдельной статьи. Но сейчас я хочу продемонстрировать вам проблемы, которые могут из-за этого возникнуть при использовании Spring Data.
Давайте перепишем самый первый пример с java.lang.Exception на Kotlin.
@Service
class PersonService(
@Autowired
private val personRepository: PersonRepository
) {
@Transactional
fun addPeople(name: String?) {
personRepository.saveAndFlush(Person("Jack", "Brown"))
personRepository.saveAndFlush(Person("Julia", "Green"))
if (name == null) {
throw Exception("name cannot be null")
}
personRepository.saveAndFlush(Person(name, "Purple"))
}
}
@SpringBootTest
@AutoConfigureTestDatabase
internal class PersonServiceTest {
@Autowired
lateinit var personRepository: PersonRepository
@Autowired
lateinit var personService: PersonService
@BeforeEach
fun beforeEach() {
personRepository.deleteAll()
}
@Test
fun `should rollback transaction if name is null`() {
assertThrows(Exception::class.java) { personService.addPeople(null) }
assertEquals(0, personRepository.count())
}
}
Тест падает как в Java.
expected: <0> but was: <2>
Expected :0
Actual :2
Здесь нет ничего удивительного. Spring управляет транзакциями в Kotlin ровно так же, как и в Java. Но в Java мы не можем вызвать метод, который выбрасывает java.lang.Exception, не оборачивая инструкцию в try-catch или не пробрасывая исключение дальше. Kotlin же позволяет. Это может привести к непредвиденным ошибкам и трудно уловимым багам. Так что к таким вещам следует относиться вдвойне внимательнее.
Строго говоря, в Java есть хак, который позволяет выбросить checked исключения, не указывая
throwsв сигнатуре.
Заключение
Это все, что я хотел рассказать о @Transactionalв Spring. Если у вас есть какие-то вопросы или пожелания, пожалуйста, оставляйте комментарии. Спасибо за чтение!
Spring — самый популярный фреймворк в мире Java. Разработчикам из «коробки» доступны инструменты для API, ролевой модели, кэширования и доступа к данным. Spring Data в особенности делает жизнь программиста гораздо легче. Нам больше не нужно беспокоиться о соединениях к базе данных и управлении транзакциями. Фреймворк сделает все за нас. Однако тот факт, что многие детали остаются для нас скрытыми, может привести к трудно отлавливаемым багам и ошибкам. Так что давайте глубже погрузимся в аннотацию @Transaсtional и узнаем, что же там происходит.