Обработка исключений во время метода @Transactional в Spring

Я пытаюсь понять, как лучше всего обрабатывать постоянные (и потенциально другие) исключения в сочетании с Spring @Transactional. В этом посте я просто возьму простой пример регистрации пользователя, который может вызвать DataIntegrityViolationException из-за дублирования имени пользователя.

Следующие вещи, которые я пробовал, меня не удовлетворили:

1. Наивный подход: просто поймайте исключение

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
}

Это не работает в методе @Transactional, поскольку исключения сохраняемости не будут происходить до тех пор, пока транзакция не будет зафиксирована, что происходит вне моего метода службы в оболочке транзакции Spring.

2. Промойте EntityManager перед выходом.

Явно вызываю flush на EntityManager в конце моих методов обслуживания. Это вызовет запись в базу данных и вызовет исключение. Однако это потенциально неэффективно, так как теперь я должен позаботиться о том, чтобы не сбрасывать несколько раз во время запроса без причины. Мне также лучше никогда не забывать об этом, иначе исключения исчезнут в воздухе.

3. Сделайте два класса обслуживания

Поместите методы @Transactional в отдельный компонент Spring и попробуйте обработать их в основной службе. Это странно, так как я должен позаботиться о том, чтобы одна часть моего кода была на месте A, а другая - на месте B.

4. Обработайте DataIntegrityViolationException в контроллере.

Просто нет. Контроллер не занимается обработкой исключений из базы данных (оттенок оттенок оттенок).

5. Не ловите DataIntegrityViolationException

Я видел несколько ресурсов в Интернете, особенно в сочетании с Hibernate, предполагающих, что перехват этого исключения неправильный и что нужно просто проверить условие перед сохранением (т.е. проверить, существует ли имя пользователя с помощью ручного запроса). Это не работает в параллельном сценарии даже с транзакцией. Да, вы получите согласованность с транзакцией, но вы все равно получите DataIntegrityViolationException, когда «кто-то другой приходит первым». Поэтому это неприемлемое решение.

7. Не используйте декларативное управление транзакциями.

Используйте Spring TransactionTemplate вместо @Transactional. Это единственное отчасти удовлетворительное решение. Однако его использование несколько «неуклюже», чем «просто бросить @Transactional в метод», и даже документация Spring, кажется, подталкивает вас к использованию @Transactional.

Я хотел бы посоветовать, как лучше всего справиться с этой ситуацией. Есть ли лучшая альтернатива моему последнему предложенному решению?

Почему бы, как вариант (3), не использовать два метода в одном сервисе? Один из них - @Transactional, другой - DataIntegrityViolationException.

Markus Pscheidt 15.05.2018 09:27

Имея ту же проблему, я использую этот вариант (3), звучит как лучший подход. Не забудьте сделать этот второй метод также общедоступным, поскольку он должен быть «аспектированным» (возможно, в зависимости от того, какой фреймворк AOP вы используете).

Jens 03.04.2019 18:38

Привет, вы пробовали использовать @ControlerAdvice? Если нет, возможно, посмотрите здесь.

alexandrum 27.09.2020 16:59

По сути, это будет пункт 4. Согласно MVC, уровень контроллера не должен иметь дело с исключениями из базы данных.

diesieben07 28.09.2020 19:57
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
29
4
3 489
3

Ответы 3

В своем проекте я использую следующий подход.

  1. Пользовательская аннотация.
public @interface InterceptExceptions
{
}
  1. Бин и аспект в весеннем контексте.
<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. И, наконец, использование.
@Service
@Transactional
public class SomeService
{
  // ...

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

Вы можете использовать класс, аннотированный с помощью @ControllerAdvice или @RestControllerAdvice, для обработки исключений.

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

Этот метод помогает поддерживать чистый код.

У вас есть множество примеров:

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

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

Проголосовал в (3), например:

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

Другие вопросы по теме