简体   繁体   中英

Handling exceptions during a @Transactional method in Spring

I am trying to figure out how to best handle persistence (and potentially other) exceptions in combination with Spring's @Transactional . For this post I am just going to take the simple example of a user registration, which can cause DataIntegrityViolationException due to duplicate username.

The following things I have tried and they are not really satisfactory to me:

1. Naive approach: Just catch the Exception

val entity = UserEntity(...)
try {
    repo.save(entity)
} catch (e: DataIntegrityViolationException) {
    // not included: some checks for which constraint failed
    throw DuplicateUsername(username) // to be handled by the controller
}

This does not work when in a @Transactional method, since the persistence exceptions won't happen until the transaction is commited, which happens outside my service method in the spring transaction wrapper.

2. Flush the EntityManager before exiting

Explicitly call flush on the EntityManager at the end of my service methods. This will force the write to the database and as such trigger the exception. However it is potentially inefficient, as I now must take care to not flush multiple times during a request for no reason. I also better not ever forget it or exceptions will disappear into thin air.

3. Make two service classes

Put the @Transactional methods in a separate spring bean and try-catch around them in the main service. This is weird, as I must take care to do one part of my code in place A and the other in place B.

4. Handle DataIntegrityViolationException in the controller

Just... no. The controller has no business (hue hue hue) in handling exceptions from the database.

5. Don't catch DataIntegrityViolationException

I have seen several resources on the web, especially in combination with Hibernate, suggesting that catching this exception is wrong and that one should just check the condition before saving (ie check if the username exists with a manual query). This does not work in a concurrent scenario, even with a transaction. Yes, you will get consistency with a transaction, but you'll still get DataIntegrityViolationException when "someone else comes first". Therefor this is not an acceptable solution.

7. Do not use declarative transaction management

Use Spring's TransactionTemplate instead of @Transactional . This is the only somewhat satisfactory solution. However it is quite a bit more "clunky" to use than "just throwing @Transactional on the method" and even the Spring documentation seems to nudge you towards using @Transactional .

I would like some advice about how to best handle this situation. Is there a better alternative to my last proposed solution?

I use the following approach in my project.

  1. Custom annotation.
public @interface InterceptExceptions
{
}
  1. Bean and aspect at spring context.
<beans ...>
  <bean id="exceptionInterceptor" class="com.example.ExceptionInterceptor"/>

  <aop:config>
    <aop:aspect ref="exceptionInterceptor">
      <aop:pointcut id="exception" expression="@annotation(com.example.InterceptExceptions)"/>
      <aop:around pointcut-ref="exception" method="catchExceptions"/>
    </aop:aspect>
  </aop:config>
</beans>
import org.aspectj.lang.ProceedingJoinPoint;

public class ExceptionInterceptor
{

  public Object catchExceptions(ProceedingJoinPoint joinPont)
  {
    try
    {
      return joinPont.proceed();
    }
    catch (MyException e)
    {
      // ...
    }
  }  
}
  1. And finally, usage.
@Service
@Transactional
public class SomeService
{
  // ...

  @InterceptExceptions
  public SomeResponse doSomething(...)
  {
    // ...
  }
}

Voted to (3),like:

@Service
public class UserExtService extend UserService{
}

@Service
public class UserService {
    @Autowired
    UserExtService userExtService;

    public int saveUser(User user) {
        try {
            return userExtService.save(user);
        } catch (DataIntegrityViolationException e) {
          throw DuplicateUsername(username);// GlobalExceptionHandler to response
        }
        return 0;
    }

    @Transactional(rollbackFor = Exception.class)
    public int save(User user) {
        //...
        return 0;
    }
}

You can use a class annotated with @ControllerAdvice or @RestControllerAdvice to handle the exceptions

When a controller throw a exception you can catch it at this class and change the response status to a suitable one or add an extra info of the exception

This method helps you to maintain a clean code

You have numerous examples:

https://www.javainuse.com/spring/boot-exception-handling

https://dzone.com/articles/best-practice-for-exception-handling-in-spring-boo

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM