【问题标题】:Spring: Uncaught Exception in Transactional MethodSpring:事务方法中未捕获的异常
【发布时间】:2019-12-31 00:51:53
【问题描述】:

我正在构建一个 RESTful API 并在我的 ProductController 中有以下更新方法:

@Slf4j
@RestController
@RequiredArgsConstructor
public class ProductController implements ProductAPI {

    private final ProductService productService;

    @Override
    public Product updateProduct(Integer id, @Valid UpdateProductDto productDto) throws ProductNotFoundException,
            ProductAlreadyExistsException {

        log.info("Updating product {}", id);
        log.debug("Update Product DTO: {}", productDto);

        Product product = productService.updateProduct(id, productDto);

        log.info("Updated product {}", id);
        log.debug("Updated Product: {}", product);

        return product;
    }

}

可抛出的异常来自具有以下实现的 ProductService:

package com.example.ordersapi.product.service.impl;

import com.example.ordersapi.product.api.dto.CreateProductDto;
import com.example.ordersapi.product.api.dto.UpdateProductDto;
import com.example.ordersapi.product.entity.Product;
import com.example.ordersapi.product.exception.ProductAlreadyExistsException;
import com.example.ordersapi.product.exception.ProductNotFoundException;
import com.example.ordersapi.product.mapper.ProductMapper;
import com.example.ordersapi.product.repository.ProductRepository;
import com.example.ordersapi.product.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {

    private final ProductRepository productRepository;
    private final ProductMapper productMapper;

    @Override
    public Set<Product> getAllProducts() {
        return StreamSupport.stream(productRepository.findAll().spliterator(), false)
                .collect(Collectors.toSet());
    }

    @Override
    public Product getOneProduct(Integer id) throws ProductNotFoundException {
        return productRepository.findById(id)
                .orElseThrow(() -> new ProductNotFoundException(id));
    }

    @Override
    public Product createProduct(CreateProductDto productDto) throws ProductAlreadyExistsException {
        Product product = productMapper.createProductDtoToProduct(productDto);
        Product savedProduct = saveProduct(product);

        return savedProduct;
    }

    private Product saveProduct(Product product) throws ProductAlreadyExistsException {
        try {
            return productRepository.save(product);
        } catch (DataIntegrityViolationException ex) {
            throw new ProductAlreadyExistsException(product.getName());
        }
    }

    /**
     * Method needs to be wrapped in a transaction because we are making two database queries:
     *  1. Finding the Product by id (read)
     *  2. Updating found product (write)
     *
     *  Other database clients might perform a write operation over the same entity between our read and write,
     *  which would cause inconsistencies in the system. Thus, we have to operate over a snapshot of the database and
     *  commit or rollback (and probably re-attempt the operation?) depending if its state has changed meanwhile.
     */
    @Override
    @Transactional
    public Product updateProduct(Integer id, UpdateProductDto productDto) throws ProductNotFoundException,
            ProductAlreadyExistsException {

        Product foundProduct = getOneProduct(id);
        boolean productWasUpdated = false;

        if (productDto.getName() != null && !productDto.getName().equals(foundProduct.getName())) {
            foundProduct.setName(productDto.getName());
            productWasUpdated = true;
        }

        if (productDto.getDescription() != null && !productDto.getDescription().equals(foundProduct.getDescription())) {
            foundProduct.setDescription(productDto.getDescription());
            productWasUpdated = true;
        }

        if (productDto.getImageUrl() != null && !productDto.getImageUrl().equals(foundProduct.getImageUrl())) {
            foundProduct.setImageUrl(productDto.getImageUrl());
            productWasUpdated = true;
        }

        if (productDto.getPrice() != null && !productDto.getPrice().equals(foundProduct.getPrice())) {
            foundProduct.setPrice(productDto.getPrice());
            productWasUpdated = true;
        }

        Product updateProduct = productWasUpdated ? saveProduct(foundProduct) : foundProduct;

        return updateProduct;
    }

}

因为我在我的数据库中将 NAME 列设置为 UNIQUE,所以当使用已经存在的名称发出更新时,存储库保存方法将抛出 DataIntegrityViolationException。在 createProduct 方法中它可以正常工作,但在 updateProduct 中,对私有方法 saveProduct 的调用无论如何都不会捕获异常,因此 DataIntegrityViolationException 会冒泡到控制器。

我知道这是因为我将代码包装在事务中,因为删除 @Transactional “解决”了问题。我认为这与 Spring 使用代理将方法包装在事务中的事实有关,因此控制器中的服务调用实际上并没有(直接)调用服务方法。尽管如此,我不明白为什么当它适用于ProductNotFoundException 时,它会忽略catch 分支来抛出ProductAlreadyExistsException

