1、问题描述
从关于sping事务不起效问题中,我们能看出事务控制的基本流程,就是拦截器拦截方法的执行,然后对方法的名称和类型到advices中查找是否有匹配的切面逻辑,并组成一个拦截器list,并顺序执行。
在spring中,发现了一个奇怪的问题,就是在设置事务的切面逻辑表达式中设置了readOnly之后,在对应开启了readOnly事务的方法中,执行写入或更新操作,但并没有如所料报异常或者返回更新失败,反而返回的是更新成功。但最可怕的是,数据库并没有更新数据,这样会造成了假更新或假插入的问题。跟踪多次断点之后发现,插入或更新操作都如所料进行了操作,返回的对象也是更新后的成功对象,但实际上此时数据只在客户端内存页中,并没有flush到服务器内存,因此也不会打印对应的SQL语句。
因此希望能够分析出原因,至少在事务的执行过程中,如果事务的状态是readOnly下进行更新或保存时,报异常或者返回更新失败,能告知调用者无法这样调用。
2、源码分析
在调查资料的过程中,发现了一个博主写的非常清晰的笔记,值得学习:
在源码断点debug后,最终的发现是在开启了readOnly的事务状态下,在执行JPASimpleRepo的doSave的方法中,在save方法后,调用flush方法时进入了拦截器逻辑,但最后并没有执行flush,或者说并没有真正执行sql语句。
以下是核心的invokeWithinTransaction方法全貌
/**
* General delegate for around-advice-based subclasses, delegating to several other template
* methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}
* as well as regular {@link PlatformTransactionManager} implementations and
* {@link ReactiveTransactionManager} implementations for reactive return types.
* @param method the Method being invoked
* @param targetClass the target class that we're invoking the method on
* @param invocation the callback to use for proceeding with the target invocation
* @return the return value of the method, if any
* @throws Throwable propagated from the target invocation
*/
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final TransactionManager tm = determineTransactionManager(txAttr);
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new TransactionUsageException(
"Unsupported annotated transaction on suspending function detected: " + method +
". Use TransactionalOperator.transactional extensions instead.");
}
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
}
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
else {
Object result;
final ThrowableHolder throwableHolder = new ThrowableHolder();
// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
try {
Object retVal = invocation.proceedWithInvocation();
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
}
以下是自行封装的类org.fsdcic.common.springjpa.service.impl.BaseServiceImpl的save逻辑
private int doSave(RefObject<T> t, StringBuilder sb) {
int result = -1;
if (this.isValidEnity((IBaseEntity)t.argvalue, sb)) {
T tmp = (IBaseEntity)this.getRepository().save(t.argvalue);
if (tmp != null) {
//问题就出在此处,getRepository得到的是一个代理对象,在调用flush的时候会经过各种切面逻辑,但最终在readOnly状态下,并没有任何的响应
this.getRepository().flush();
t.argvalue = tmp;
result = 0;
}
}
return result;
}
在em.flush --> org.hibernate.internal.SessionImpl#doFlush方法中,发布了FlushEvent事件。并由org.hibernate.event.internal.DefaultFlushEventListener#onFlush接收,处理到以下三个核心方法时,输出sql
再进一步对图中的三个方法进行分析:
- flushEeverythingToExecutions:会发布FlushEntityEvent事件,org.hibernate.event.internal.DefaultFlushEntityEventListener#onFlushEntity中处理,在处理过程中会查看是否存在dirty数据,并把需要提交到服务端的数据,逐一放到session的actionQueue中。(关键就在此,判断是否dirty数据时,会判断事务是否readOnly状态,否则不会放入queue中)
- performExecutions:执行queue中的event(正是由于上一步,因此这里执行了一个空,造成了flush失败的假象)
- postFlush:在flush后,需要处理一些对象的消除等操作
3、初步结论:
由于JPA会对操作数据进行flush操作,在该过程中调用的接口方法是javax.persistence.EntityManager#flush,该接口方法的默认实现是Hibernate,因此我们看到不管是flushEvent的发布,或是dirty数据的检查,都是hibernate自己的实现,因此若我们对实现进行更改,会造成侵入式的改造,让日后框架的版本迭代带来风险。因此我们应该思考的是,如果在JPA和Spring层面上解决该问题,利用事务切点的方式。
4、最后解决方式:
通过扩展事务配置的切面类:TransactionInterceptor,在关键方法createTransactionIfNecessary进行判断:若当下存在事务,且事务状态为readOnly的情况下,若新建的事务信息状态为非readOnly,则抛出异常。测试成功!完结撒花
(注意ReadOnlyCheckTransactionInterceptor需要配置好切点,以免无法进入逻辑 ,除了切入service类的方法外,repo方法的切入最好也进行设置,能进一步降低假更新操作带来的风险):
public class ReadOnlyCheckTransactionInterceptor extends TransactionInterceptor {
@Override
protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, String joinpointIdentification) {
boolean needToCheck = TransactionSynchronizationManager.isActualTransactionActive() && TransactionSynchronizationManager.isCurrentTransactionReadOnly();
TransactionInfo currentTransactionInfo = super.createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
if (needToCheck) {
DefaultTransactionStatus transactionStatus = (DefaultTransactionStatus) (currentTransactionInfo.getTransactionStatus());
if (!transactionStatus.isNewTransaction() && !transactionStatus.isReadOnly()) {
String errorMsg = "当前已开启的事务状态为readOnly,现操作沿用已有事务,但操作所定义的事务状态非readOnly!请确认是否在get、find方法中执行数据更新操作";
ReadOnlyTransactionCheckException ex = new ReadOnlyTransactionCheckException(errorMsg);
// //回滚当前已经创建的事务
// completeTransactionAfterThrowing(currentTransactionInfo, ex);
throw ex;
}
}
return currentTransactionInfo;
}
题外话:
@FunctionalInterface注解:实际上就是可以接受一个lamda表达式(或function,调用某个具体方法的句柄)
例如在事务核心方法invokeWithinTransaction中的调用例子:
- 方法的入参类型为一个被FunctionalInterface注解的interface类型,而该接口中也仅有一个方法
- 调用该方法时,接口类型的入参是一个lamda表达式(function),是一个proceed方法的句柄
- 在invokeWithinTransaction方法中调用的是接口中的方法,但实际上调用执行的是句柄
实际上这样设计就是体现了interceptor基于代理类的递归循环的高阶版实现:invocation::proceed方法,是一个在主抽象类ReflectiveMethodInvocation定义的proceed方法(这里的invocation也是继承于ReflectiveMethodInvocation的Cglib代理类或JDK代理类,spring中的bean都是代理对象)。同时该类中声明了一个index索引,并掌握着interceptor chain对象,只要proceed方法被调用一次,就会执行++index,并从chain中取下一个interceptor后,调用其invoke方法(把自己作为入参传递入去给inteceptor),让inteceptor去决定是否继续走下去就执行proceed方法,从而实现对方法的逻辑增强。
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
.......
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
.......
retVal = invocation.proceedWithInvocation();
}
//接口名字怎么取都行,这里写的callback就是为了体现语义,回调methodInvocation.proceed方法
@FunctionalInterface
protected interface InvocationCallback {
//该方法实际上实现了松解耦,只要句柄的返回参数相同以及入参类型和数量相同即可
@Nullable
Object proceedWithInvocation() throws Throwable;
}