@Transactional 不会通过调用抛出 RuntimeException 的外部方法来回滚

@Transactional doesn't rollback by calling an External method that throw an RuntimeException

我正在玩 Spring 和 @Transactional 注释。我正在做一个简单的实验来测试这个注释的行为。这些是我超级简单的 java 类:

我的域名:

package com.xxx.springdemo.transactionalAnnotation.domain;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;

@Data
@Entity
public class Counter {

    @Id
    int id;
    int count = 0;

}

我的服务:

package com.xxx.springdemo.transactionalAnnotation.services;

import com.xxx.springdemo.transactionalAnnotation.domain.Counter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class TransactionalService {

    public void throwException(Counter counter) {
        counter.setCount(1);
        throw new RuntimeException("Runtime exception");
    }

    public void dontThrowException(Counter counter) {
        counter.setCount(2);
    }
}

和我的控制器:

package com.xxx.springdemo.transactionalAnnotation.controllers;

import com.xxx.springdemo.transactionalAnnotation.domain.Counter;
import com.xxx.springdemo.transactionalAnnotation.services.TransactionalService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class TransactionController {

    private final TransactionalService transactionalService;

    @GetMapping("test-transactional")
    public void testTransactional() {
        Counter counter = new Counter();
        try {
            transactionalService.throwException(counter);
        } catch (Exception e) {
            System.out.println("Exception caught: " + e.getMessage());
        }
        System.out.println("Variable hasn't changed (counter = " + counter.getCount() + ")");
    }

    @GetMapping("test-transactional-2")
    public void testTransactionalWithoutException() {
        Counter counter = new Counter();
        try {
            transactionalService.dontThrowException(counter);
        } catch (Exception e) {
            System.out.println("Exception caught: " + e.getMessage());
        }
        System.out.println("Variable changed (counter = " + counter.getCount() + ")");
    }
}

代码应该是不言自明的。我希望打印:

Variable hasn't changed (counter = 0)

以防调用 test-transactional 端点。我得到的是:

Variable hasn't changed (counter = 1)

这意味着在服务方法 throwException 中,计数 属性 在抛出异常后不会回滚。

P.S。这是我的 build.gradle 文件:

plugins {
    id 'org.springframework.boot' version '2.6.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus'

    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.h2database:h2'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

Spring 的 @Transactional 只是管理底层 JPA 事务的一种声明方式。所以它取决于 JPA 的回滚行为,它由 spec. 定义如下:

For both transaction-scoped persistence contexts and for extended persistence contexts that are joined to the current transaction, transaction rollback causes all pre-existing managed instances and removed instances[29] to become detached. The instances' state will be the state of the instances at the point at which the transaction was rolled back. Transaction rollback typically causes the persistence context to be in an inconsistent state at the point of rollback.

Hibernate docs也提到了相同的:

Rolling back the database transaction does not put your business objects back into the state they were at the start of the transaction. This means that the database state and the business objects will be out of sync. Usually, this is not a problem because exceptions are not recoverable and you will have to start over after rollback anyway.

因此,Counter 仍将具有事务回滚之前的状态(即 count=1),这是预期的回滚行为。

对于数据库中已经存在的实体,您可以使用entityManager.refresh()手动将其状态恢复到与数据库相同的状态,或者只需使用entityManager.get()或JPQL再次从数据库。

对于数据库中不存在的实体,只需re-execute代码重新创建即可。

所以这里的回滚只是意味着数据库中没有任何更新。这并不意味着它将对象状态恢复到执行@Transactional方法之前的那一刻。