我知道我还可以再次访问数据库,尝试按名称查找产品,如果不存在,我会尝试将我的实体保存回来。但这会使一切变得更加低效。

我也可以在控制器层捕获DataIntegrityViolationException 并将ProductAlreadyExistsException 扔在那里,但我会在那里暴露持久层的细节,这似乎不合适。

有没有办法在服务层中处理这一切,就像我现在正在尝试做的那样?

P.S.:将逻辑外包给一个新方法并在内部使用它似乎可以工作,但这只是因为对 this 的调用实际上不会被事务管理器拦截代理,因此实际上没有执行任何事务

【问题讨论】:

  • 如果要求不回滚 DataIntegrityViolationException ,您可以使用事务注释的docs.spring.io/spring-framework/docs/current/javadoc-api/org/… 属性。事务中抛出的运行时异常 (DataIntegrityVIolationException) 会将事务标记为回滚。
  • @Transactional(noRollbackFor = DataIntegrityViolationException.class)
  • 感谢您的回复。我已经尝试过 @Transactional(noRollbackFor = DataIntegrityViolationException.class) 但没有成功。也许是因为为 CrudRepository 提供的实现也用 @Transactional 注释?
  • 你确定抛出的异常是 DataIntegrityViolationException 吗?请分享堆栈跟踪
  • 是的,这是一个 DataIntegrityViolationException。我什至将 catch 子句中的 DataIntegrityViolationException 替换为 Exception ,但它仍然无法捕获它。但 Piotr 的回答最终解决了问题。无论如何,感谢您的帮助!

标签: spring hibernate spring-boot spring-data-jpa spring-transactions


【解决方案1】:

要使其正常工作,您需要更改saveProduct 方法,如下所示:

        try {
            return productRepository.saveAndFlush(product);
        } catch (DataIntegrityViolationException ex) {
            throw new ProductAlreadyExistsException(product.getName());
        }

当您使用save() 方法时,与保存操作关联的数据将不会刷新到您的数据库,除非并且直到显式调用flush()commit() 方法。这会导致 DataIntegrityViolationException 稍后被抛出,因此您不会在上面的狙击手中捕获它。

另一方面,saveAndFlush() 方法会立即刷新数据。这应该会触发预期的异常被立即捕获并重新抛出为ProductAlreadyExistsException

【讨论】:

  • 谢谢!我扩展了 JpaRepository 而不是 CrudRepository,替换了 saveAndFlush 的保存方法,最后在方法中添加了@Transactional(rollbackFor = ProductAlreadyExistsException.class),它现在可以工作了!
  • 没问题,很高兴它有帮助。我还想确认一件事——@Transactional(rollbackFor = ProductAlreadyExistsException.class) 只会在ProductAlreadyExistsException 被抛出或其他扩展它的异常时回滚事务。您必须注意,如果在此事务中抛出任何其他异常(NullPointerExceptionIllegalStateExceptionIllegalArgumentException - 您的名字),无论如何都会提交。你可能没问题,只是想确保你意识到这一点。
  • 我认为默认情况下,如果抛出未经检查的异常,Spring 事务将回滚。我不确定我是否以这种方式覆盖了这种行为......我在很多地方都看到,如果我们也想在检查异常上回滚它们,我们可以使用@Transactional(rollbackFor = Exception.class)。我只是决定更明确一点,但你是对的,这样我可能会留出一些错误余地......无论哪种方式,我都需要指定 ProductAlreadyExistsException Exception 才能正常工作
  • 是的,默认情况下它会在 RuntimeException 上回滚,因此您必须覆盖 rollbackFor 参数,因为 ProductAlreadyExistsException 不会扩展 RuntimeException。问题是您是否只想在ProductAlreadyExistsException 或任何例外上进行具体化和回滚。决定权在你手中 :)
  • 完全正确 :) 我认为如果我使用 @Transactional(rollbackFor = Exception.class) 会更安全 另一件让我感到惊讶的事情是我确实必须在回滚选项中指定 ProductAlreadyExistsException 但是我从来不用指定ProductNotFoundException,即使它们处于同一水平......我试图找到一个解释,但无法理解那里可能发生的事情
猜你喜欢
  • 2015-02-27
  • 2018-06-06
  • 2015-04-22
  • 2017-03-29
  • 2016-11-25
  • 1970-01-01
  • 2021-02-27
  • 2011-10-27
  • 1970-01-01
相关资源
最近更新 更多