首页 > 解决方案 > 使用 Spring Data JPA 时如何处理 ObjectOptimisticLockingFailureException 和 StaleObjectStateException?

问题描述

我正在开发一个小型 RESTful 服务,它使用定义为和扩展的域对象的spring-boot-starter-data-jpa依赖关系。该服务使客户能够重置他们忘记的密码。作为第一部分,从客户端接收电子邮件的方法检查具有此类电子邮件的用户是否存在且未被禁用,如果存在 - 为该用户创建一个临时重置令牌,更新它并将带有令牌的特殊链接发送到客户:User@EntityUsersRepositoryCrudRepository

@RestController
@RequestMapping
@Validated
public class ResetTokenController {

    private final UsersRepository usersRepo;
    private final JavaMailSender mailSender;

    @Autowired
    public ResetTokenController(UsersRepository usersRepo, JavaMailSender mailSender) {
        this.usersRepo = usersRepo;
        this.mailSender = mailSender;
    }

    @GetMapping(path = "/auth/reset-token", produces = "application/json")
    @ResponseStatus(HttpStatus.OK)
    public Map<String, String> sendMeResetToken(@RequestParam
                                                @NotBlank(message = "Parameter `email` must not be empty")
                                                @Email(message = "Parameter `email` is not valid")
                                                         String email) {
        // step #1: fetch user with given e-mail
        User user = usersRepo.findByEmail(email)
                .filter(User::isEnabled)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
                        "User with given e-mail not found or currently disabled"));

        // step #2: create a reset-token for user
        String token = UUID.randomUUID().toString();
        user.setResetToken(token);
        user.setExpBefore(LocalDateTime.now().plusHours(24));

        // step #3: save user with his new token and send instructions
        usersRepo.save(user);
        MimeMessage message = constructHtmlMessage(token, email);
        mailSender.send(message);
        return Collections.singletonMap("message", "The tokenized link for resetting your password " +
                "was sent to the provided e-mail and will be active for the next 24 hours.");
    }

    // a helper method to construct the mime-message
    private MimeMessage constructHtmlMessage(String token, String email) {

        // here, we build a link based on the token that's been just created
        // and include it in the message along with the instructions details 
        
        return mimeMessage;
    }

}

然而,上面的代码有一个潜在的缺陷。假设有人(例如,我们服务的管理员)意外地从另一个线程进行干预,并在步骤 #1 和步骤 #3 之间的中间位置删除了正在从数据库中更新的用户。为了避免这种情况下可能出现的不一致,我首先尝试用 包装整个方法@Transactional(isolation = Isolation.REPEATABLE_READ),但很快发现这样做没有效果,因为在这种情况下无论如何都会抛出ObjectOptimisticLockingFailureExceptionwith 嵌套,无论方法是否注释。StaleObjectStateException@Transactional

现在,问题是:如何处理这种异常?就个人而言,我不希望将其翻译给客户端,到目前为止,我做出的唯一决定是将方法的内部结构放入 while 循环中,如下所示:

public Map<String, String> sendMeResetToken(...) {
    while (true) {
        try {
            // #1: fetch user with given e-mail
            // #2: create a reset-token for user
            // #3: update user and send token
            // leave the loop on successful update
            return Collections.singletonMap("message", "The tokenized link for resetting your password " +
                    "was sent to the provided e-mail and will be active for the next 24 hours.");
        } catch (ObjectOptimisticLockingFailureException ignore) {
            // continue the loop and make another attempt from the start
        }
    }
}

是否有任何解决方案(也许,更优雅的不使用 while 循环)来处理这种情况,并且在更一般的意义上,在生产中通常如何处理这些异常(我的意思是,ObjectOptimisticLockingFailureException和)?StaleObjectStateException

标签: javaspringhibernateconcurrencyspring-data-jpa

解决方案


推荐阅读