Ошибка сообщение transaction rolled back because it has been marked as rollback only

I have this scenario:

  1. fetch (read and delete) a record from IncomingMessage table
  2. read record content
  3. insert something to some tables
  4. if an error (any exception) occurred in steps 1-3, insert an error-record to OutgoingMessage table
  5. 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 x.y.z.ReceiveMessagesJob$$EnhancerByCGLIB$$63524c6b.run(<generated>)
    at x.y.z.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" />

Сегодня я рад поехать на работу. Как только я включу компьютер, чтобы запустить проект, я внезапно запрашиваю

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

Что, какие операции, код не двигался, так что 🤔, затем начните тщательно анализировать, сначала, сначалаТогда methoda ()-> methodb (), аномалия метода данных вызвала нарушения бизнеса. Метод B был брошен вверх. Когда B () возвращалась, транзакция была установлена ​​на отказ только для отката и не продолжал выбросить. Затем, когда a () закончится, транзакция выполнит коммит, но транзакция была установлена ​​только для отката.

Так что эта ошибка произойдет.

Посмотрите на исходный код

public final void commit(TransactionStatus status) throws TransactionException {
        if (status.isCompleted()) {
            throw new IllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");
        } else {
            DefaultTransactionStatus defStatus = (DefaultTransactionStatus)status;
            if (defStatus.isLocalRollbackOnly()) {
                if (defStatus.isDebug()) {
                    this.logger.debug("Transactional code has requested rollback");
                }

                this.processRollback(defStatus);
            // выполнит это решение
            } else if (!this.shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
               
                if (defStatus.isDebug()) {
                    this.logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
                }
                                   // выполнить откат
                this.processRollback(defStatus);
                                 // ненормальное суждение также является местом, которое выброшено из проблемы в этом вопросе
                if (status.isNewTransaction() || this.isFailEarlyOnGlobalRollbackOnly()) {
                    throw new UnexpectedRollbackException("Transaction rolled back because it has been marked as rollback-only");
                }
            } else {
                this.processCommit(defStatus);
            }
        }

Так как это решить?

Поскольку мой a () -это метод с прочтением, я повторно сфигурировал механизм изоляции

@Transactional(readOnly = true,propagation = Propagation.SUPPORTS)

Это состояние суммировано в одном предложении: «Если вам есть что присоединиться, все создано». Для единственного способа прочитать, это не имеет значения, если он не включен.

После пропуска работы по прекращению совершения транзакции в A () она также избегала аномального броска. Конечно, его можно изменить, чтобы продолжать устранять аномалии и обрабатывать его на уровне контроллера.

Примечание. Аномалии, брошенные мне, как можно больше для Runtimeexception или их подклассов, потому что транзакция Spring откатится назад по умолчаниюRuntimeException,

Даже если он настроенrollbackFor = Exception.classНезависимо от того, как его использовать

Оглядываясь назад на уровень изоляции транзакции следующей весной:

MANDATORY 
          Support a current transaction, throw an exception if none exists.
NESTED 
          Execute within a nested transaction if a current transaction exists, behave like PROPAGATION_REQUIRED else.
NEVER 
          Execute non-transactionally, throw an exception if a transaction exists.
NOT_SUPPORTED 
          Execute non-transactionally, suspend the current transaction if one exists.
REQUIRED 
          Support a current transaction, create a new one if none exists.
REQUIRES_NEW 
          Create a new transaction, suspend the current transaction if one exists.
SUPPORTS 
          Support a current transaction, execute non-transactionally if none exists.

Solution 1

This is the normal behavior and the reason is that your sqlCommandHandlerService.persist method needs a TX when being executed (because it is marked with @Transactional annotation). But when it is called inside processNextRegistrationMessage, because there is a TX available, the container doesn’t create a new one and uses existing TX. So if any exception occurs in sqlCommandHandlerService.persist method, it causes TX to be set to rollBackOnly (even if you catch the exception in the caller and ignore it).

To overcome this you can use propagation levels for transactions. Have a look at this to find out which propagation best suits your requirements.

Update; Read this!

Well after a colleague came to me with a couple of questions about a similar situation, I feel this needs a bit of clarification.
Although propagations solve such issues, you should be VERY careful about using them and do not use them unless you ABSOLUTELY understand what they mean and how they work. You may end up persisting some data and rolling back some others where you don’t expect them to work that way and things can go horribly wrong.


EDIT Link to current version of the documentation

Solution 2

The answer of Shyam was right. I already faced with this issue before. It’s not a problem, it’s a SPRING feature. «Transaction rolled back because it has been marked as rollback-only» is acceptable.

Conclusion

  • USE REQUIRES_NEW if you want to commit what did you do before exception (Local commit)
  • USE REQUIRED if you want to commit only when all processes are done (Global commit) And you just need to ignore «Transaction rolled back because it has been marked as rollback-only» exception. But you need to try-catch out side the caller processNextRegistrationMessage() to have a meaning log.

Let’s me explain more detail:

Question: How many Transaction we have? Answer: Only one

Because you config the PROPAGATION is PROPAGATION_REQUIRED so that the @Transaction persist() is using the same transaction with the caller-processNextRegistrationMessage(). Actually, when we get an exception, the Spring will set rollBackOnly for the TransactionManager so the Spring will rollback just only one Transaction.

Question: But we have a try-catch outside (), why does it happen this exception?
Answer Because of unique Transaction

  1. When persist() method has an exception
  2. Go to the catch outside

    Spring will set the rollBackOnly to true -> it determine we must 
    rollback the caller (processNextRegistrationMessage) also.
    
  3. The persist() will rollback itself first.

  4. Throw an UnexpectedRollbackException to inform that, we need to rollback the caller also.
  5. The try-catch in run() will catch UnexpectedRollbackException and print the stack trace

Question: Why we change PROPAGATION to REQUIRES_NEW, it works?

Answer: Because now the processNextRegistrationMessage() and persist() are in the different transaction so that they only rollback their transaction.

Thanks

Comments

  • I have this scenario:

    1. fetch (read and delete) a record from IncomingMessage table
    2. read record content
    3. insert something to some tables
    4. if an error (any exception) occurred in steps 1-3, insert an error-record to OutgoingMessage table
    5. 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" />
    

    Thanks in advance.

Recents

Related

I have this scenario:

  1. fetch (read and delete) a record from IncomingMessage table
  2. read record content
  3. insert something to some tables
  4. if an error (any exception) occurred in steps 1-3, insert an error-record to OutgoingMessage table
  5. 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" />

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

Предположим, что у нас есть сервис, который создает трех пользователей в рамках одной транзакции. Если что-то идет не так, выбрасывается 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 и узнаем, что же там происходит.

Transaction rolled back because it has been marked as rollback-only

This error is because multiple methods are in the same transaction in Spring, but one method has gone wrong. This time the thread stack went wrong directly, but when this error is thrown up, it is also involved in this transaction. methodtry catchLive, at this time the program can’t continue to throw exceptions, but the program can continue to go down, after the end, the program tries to commit the transaction, but this time the transaction has been marked as rollback only by spring, so this error.

Let’s look at the pseudo code below

Rest.java

@RequestMapping("rest")
public void rest() {
	Aservice.service();
}

AService.java

@Transactional
public void service() {
  try {
    BService.service();
  } catch(Exception ex) {
 }
}

BService.java

@Transactional
public void service() {
	// This method is abnormal
}

Both A and B are in the same transaction, but B made an error. At this time, the method error was first caught by spring. Spring marked the transaction as rollback only, and then A did not do any processing because of the catch, and continued to wait. The method is completed. At this time, after the service method of A ends, Spring tries to submit the transaction, and finds that the transaction has been marked as an exception, so it refuses to submit. This is also a kind of protection for our code by Spring. He should not want to back The business is obviously abnormal but the transaction is still submitted.

  • Ошибка соо 650 скания
  • Ошибка спираль фольксваген крафтер
  • Ошибка сохранения фотографий вк
  • Ошибка спираль на фольксваген пассат б6
  • Ошибка сохранения файла сохранение невозможно es проводник